Skip to content

Commit

Permalink
Rewrite relative import extensions with flag (#59767)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewbranch authored Sep 27, 2024
1 parent 9d98874 commit bd3d700
Show file tree
Hide file tree
Showing 54 changed files with 3,132 additions and 89 deletions.
51 changes: 50 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ import {
getAllJSDocTags,
getAllowSyntheticDefaultImports,
getAncestor,
getAnyExtensionFromPath,
getAssignedExpandoInitializer,
getAssignmentDeclarationKind,
getAssignmentDeclarationPropertyAccessKind,
Expand All @@ -259,6 +260,7 @@ import {
getCombinedLocalAndExportSymbolFlags,
getCombinedModifierFlags,
getCombinedNodeFlags,
getCommonSourceDirectoryOfConfig,
getContainingClass,
getContainingClassExcludingClassDecorators,
getContainingClassStaticBlock,
Expand Down Expand Up @@ -352,6 +354,8 @@ import {
getPropertyAssignmentAliasLikeExpression,
getPropertyNameForPropertyNameNode,
getPropertyNameFromType,
getRelativePathFromDirectory,
getRelativePathFromFile,
getResolutionDiagnostic,
getResolutionModeOverride,
getResolveJsonModule,
Expand Down Expand Up @@ -414,6 +418,7 @@ import {
hasSyntacticModifiers,
hasType,
HeritageClause,
hostGetCanonicalFileName,
Identifier,
identifierToKeywordKind,
IdentifierTypePredicate,
Expand Down Expand Up @@ -693,6 +698,7 @@ import {
isParenthesizedTypeNode,
isPartOfParameterDeclaration,
isPartOfTypeNode,
isPartOfTypeOnlyImportOrExportDeclaration,
isPartOfTypeQuery,
isPlainJsFile,
isPrefixUnaryExpression,
Expand Down Expand Up @@ -994,6 +1000,7 @@ import {
ShorthandPropertyAssignment,
shouldAllowImportingTsExtension,
shouldPreserveConstEnums,
shouldRewriteModuleSpecifier,
Signature,
SignatureDeclaration,
SignatureFlags,
Expand All @@ -1007,6 +1014,7 @@ import {
skipTypeParentheses,
some,
SourceFile,
sourceFileMayBeEmitted,
SpreadAssignment,
SpreadElement,
startsWith,
Expand Down Expand Up @@ -4665,6 +4673,45 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
error(errorNode, Diagnostics.An_import_path_can_only_end_with_a_0_extension_when_allowImportingTsExtensions_is_enabled, tsExtension);
}
}
else if (
compilerOptions.rewriteRelativeImportExtensions
&& !(location.flags & NodeFlags.Ambient)
&& !isDeclarationFileName(moduleReference)
&& !isLiteralImportTypeNode(location)
&& !isPartOfTypeOnlyImportOrExportDeclaration(location)
) {
const shouldRewrite = shouldRewriteModuleSpecifier(moduleReference, compilerOptions);
if (!resolvedModule.resolvedUsingTsExtension && shouldRewrite) {
error(
errorNode,
Diagnostics.This_relative_import_path_is_unsafe_to_rewrite_because_it_looks_like_a_file_name_but_actually_resolves_to_0,
getRelativePathFromFile(getNormalizedAbsolutePath(currentSourceFile.fileName, host.getCurrentDirectory()), resolvedModule.resolvedFileName, hostGetCanonicalFileName(host)),
);
}
else if (resolvedModule.resolvedUsingTsExtension && !shouldRewrite && sourceFileMayBeEmitted(sourceFile, host)) {
error(
errorNode,
Diagnostics.This_import_uses_a_0_extension_to_resolve_to_an_input_TypeScript_file_but_will_not_be_rewritten_during_emit_because_it_is_not_a_relative_path,
getAnyExtensionFromPath(moduleReference),
);
}
else if (resolvedModule.resolvedUsingTsExtension && shouldRewrite) {
const redirect = host.getResolvedProjectReferenceToRedirect(sourceFile.path);
if (redirect) {
const ignoreCase = !host.useCaseSensitiveFileNames();
const ownRootDir = host.getCommonSourceDirectory();
const otherRootDir = getCommonSourceDirectoryOfConfig(redirect.commandLine, ignoreCase);
const rootDirPath = getRelativePathFromDirectory(ownRootDir, otherRootDir, ignoreCase);
const outDirPath = getRelativePathFromDirectory(compilerOptions.outDir || ownRootDir, redirect.commandLine.options.outDir || otherRootDir, ignoreCase);
if (rootDirPath !== outDirPath) {
error(
errorNode,
Diagnostics.This_import_path_is_unsafe_to_rewrite_because_it_resolves_to_another_project_and_the_relative_path_between_the_projects_output_files_is_not_the_same_as_the_relative_path_between_its_input_files,
);
}
}
}
}

if (sourceFile.symbol) {
if (errorNode && resolvedModule.isExternalLibraryImport && !resolutionExtensionIsTSOrJson(resolvedModule.extension)) {
Expand Down Expand Up @@ -50871,6 +50918,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return ["__propKey"];
case ExternalEmitHelpers.AddDisposableResourceAndDisposeResources:
return ["__addDisposableResource", "__disposeResources"];
case ExternalEmitHelpers.RewriteRelativeImportExtension:
return ["__rewriteRelativeImportExtension"];
default:
return Debug.fail("Unrecognized helper");
}
Expand Down Expand Up @@ -52911,7 +52960,7 @@ function createBasicNodeBuilderModuleSpecifierResolutionHost(host: TypeCheckerHo
getCurrentDirectory: () => host.getCurrentDirectory(),
getSymlinkCache: maybeBind(host, host.getSymlinkCache),
getPackageJsonInfoCache: () => host.getPackageJsonInfoCache?.(),
useCaseSensitiveFileNames: maybeBind(host, host.useCaseSensitiveFileNames),
useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(),
redirectTargetsMap: host.redirectTargetsMap,
getProjectReferenceRedirect: fileName => host.getProjectReferenceRedirect(fileName),
isSourceOfProjectReferenceRedirect: fileName => host.isSourceOfProjectReferenceRedirect(fileName),
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,15 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
defaultValueDescription: false,
transpileOptionValue: undefined,
},
{
name: "rewriteRelativeImportExtensions",
type: "boolean",
affectsSemanticDiagnostics: true,
affectsBuildInfo: true,
category: Diagnostics.Modules,
description: Diagnostics.Rewrite_ts_tsx_mts_and_cts_file_extensions_in_relative_import_paths_to_their_JavaScript_equivalent_in_output_files,
defaultValueDescription: false,
},
{
name: "resolvePackageJsonExports",
type: "boolean",
Expand Down
16 changes: 16 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3964,6 +3964,18 @@
"category": "Error",
"code": 2875
},
"This relative import path is unsafe to rewrite because it looks like a file name, but actually resolves to \"{0}\".": {
"category": "Error",
"code": 2876
},
"This import uses a '{0}' extension to resolve to an input TypeScript file, but will not be rewritten during emit because it is not a relative path.": {
"category": "Error",
"code": 2877
},
"This import path is unsafe to rewrite because it resolves to another project, and the relative path between the projects' output files is not the same as the relative path between its input files.": {
"category": "Error",
"code": 2878
},

"Import declaration '{0}' is using private name '{1}'.": {
"category": "Error",
Expand Down Expand Up @@ -5947,6 +5959,10 @@
"category": "Message",
"code": 6420
},
"Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files.": {
"category": "Message",
"code": 6421
},

"The expected type comes from property '{0}' which is declared here on type '{1}'": {
"category": "Message",
Expand Down
31 changes: 31 additions & 0 deletions src/compiler/factory/emitHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
isCallExpression,
isComputedPropertyName,
isIdentifier,
JsxEmit,
memoize,
ObjectLiteralElementLike,
ParameterDeclaration,
Expand Down Expand Up @@ -139,6 +140,8 @@ export interface EmitHelperFactory {
// 'using' helpers
createAddDisposableResourceHelper(envBinding: Expression, value: Expression, async: boolean): Expression;
createDisposeResourcesHelper(envBinding: Expression): Expression;
// --rewriteRelativeImportExtensions helpers
createRewriteRelativeImportExtensionsHelper(expression: Expression): Expression;
}

/** @internal */
Expand Down Expand Up @@ -189,6 +192,8 @@ export function createEmitHelperFactory(context: TransformationContext): EmitHel
// 'using' helpers
createAddDisposableResourceHelper,
createDisposeResourcesHelper,
// --rewriteRelativeImportExtensions helpers
createRewriteRelativeImportExtensionsHelper,
};

/**
Expand Down Expand Up @@ -682,6 +687,17 @@ export function createEmitHelperFactory(context: TransformationContext): EmitHel
context.requestEmitHelper(disposeResourcesHelper);
return factory.createCallExpression(getUnscopedHelperName("__disposeResources"), /*typeArguments*/ undefined, [envBinding]);
}

function createRewriteRelativeImportExtensionsHelper(expression: Expression) {
context.requestEmitHelper(rewriteRelativeImportExtensionsHelper);
return factory.createCallExpression(
getUnscopedHelperName("__rewriteRelativeImportExtension"),
/*typeArguments*/ undefined,
context.getCompilerOptions().jsx === JsxEmit.Preserve
? [expression, factory.createTrue()]
: [expression],
);
}
}

/** @internal */
Expand Down Expand Up @@ -1422,6 +1438,21 @@ const disposeResourcesHelper: UnscopedEmitHelper = {
});`,
};

const rewriteRelativeImportExtensionsHelper: UnscopedEmitHelper = {
name: "typescript:rewriteRelativeImportExtensions",
importName: "__rewriteRelativeImportExtension",
scoped: false,
text: `
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
if (typeof path === "string" && /^\\.\\.?\\//.test(path)) {
return path.replace(/\\.(tsx)$|((?:\\.d)?)((?:\\.[^./]+?)?)\\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
});
}
return path;
};`,
};

/** @internal */
export const asyncSuperHelper: EmitHelper = {
name: "typescript:async-super",
Expand Down
18 changes: 10 additions & 8 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
forEach,
forEachAncestorDirectory,
formatMessage,
getAllowImportingTsExtensions,
getAllowJSCompilerOption,
getAnyExtensionFromPath,
getBaseFileName,
Expand Down Expand Up @@ -1484,7 +1485,7 @@ export function resolveModuleName(moduleName: string, containingFile: string, co
* 'typings' entry or file 'index' with some supported extension
* - Classic loader will only try to interpret '/a/b/c' as file.
*/
type ResolutionKindSpecificLoader = (extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState) => Resolved | undefined;
type ResolutionKindSpecificLoader = (extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState, packageJsonValue?: string) => Resolved | undefined;

/**
* Any module resolution kind can be augmented with optional settings: 'baseUrl', 'paths' and 'rootDirs' - they are used to
Expand Down Expand Up @@ -2094,13 +2095,14 @@ function loadModuleFromFileNoImplicitExtensions(extensions: Extensions, candidat
* module specifiers written in source files - and so it always allows the
* candidate to end with a TS extension (but will also try substituting a JS extension for a TS extension).
*/
function loadFileNameFromPackageJsonField(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {
function loadFileNameFromPackageJsonField(extensions: Extensions, candidate: string, packageJsonValue: string | undefined, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {
if (
extensions & Extensions.TypeScript && fileExtensionIsOneOf(candidate, supportedTSImplementationExtensions) ||
extensions & Extensions.Declaration && fileExtensionIsOneOf(candidate, supportedDeclarationExtensions)
) {
const result = tryFile(candidate, onlyRecordFailures, state);
return result !== undefined ? { path: candidate, ext: tryExtractTSExtension(candidate) as Extension, resolvedUsingTsExtension: undefined } : undefined;
const ext = tryExtractTSExtension(candidate) as Extension;
return result !== undefined ? { path: candidate, ext, resolvedUsingTsExtension: packageJsonValue ? !endsWith(packageJsonValue, ext) : undefined } : undefined;
}

if (state.isConfigLookup && extensions === Extensions.Json && fileExtensionIs(candidate, Extension.Json)) {
Expand Down Expand Up @@ -2316,7 +2318,7 @@ function loadEntrypointsFromExportMap(
}
const resolvedTarget = combinePaths(scope.packageDirectory, target);
const finalPath = getNormalizedAbsolutePath(resolvedTarget, state.host.getCurrentDirectory?.());
const result = loadFileNameFromPackageJsonField(extensions, finalPath, /*onlyRecordFailures*/ false, state);
const result = loadFileNameFromPackageJsonField(extensions, finalPath, target, /*onlyRecordFailures*/ false, state);
if (result) {
entrypoints = appendIfUnique(entrypoints, result, (a, b) => a.path === b.path);
return true;
Expand Down Expand Up @@ -2487,7 +2489,7 @@ function loadNodeModuleFromDirectoryWorker(extensions: Extensions, candidate: st
}

const loader: ResolutionKindSpecificLoader = (extensions, candidate, onlyRecordFailures, state) => {
const fromFile = loadFileNameFromPackageJsonField(extensions, candidate, onlyRecordFailures, state);
const fromFile = loadFileNameFromPackageJsonField(extensions, candidate, /*packageJsonValue*/ undefined, onlyRecordFailures, state);
if (fromFile) {
return noPackageId(fromFile);
}
Expand Down Expand Up @@ -2790,7 +2792,7 @@ function getLoadModuleFromTargetImportOrExport(extensions: Extensions, state: Mo
const finalPath = toAbsolutePath(pattern ? resolvedTarget.replace(/\*/g, subpath) : resolvedTarget + subpath);
const inputLink = tryLoadInputFileForPath(finalPath, subpath, combinePaths(scope.packageDirectory, "package.json"), isImports);
if (inputLink) return inputLink;
return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, finalPath, /*onlyRecordFailures*/ false, state), state));
return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, finalPath, target, /*onlyRecordFailures*/ false, state), state));
}
else if (typeof target === "object" && target !== null) { // eslint-disable-line no-restricted-syntax
if (!Array.isArray(target)) {
Expand Down Expand Up @@ -2936,7 +2938,7 @@ function getLoadModuleFromTargetImportOrExport(extensions: Extensions, state: Mo
if (!extensionIsOk(extensions, possibleExt)) continue;
const possibleInputWithInputExtension = changeAnyExtension(possibleInputBase, possibleExt, ext, !useCaseSensitiveFileNames(state));
if (state.host.fileExists(possibleInputWithInputExtension)) {
return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, possibleInputWithInputExtension, /*onlyRecordFailures*/ false, state), state));
return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, possibleInputWithInputExtension, /*packageJsonValue*/ undefined, /*onlyRecordFailures*/ false, state), state));
}
}
}
Expand Down Expand Up @@ -3333,7 +3335,7 @@ function resolveFromTypeRoot(moduleName: string, state: ModuleResolutionState) {
// so this function doesn't check them to avoid propagating errors.
/** @internal */
export function shouldAllowImportingTsExtension(compilerOptions: CompilerOptions, fromFileName?: string): boolean {
return !!compilerOptions.allowImportingTsExtensions || !!fromFileName && isDeclarationFileName(fromFileName);
return getAllowImportingTsExtensions(compilerOptions) || !!fromFileName && isDeclarationFileName(fromFileName);
}

/**
Expand Down
Loading

0 comments on commit bd3d700

Please sign in to comment.