Rewrite relative import extensions with flag (#59767)

This commit is contained in:
Andrew Branch
2024-09-27 12:27:16 -07:00
committed by GitHub
parent 9d98874739
commit bd3d70058c
54 changed files with 3132 additions and 89 deletions
+50 -1
View File
@@ -248,6 +248,7 @@ import {
getAllJSDocTags,
getAllowSyntheticDefaultImports,
getAncestor,
getAnyExtensionFromPath,
getAssignedExpandoInitializer,
getAssignmentDeclarationKind,
getAssignmentDeclarationPropertyAccessKind,
@@ -259,6 +260,7 @@ import {
getCombinedLocalAndExportSymbolFlags,
getCombinedModifierFlags,
getCombinedNodeFlags,
getCommonSourceDirectoryOfConfig,
getContainingClass,
getContainingClassExcludingClassDecorators,
getContainingClassStaticBlock,
@@ -352,6 +354,8 @@ import {
getPropertyAssignmentAliasLikeExpression,
getPropertyNameForPropertyNameNode,
getPropertyNameFromType,
getRelativePathFromDirectory,
getRelativePathFromFile,
getResolutionDiagnostic,
getResolutionModeOverride,
getResolveJsonModule,
@@ -414,6 +418,7 @@ import {
hasSyntacticModifiers,
hasType,
HeritageClause,
hostGetCanonicalFileName,
Identifier,
identifierToKeywordKind,
IdentifierTypePredicate,
@@ -693,6 +698,7 @@ import {
isParenthesizedTypeNode,
isPartOfParameterDeclaration,
isPartOfTypeNode,
isPartOfTypeOnlyImportOrExportDeclaration,
isPartOfTypeQuery,
isPlainJsFile,
isPrefixUnaryExpression,
@@ -994,6 +1000,7 @@ import {
ShorthandPropertyAssignment,
shouldAllowImportingTsExtension,
shouldPreserveConstEnums,
shouldRewriteModuleSpecifier,
Signature,
SignatureDeclaration,
SignatureFlags,
@@ -1007,6 +1014,7 @@ import {
skipTypeParentheses,
some,
SourceFile,
sourceFileMayBeEmitted,
SpreadAssignment,
SpreadElement,
startsWith,
@@ -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)) {
@@ -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");
}
@@ -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),
+9
View File
@@ -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",
+16
View File
@@ -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",
@@ -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",
+31
View File
@@ -23,6 +23,7 @@ import {
isCallExpression,
isComputedPropertyName,
isIdentifier,
JsxEmit,
memoize,
ObjectLiteralElementLike,
ParameterDeclaration,
@@ -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 */
@@ -189,6 +192,8 @@ export function createEmitHelperFactory(context: TransformationContext): EmitHel
// 'using' helpers
createAddDisposableResourceHelper,
createDisposeResourcesHelper,
// --rewriteRelativeImportExtensions helpers
createRewriteRelativeImportExtensionsHelper,
};
/**
@@ -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 */
@@ -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",
+10 -8
View File
@@ -36,6 +36,7 @@ import {
forEach,
forEachAncestorDirectory,
formatMessage,
getAllowImportingTsExtensions,
getAllowJSCompilerOption,
getAnyExtensionFromPath,
getBaseFileName,
@@ -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
@@ -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)) {
@@ -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;
@@ -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);
}
@@ -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)) {
@@ -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));
}
}
}
@@ -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);
}
/**
+6 -49
View File
@@ -99,8 +99,8 @@ import {
flatten,
forEach,
forEachAncestorDirectory,
forEachChild,
forEachChildRecursively,
forEachDynamicImportOrRequireCall,
forEachEmittedFile,
forEachEntry,
forEachKey,
@@ -162,7 +162,6 @@ import {
hasExtension,
HasInvalidatedLibResolutions,
HasInvalidatedResolutions,
hasJSDocNodes,
hasJSFileExtension,
hasJsonModuleEmitEnabled,
hasProperty,
@@ -200,7 +199,6 @@ import {
isImportTypeNode,
isInJSFile,
isJSDocImportTag,
isLiteralImportTypeNode,
isModifier,
isModuleDeclaration,
isObjectLiteralExpression,
@@ -3521,7 +3519,10 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
}
if ((file.flags & NodeFlags.PossiblyContainsDynamicImport) || isJavaScriptFile) {
collectDynamicImportOrRequireOrJsDocImportCalls(file);
forEachDynamicImportOrRequireCall(file, /*includeTypeSpaceImports*/ true, /*requireStringLiteralLikeArgument*/ true, (node, moduleSpecifier) => {
setParentRecursive(node, /*incremental*/ false); // we need parent data on imports before the program is fully bound, so we ensure it's set here
imports = append(imports, moduleSpecifier);
});
}
file.imports = imports || emptyArray;
@@ -3584,50 +3585,6 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
}
}
}
function collectDynamicImportOrRequireOrJsDocImportCalls(file: SourceFile) {
const r = /import|require/g;
while (r.exec(file.text) !== null) { // eslint-disable-line no-restricted-syntax
const node = getNodeAtPosition(file, r.lastIndex);
if (isJavaScriptFile && isRequireCall(node, /*requireStringLiteralLikeArgument*/ true)) {
setParentRecursive(node, /*incremental*/ false); // we need parent data on imports before the program is fully bound, so we ensure it's set here
imports = append(imports, node.arguments[0]);
}
// we have to check the argument list has length of at least 1. We will still have to process these even though we have parsing error.
else if (isImportCall(node) && node.arguments.length >= 1 && isStringLiteralLike(node.arguments[0])) {
setParentRecursive(node, /*incremental*/ false); // we need parent data on imports before the program is fully bound, so we ensure it's set here
imports = append(imports, node.arguments[0]);
}
else if (isLiteralImportTypeNode(node)) {
setParentRecursive(node, /*incremental*/ false); // we need parent data on imports before the program is fully bound, so we ensure it's set here
imports = append(imports, node.argument.literal);
}
else if (isJavaScriptFile && isJSDocImportTag(node)) {
const moduleNameExpr = getExternalModuleName(node);
if (moduleNameExpr && isStringLiteral(moduleNameExpr) && moduleNameExpr.text) {
setParentRecursive(node, /*incremental*/ false);
imports = append(imports, moduleNameExpr);
}
}
}
}
/** Returns a token if position is in [start-of-leading-trivia, end), includes JSDoc only in JS files */
function getNodeAtPosition(sourceFile: SourceFile, position: number): Node {
let current: Node = sourceFile;
const getContainingChild = (child: Node) => {
if (child.pos <= position && (position < child.end || (position === child.end && (child.kind === SyntaxKind.EndOfFileToken)))) {
return child;
}
};
while (true) {
const child = isJavaScriptFile && hasJSDocNodes(current) && forEach(current.jsDoc, getContainingChild) || forEachChild(current, getContainingChild);
if (!child) {
return current;
}
current = child;
}
}
}
function getLibFileFromReference(ref: FileReference) {
@@ -4557,7 +4514,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
}
}
if (options.allowImportingTsExtensions && !(options.noEmit || options.emitDeclarationOnly)) {
if (options.allowImportingTsExtensions && !(options.noEmit || options.emitDeclarationOnly || options.rewriteRelativeImportExtensions)) {
createOptionValueDiagnostic("allowImportingTsExtensions", Diagnostics.Option_allowImportingTsExtensions_can_only_be_used_when_either_noEmit_or_emitDeclarationOnly_is_set);
}
@@ -1,7 +1,9 @@
import {
addEmitHelpers,
addRange,
append,
Bundle,
CallExpression,
chainBundle,
createEmptyExports,
createExternalHelpersImportDeclarationIfNeeded,
@@ -12,6 +14,7 @@ import {
ExportDeclaration,
Expression,
ExpressionStatement,
forEachDynamicImportOrRequireCall,
GeneratedIdentifierFlags,
getEmitFlags,
getEmitModuleKind,
@@ -31,17 +34,22 @@ import {
isExternalModuleImportEqualsDeclaration,
isExternalModuleIndicator,
isIdentifier,
isInJSFile,
isNamespaceExport,
isSourceFile,
isStatement,
isStringLiteralLike,
ModifierFlags,
ModuleKind,
Node,
NodeFlags,
NodeId,
rangeContainsRange,
rewriteModuleSpecifier,
ScriptTarget,
setOriginalNode,
setTextRange,
shouldRewriteModuleSpecifier,
singleOrMany,
some,
SourceFile,
@@ -73,6 +81,7 @@ export function transformECMAScriptModule(context: TransformationContext): (x: S
context.enableSubstitution(SyntaxKind.Identifier);
const noSubstitution = new Set<NodeId>();
let importsAndRequiresToRewriteOrShim: CallExpression[] | undefined;
let helperNameSubstitutions: Map<string, Identifier> | undefined;
let currentSourceFile: SourceFile | undefined;
let importRequireStatements: [ImportDeclaration, VariableStatement] | undefined;
@@ -86,7 +95,15 @@ export function transformECMAScriptModule(context: TransformationContext): (x: S
if (isExternalModule(node) || getIsolatedModules(compilerOptions)) {
currentSourceFile = node;
importRequireStatements = undefined;
if (compilerOptions.rewriteRelativeImportExtensions && (currentSourceFile.flags & NodeFlags.PossiblyContainsDynamicImport || isInJSFile(node))) {
forEachDynamicImportOrRequireCall(node, /*includeTypeSpaceImports*/ false, /*requireStringLiteralLikeArgument*/ false, node => {
if (!isStringLiteralLike(node.arguments[0]) || shouldRewriteModuleSpecifier(node.arguments[0].text, compilerOptions)) {
importsAndRequiresToRewriteOrShim = append(importsAndRequiresToRewriteOrShim, node);
}
});
}
let result = updateExternalModule(node);
addEmitHelpers(result, context.readEmitHelpers());
currentSourceFile = undefined;
if (importRequireStatements) {
result = factory.updateSourceFile(
@@ -135,11 +152,53 @@ export function transformECMAScriptModule(context: TransformationContext): (x: S
case SyntaxKind.ExportDeclaration:
const exportDecl = node as ExportDeclaration;
return visitExportDeclaration(exportDecl);
case SyntaxKind.ImportDeclaration:
return visitImportDeclaration(node as ImportDeclaration);
case SyntaxKind.CallExpression:
if (node === importsAndRequiresToRewriteOrShim?.[0]) {
return visitImportOrRequireCall(importsAndRequiresToRewriteOrShim.shift()!);
}
break;
default:
if (importsAndRequiresToRewriteOrShim?.length && rangeContainsRange(node, importsAndRequiresToRewriteOrShim[0])) {
return visitEachChild(node, visitor, context);
}
}
return node;
}
function visitImportDeclaration(node: ImportDeclaration): VisitResult<ImportDeclaration> {
if (!compilerOptions.rewriteRelativeImportExtensions) {
return node;
}
const updatedModuleSpecifier = rewriteModuleSpecifier(node.moduleSpecifier, compilerOptions);
if (updatedModuleSpecifier === node.moduleSpecifier) {
return node;
}
return factory.updateImportDeclaration(
node,
node.modifiers,
node.importClause,
updatedModuleSpecifier,
node.attributes,
);
}
function visitImportOrRequireCall(node: CallExpression): VisitResult<Expression> {
return factory.updateCallExpression(
node,
node.expression,
node.typeArguments,
[
isStringLiteralLike(node.arguments[0])
? rewriteModuleSpecifier(node.arguments[0], compilerOptions)
: emitHelpers().createRewriteRelativeImportExtensionsHelper(node.arguments[0]),
...node.arguments.slice(1),
],
);
}
/**
* Creates a `require()` call to import an external module.
*
@@ -149,7 +208,7 @@ export function transformECMAScriptModule(context: TransformationContext): (x: S
const moduleName = getExternalModuleNameLiteral(factory, importNode, Debug.checkDefined(currentSourceFile), host, resolver, compilerOptions);
const args: Expression[] = [];
if (moduleName) {
args.push(moduleName);
args.push(rewriteModuleSpecifier(moduleName, compilerOptions));
}
if (getEmitModuleKind(compilerOptions) === ModuleKind.Preserve) {
return factory.createCallExpression(factory.createIdentifier("require"), /*typeArguments*/ undefined, args);
@@ -270,14 +329,21 @@ export function transformECMAScriptModule(context: TransformationContext): (x: S
}
function visitExportDeclaration(node: ExportDeclaration) {
// `export * as ns` only needs to be transformed in ES2015
if (compilerOptions.module !== undefined && compilerOptions.module > ModuleKind.ES2015) {
return node;
}
// Either ill-formed or don't need to be tranformed.
if (!node.exportClause || !isNamespaceExport(node.exportClause) || !node.moduleSpecifier) {
return node;
const updatedModuleSpecifier = rewriteModuleSpecifier(node.moduleSpecifier, compilerOptions);
if (
(compilerOptions.module !== undefined && compilerOptions.module > ModuleKind.ES2015)
|| !node.exportClause || !isNamespaceExport(node.exportClause) || !node.moduleSpecifier
) {
// Either ill-formed or don't need to be tranformed.
return (!node.moduleSpecifier || updatedModuleSpecifier === node.moduleSpecifier) ? node :
factory.updateExportDeclaration(
node,
node.modifiers,
node.isTypeOnly,
node.exportClause,
updatedModuleSpecifier,
node.attributes,
);
}
const oldIdentifier = node.exportClause.name;
@@ -291,7 +357,7 @@ export function transformECMAScriptModule(context: TransformationContext): (x: S
synthName,
),
),
node.moduleSpecifier,
updatedModuleSpecifier!,
node.attributes,
);
setOriginalNode(importDecl, node.exportClause);
+44 -5
View File
@@ -34,6 +34,7 @@ import {
firstOrUndefined,
flattenDestructuringAssignment,
FlattenLevel,
forEachDynamicImportOrRequireCall,
ForInStatement,
ForOfStatement,
ForStatement,
@@ -114,6 +115,7 @@ import {
isSpreadElement,
isStatement,
isStringLiteral,
isStringLiteralLike,
isVariableDeclaration,
isVariableDeclarationList,
LabeledStatement,
@@ -136,11 +138,13 @@ import {
PrefixUnaryExpression,
reduceLeft,
removeAllComments,
rewriteModuleSpecifier,
ScriptTarget,
setEmitFlags,
setOriginalNode,
setTextRange,
ShorthandPropertyAssignment,
shouldRewriteModuleSpecifier,
singleOrMany,
some,
SourceFile,
@@ -213,6 +217,7 @@ export function transformModule(context: TransformationContext): (x: SourceFile
let currentSourceFile: SourceFile; // The current file.
let currentModuleInfo: ExternalModuleInfo; // The ExternalModuleInfo for the current file.
let importsAndRequiresToRewriteOrShim: CallExpression[] | undefined;
const noSubstitution: boolean[] = []; // Set of nodes for which substitution rules should be ignored.
let needUMDDynamicImportHelper: boolean;
@@ -236,6 +241,13 @@ export function transformModule(context: TransformationContext): (x: SourceFile
currentSourceFile = node;
currentModuleInfo = collectExternalModuleInfo(context, node);
moduleInfoMap[getOriginalNodeId(node)] = currentModuleInfo;
if (compilerOptions.rewriteRelativeImportExtensions) {
forEachDynamicImportOrRequireCall(node, /*includeTypeSpaceImports*/ false, /*requireStringLiteralLikeArgument*/ false, node => {
if (!isStringLiteralLike(node.arguments[0]) || shouldRewriteModuleSpecifier(node.arguments[0].text, compilerOptions)) {
importsAndRequiresToRewriteOrShim = append(importsAndRequiresToRewriteOrShim, node);
}
});
}
// Perform the transformation.
const transformModule = getTransformModuleDelegate(moduleKind);
@@ -783,7 +795,7 @@ export function transformModule(context: TransformationContext): (x: SourceFile
function visitorWorker(node: Node, valueIsDiscarded: boolean): VisitResult<Node> {
// This visitor does not need to descend into the tree if there is no dynamic import, destructuring assignment, or update expression
// as export/import statements are only transformed at the top level of a file.
if (!(node.transformFlags & (TransformFlags.ContainsDynamicImport | TransformFlags.ContainsDestructuringAssignment | TransformFlags.ContainsUpdateExpressionForIdentifier))) {
if (!(node.transformFlags & (TransformFlags.ContainsDynamicImport | TransformFlags.ContainsDestructuringAssignment | TransformFlags.ContainsUpdateExpressionForIdentifier)) && !importsAndRequiresToRewriteOrShim?.length) {
return node;
}
@@ -797,8 +809,15 @@ export function transformModule(context: TransformationContext): (x: SourceFile
case SyntaxKind.PartiallyEmittedExpression:
return visitPartiallyEmittedExpression(node as PartiallyEmittedExpression, valueIsDiscarded);
case SyntaxKind.CallExpression:
const needsRewrite = node === firstOrUndefined(importsAndRequiresToRewriteOrShim);
if (needsRewrite) {
importsAndRequiresToRewriteOrShim!.shift();
}
if (isImportCall(node) && host.shouldTransformImportCall(currentSourceFile)) {
return visitImportCallExpression(node);
return visitImportCallExpression(node, needsRewrite);
}
else if (needsRewrite) {
return shimOrRewriteImportOrRequireCall(node as CallExpression);
}
break;
case SyntaxKind.BinaryExpression:
@@ -1170,14 +1189,34 @@ export function transformModule(context: TransformationContext): (x: SourceFile
return visitEachChild(node, visitor, context);
}
function visitImportCallExpression(node: ImportCall): Expression {
function shimOrRewriteImportOrRequireCall(node: CallExpression): CallExpression {
return factory.updateCallExpression(
node,
node.expression,
/*typeArguments*/ undefined,
visitNodes(node.arguments, (arg: Expression) => {
if (arg === node.arguments[0]) {
return isStringLiteralLike(arg)
? rewriteModuleSpecifier(arg, compilerOptions)
: emitHelpers().createRewriteRelativeImportExtensionsHelper(arg);
}
return visitor(arg);
}, isExpression),
);
}
function visitImportCallExpression(node: ImportCall, rewriteOrShim: boolean): Expression {
if (moduleKind === ModuleKind.None && languageVersion >= ScriptTarget.ES2020) {
return visitEachChild(node, visitor, context);
}
const externalModuleName = getExternalModuleNameLiteral(factory, node, currentSourceFile, host, resolver, compilerOptions);
const firstArgument = visitNode(firstOrUndefined(node.arguments), visitor, isExpression);
// Only use the external module name if it differs from the first argument. This allows us to preserve the quote style of the argument on output.
const argument = externalModuleName && (!firstArgument || !isStringLiteral(firstArgument) || firstArgument.text !== externalModuleName.text) ? externalModuleName : firstArgument;
const argument = externalModuleName && (!firstArgument || !isStringLiteral(firstArgument) || firstArgument.text !== externalModuleName.text)
? externalModuleName
: firstArgument && rewriteOrShim
? isStringLiteral(firstArgument) ? rewriteModuleSpecifier(firstArgument, compilerOptions) : emitHelpers().createRewriteRelativeImportExtensionsHelper(firstArgument)
: firstArgument;
const containsLexicalThis = !!(node.transformFlags & TransformFlags.ContainsLexicalThis);
switch (compilerOptions.module) {
case ModuleKind.AMD:
@@ -1500,7 +1539,7 @@ export function transformModule(context: TransformationContext): (x: SourceFile
const moduleName = getExternalModuleNameLiteral(factory, importNode, currentSourceFile, host, resolver, compilerOptions);
const args: Expression[] = [];
if (moduleName) {
args.push(moduleName);
args.push(rewriteModuleSpecifier(moduleName, compilerOptions));
}
return factory.createCallExpression(factory.createIdentifier("require"), /*typeArguments*/ undefined, args);
+12 -3
View File
@@ -2343,7 +2343,14 @@ export function transformTypeScript(context: TransformationContext): Transformer
// never elide `export <whatever> from <whereever>` declarations -
// they should be kept for sideffects/untyped exports, even when the
// type checker doesn't know about any exports
return node;
return factory.updateExportDeclaration(
node,
node.modifiers,
node.isTypeOnly,
node.exportClause,
node.moduleSpecifier,
node.attributes,
);
}
// Elide the export declaration if all of its named exports are elided.
@@ -2422,8 +2429,10 @@ export function transformTypeScript(context: TransformationContext): Transformer
}
if (isExternalModuleImportEqualsDeclaration(node)) {
const isReferenced = shouldEmitAliasDeclaration(node);
return isReferenced ? visitEachChild(node, visitor, context) : undefined;
if (!shouldEmitAliasDeclaration(node)) {
return undefined;
}
return visitEachChild(node, visitor, context);
}
if (!shouldEmitImportEqualsDeclaration(node)) {
+21
View File
@@ -7,11 +7,13 @@ import {
BindingElement,
Bundle,
cast,
changeExtension,
ClassDeclaration,
ClassElement,
ClassExpression,
ClassLikeDeclaration,
ClassStaticBlockDeclaration,
CompilerOptions,
CompoundAssignmentOperator,
CoreTransformationContext,
createExternalHelpersImportDeclarationIfNeeded,
@@ -21,6 +23,7 @@ import {
ExportDeclaration,
ExportSpecifier,
Expression,
factory,
filter,
formatGeneratedName,
FunctionDeclaration,
@@ -33,6 +36,7 @@ import {
getNodeForGeneratedName,
getNodeId,
getOriginalNode,
getOutputExtension,
hasDecorators,
hasStaticModifier,
hasSyntacticModifier,
@@ -61,6 +65,7 @@ import {
isPrivateIdentifier,
isPropertyDeclaration,
isStatic,
isStringLiteral,
isStringLiteralLike,
isSuperCall,
isTryStatement,
@@ -83,6 +88,9 @@ import {
PrivateIdentifierAutoAccessorPropertyDeclaration,
PrivateIdentifierMethodDeclaration,
PropertyDeclaration,
setOriginalNode,
setTextRange,
shouldRewriteModuleSpecifier,
skipParentheses,
some,
SourceFile,
@@ -863,3 +871,16 @@ function isSimpleParameter(node: ParameterDeclaration) {
export function isSimpleParameterList(nodes: NodeArray<ParameterDeclaration>): boolean {
return every(nodes, isSimpleParameter);
}
/** @internal */
export function rewriteModuleSpecifier(node: Expression, compilerOptions: CompilerOptions): Expression;
/** @internal */
export function rewriteModuleSpecifier(node: Expression | undefined, compilerOptions: CompilerOptions): Expression | undefined;
/** @internal */
export function rewriteModuleSpecifier(node: Expression | undefined, compilerOptions: CompilerOptions): Expression | undefined {
if (!node || !isStringLiteral(node) || !shouldRewriteModuleSpecifier(node.text, compilerOptions)) {
return node;
}
const updatedText = changeExtension(node.text, getOutputExtension(node.text, compilerOptions));
return updatedText !== node.text ? setOriginalNode(setTextRange(factory.createStringLiteral(updatedText, node.singleQuote), node), node) : node;
}
+4 -2
View File
@@ -5016,7 +5016,7 @@ export interface EmitResult {
}
/** @internal */
export interface TypeCheckerHost extends ModuleSpecifierResolutionHost {
export interface TypeCheckerHost extends ModuleSpecifierResolutionHost, SourceFileMayBeEmittedHost {
getCompilerOptions(): CompilerOptions;
getSourceFiles(): readonly SourceFile[];
@@ -7451,6 +7451,7 @@ export interface CompilerOptions {
removeComments?: boolean;
resolvePackageJsonExports?: boolean;
resolvePackageJsonImports?: boolean;
rewriteRelativeImportExtensions?: boolean;
rootDir?: string;
rootDirs?: string[];
skipLibCheck?: boolean;
@@ -8468,6 +8469,7 @@ export const enum ExternalEmitHelpers {
SetFunctionName = 1 << 22, // __setFunctionName (used by class fields and ECMAScript decorators)
PropKey = 1 << 23, // __propKey (used by class fields and ECMAScript decorators)
AddDisposableResourceAndDisposeResources = 1 << 24, // __addDisposableResource and __disposeResources (used by ESNext transformations)
RewriteRelativeImportExtension = 1 << 25, // __rewriteRelativeImportExtension (used by --rewriteRelativeImportExtensions)
FirstEmitHelper = Extends,
LastEmitHelper = AddDisposableResourceAndDisposeResources,
@@ -9890,7 +9892,7 @@ export interface HasCurrentDirectory {
/** @internal */
export interface ModuleSpecifierResolutionHost {
useCaseSensitiveFileNames?(): boolean;
useCaseSensitiveFileNames(): boolean;
fileExists(path: string): boolean;
getCurrentDirectory(): string;
directoryExists?(path: string): boolean;
+71
View File
@@ -298,6 +298,7 @@ import {
isJSDocAugmentsTag,
isJSDocFunctionType,
isJSDocImplementsTag,
isJSDocImportTag,
isJSDocLinkLike,
isJSDocMemberName,
isJSDocNameReference,
@@ -4302,6 +4303,11 @@ export function tryGetImportFromModuleSpecifier(node: StringLiteralLike): AnyVal
}
}
/** @internal */
export function shouldRewriteModuleSpecifier(specifier: string, compilerOptions: CompilerOptions): boolean {
return !!compilerOptions.rewriteRelativeImportExtensions && pathIsRelative(specifier) && !isDeclarationFileName(specifier);
}
/** @internal */
export function getExternalModuleName(node: AnyImportOrReExport | ImportTypeNode | ImportCall | ModuleDeclaration | JSDocImportTag): Expression | undefined {
switch (node.kind) {
@@ -7854,6 +7860,16 @@ export function getLinesBetweenPositionAndNextNonWhitespaceCharacter(pos: number
return getLinesBetweenPositions(sourceFile, pos, Math.min(stopPos, nextPos));
}
/** @internal */
export function rangeContainsRange(r1: TextRange, r2: TextRange): boolean {
return startEndContainsRange(r1.pos, r1.end, r2);
}
/** @internal */
export function startEndContainsRange(start: number, end: number, range: TextRange): boolean {
return start <= range.pos && end >= range.end;
}
function getPreviousNonWhitespacePosition(pos: number, stopPos = 0, sourceFile: SourceFile) {
while (pos-- > stopPos) {
if (!isWhiteSpaceLike(sourceFile.text.charCodeAt(pos))) {
@@ -8900,6 +8916,12 @@ function createComputedCompilerOptions<T extends Record<string, CompilerOptionKe
}
const _computedOptions = createComputedCompilerOptions({
allowImportingTsExtensions: {
dependencies: ["rewriteRelativeImportExtensions"],
computeValue: compilerOptions => {
return !!(compilerOptions.allowImportingTsExtensions || compilerOptions.rewriteRelativeImportExtensions);
},
},
target: {
dependencies: ["module"],
computeValue: compilerOptions => {
@@ -9128,6 +9150,8 @@ const _computedOptions = createComputedCompilerOptions({
/** @internal */
export const computedOptions: Record<string, { dependencies: readonly string[]; computeValue: (options: CompilerOptions) => CompilerOptionsValue; }> = _computedOptions;
/** @internal */
export const getAllowImportingTsExtensions: (compilerOptions: CompilerOptions) => boolean = _computedOptions.allowImportingTsExtensions.computeValue;
/** @internal */
export const getEmitScriptTarget: (compilerOptions: CompilerOptions) => ScriptTarget = _computedOptions.target.computeValue;
/** @internal */
@@ -12004,3 +12028,50 @@ export const nodeCoreModules: Set<string> = new Set([
...unprefixedNodeCoreModulesList.map(name => `node:${name}`),
...exclusivelyPrefixedNodeCoreModules,
]);
/** @internal */
export function forEachDynamicImportOrRequireCall<IncludeTypeSpaceImports extends boolean, RequireStringLiteralLikeArgument extends boolean>(
file: SourceFile,
includeTypeSpaceImports: IncludeTypeSpaceImports,
requireStringLiteralLikeArgument: RequireStringLiteralLikeArgument,
cb: (node: CallExpression | (IncludeTypeSpaceImports extends false ? never : JSDocImportTag | ImportTypeNode), argument: RequireStringLiteralLikeArgument extends true ? StringLiteralLike : Expression) => void,
): void {
const isJavaScriptFile = isInJSFile(file);
const r = /import|require/g;
while (r.exec(file.text) !== null) { // eslint-disable-line no-restricted-syntax
const node = getNodeAtPosition(file, r.lastIndex, /*includeJSDoc*/ includeTypeSpaceImports);
if (isJavaScriptFile && isRequireCall(node, requireStringLiteralLikeArgument)) {
cb(node, node.arguments[0] as RequireStringLiteralLikeArgument extends true ? StringLiteralLike : Expression);
}
else if (isImportCall(node) && node.arguments.length >= 1 && (!requireStringLiteralLikeArgument || isStringLiteralLike(node.arguments[0]))) {
cb(node, node.arguments[0] as RequireStringLiteralLikeArgument extends true ? StringLiteralLike : Expression);
}
else if (includeTypeSpaceImports && isLiteralImportTypeNode(node)) {
(cb as (node: CallExpression | JSDocImportTag | ImportTypeNode, argument: StringLiteralLike) => void)(node, node.argument.literal);
}
else if (includeTypeSpaceImports && isJSDocImportTag(node)) {
const moduleNameExpr = getExternalModuleName(node);
if (moduleNameExpr && isStringLiteral(moduleNameExpr) && moduleNameExpr.text) {
(cb as (node: CallExpression | JSDocImportTag | ImportTypeNode, argument: StringLiteralLike) => void)(node, moduleNameExpr);
}
}
}
}
/** Returns a token if position is in [start-of-leading-trivia, end), includes JSDoc only in JS files */
function getNodeAtPosition(sourceFile: SourceFile, position: number, includeJSDoc: boolean): Node {
const isJavaScriptFile = isInJSFile(sourceFile);
let current: Node = sourceFile;
const getContainingChild = (child: Node) => {
if (child.pos <= position && (position < child.end || (position === child.end && (child.kind === SyntaxKind.EndOfFileToken)))) {
return child;
}
};
while (true) {
const child = isJavaScriptFile && includeJSDoc && hasJSDocNodes(current) && forEach(current.jsDoc, getContainingChild) || forEachChild(current, getContainingChild);
if (!child) {
return current;
}
current = child;
}
}
+4
View File
@@ -1553,6 +1553,10 @@ export function isTypeOnlyImportOrExportDeclaration(node: Node): node is TypeOnl
return isTypeOnlyImportDeclaration(node) || isTypeOnlyExportDeclaration(node);
}
export function isPartOfTypeOnlyImportOrExportDeclaration(node: Node): boolean {
return findAncestor(node, isTypeOnlyImportOrExportDeclaration) !== undefined;
}
export function isStringTextContainingNode(node: Node): node is StringLiteral | TemplateLiteralToken {
return node.kind === SyntaxKind.StringLiteral || isTemplateLiteralKind(node.kind);
}
+2 -11
View File
@@ -320,6 +320,7 @@ import {
PseudoBigInt,
pseudoBigIntToString,
QualifiedName,
rangeContainsRange,
RefactorContext,
removeFileExtension,
removeSuffix,
@@ -917,11 +918,6 @@ export function getLineStartPositionForPosition(position: number, sourceFile: So
return lineStarts[line];
}
/** @internal */
export function rangeContainsRange(r1: TextRange, r2: TextRange): boolean {
return startEndContainsRange(r1.pos, r1.end, r2);
}
/** @internal */
export function rangeContainsRangeExclusive(r1: TextRange, r2: TextRange): boolean {
return rangeContainsPositionExclusive(r1, r2.pos) && rangeContainsPositionExclusive(r1, r2.end);
@@ -937,11 +933,6 @@ export function rangeContainsPositionExclusive(r: TextRange, pos: number): boole
return r.pos < pos && pos < r.end;
}
/** @internal */
export function startEndContainsRange(start: number, end: number, range: TextRange): boolean {
return start <= range.pos && end >= range.end;
}
/** @internal */
export function rangeContainsStartEnd(range: TextRange, start: number, end: number): boolean {
return range.pos <= start && range.end >= end;
@@ -2468,7 +2459,7 @@ export function createModuleSpecifierResolutionHost(program: Program, host: Lang
fileExists: fileName => program.fileExists(fileName),
getCurrentDirectory: () => host.getCurrentDirectory(),
readFile: maybeBind(host, host.readFile),
useCaseSensitiveFileNames: maybeBind(host, host.useCaseSensitiveFileNames),
useCaseSensitiveFileNames: maybeBind(host, host.useCaseSensitiveFileNames) || program.useCaseSensitiveFileNames,
getSymlinkCache: maybeBind(host, host.getSymlinkCache) || program.getSymlinkCache,
getModuleSpecifierCache: maybeBind(host, host.getModuleSpecifierCache),
getPackageJsonInfoCache: () => program.getModuleResolutionCache()?.getPackageJsonInfoCache(),