Skip to content

Port TypeScript PR #59767: Rewrite relative import extensions with flag #1138

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jun 12, 2025
Merged
Changes from 11 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions internal/checker/checker.go
Original file line number Diff line number Diff line change
@@ -537,6 +537,7 @@ type Program interface {
GetSourceFileMetaData(path tspath.Path) *ast.SourceFileMetaData
GetJSXRuntimeImportSpecifier(path tspath.Path) (moduleReference string, specifier *ast.Node)
GetImportHelpersImportSpecifier(path tspath.Path) *ast.Node
SourceFileMayBeEmitted(sourceFile *ast.SourceFile, forceDtsEmit bool) bool
IsSourceFromProjectReference(path tspath.Path) bool
GetSourceAndProjectReference(path tspath.Path) *tsoptions.SourceAndProjectReference
}
@@ -14498,6 +14499,34 @@ func (c *Checker) resolveExternalModule(location *ast.Node, moduleReference stri
tsExtension,
)
}
} else if c.compilerOptions.RewriteRelativeImportExtensions.IsTrue() &&
location.Flags&ast.NodeFlagsAmbient == 0 &&
!tspath.IsDeclarationFileName(moduleReference) &&
!ast.IsLiteralImportTypeNode(location) &&
!ast.IsPartOfTypeOnlyImportOrExportDeclaration(location) {
shouldRewrite := core.ShouldRewriteModuleSpecifier(moduleReference, c.compilerOptions)
if !resolvedModule.ResolvedUsingTsExtension && shouldRewrite {
relativeToSourceFile := tspath.GetRelativePathFromFile(
tspath.GetNormalizedAbsolutePath(importingSourceFile.FileName(), c.program.GetCurrentDirectory()),
resolvedModule.ResolvedFileName,
tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: c.program.UseCaseSensitiveFileNames(),
CurrentDirectory: c.program.GetCurrentDirectory(),
},
)
c.error(
errorNode,
diagnostics.This_relative_import_path_is_unsafe_to_rewrite_because_it_looks_like_a_file_name_but_actually_resolves_to_0,
relativeToSourceFile,
)
} else if resolvedModule.ResolvedUsingTsExtension && !shouldRewrite && c.program.SourceFileMayBeEmitted(sourceFile, false) {
c.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,
tspath.GetAnyExtensionFromPath(moduleReference, nil, false),
)
}
// TODO: Add project reference check when GetResolvedProjectReferenceToRedirect is implemented
}
}

4 changes: 4 additions & 0 deletions internal/compiler/program.go
Original file line number Diff line number Diff line change
@@ -884,6 +884,10 @@ func (p *Program) GetImportHelpersImportSpecifier(path tspath.Path) *ast.Node {
return p.importHelpersImportSpecifiers[path]
}

func (p *Program) SourceFileMayBeEmitted(sourceFile *ast.SourceFile, forceDtsEmit bool) bool {
return sourceFileMayBeEmitted(sourceFile, &emitHost{program: p}, forceDtsEmit)
}

var plainJSErrors = collections.NewSetFromItems(
// binder errors
diagnostics.Cannot_redeclare_block_scoped_variable_0.Code(),
4 changes: 4 additions & 0 deletions internal/core/core.go
Original file line number Diff line number Diff line change
@@ -559,3 +559,7 @@ func IndexAfter(s string, pattern string, startIndex int) int {
return matched + startIndex
}
}

func ShouldRewriteModuleSpecifier(specifier string, compilerOptions *CompilerOptions) bool {
return compilerOptions.RewriteRelativeImportExtensions.IsTrue() && tspath.PathIsRelative(specifier) && !tspath.IsDeclarationFileName(specifier) && tspath.HasTSFileExtension(specifier)
}
2 changes: 2 additions & 0 deletions internal/outputpaths/outputpaths.go
Original file line number Diff line number Diff line change
@@ -199,3 +199,5 @@ func getDeclarationEmitExtensionForPath(fileName string) string {
}
return tspath.ExtensionDts
}


6 changes: 1 addition & 5 deletions internal/transformers/utilities.go
Original file line number Diff line number Diff line change
@@ -336,7 +336,7 @@ func tryRenameExternalModule(factory *printer.NodeFactory, moduleName *ast.Liter
}

func rewriteModuleSpecifier(emitContext *printer.EmitContext, node *ast.Expression, compilerOptions *core.CompilerOptions) *ast.Expression {
if node == nil || !ast.IsStringLiteral(node) || !shouldRewriteModuleSpecifier(node.Text(), compilerOptions) {
if node == nil || !ast.IsStringLiteral(node) || !core.ShouldRewriteModuleSpecifier(node.Text(), compilerOptions) {
return node
}
updatedText := tspath.ChangeExtension(node.Text(), outputpaths.GetOutputExtension(node.Text(), compilerOptions.Jsx))
@@ -350,10 +350,6 @@ func rewriteModuleSpecifier(emitContext *printer.EmitContext, node *ast.Expressi
return node
}

func shouldRewriteModuleSpecifier(specifier string, compilerOptions *core.CompilerOptions) bool {
return compilerOptions.RewriteRelativeImportExtensions.IsTrue() && tspath.PathIsRelative(specifier) && !tspath.IsDeclarationFileName(specifier) && tspath.HasTSFileExtension(specifier)
}

func singleOrMany(nodes []*ast.Node, factory *printer.NodeFactory) *ast.Node {
if len(nodes) == 1 {
return nodes[0]
4 changes: 2 additions & 2 deletions internal/tsoptions/declscompiler.go
Original file line number Diff line number Diff line change
@@ -810,8 +810,8 @@ var commonOptionsWithBuild = []*CommandLineOption{
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,
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",
13 changes: 13 additions & 0 deletions internal/tspath/path.go
Original file line number Diff line number Diff line change
@@ -631,6 +631,10 @@ func GetRelativePathFromDirectory(fromDirectory string, to string, options Compa
return GetPathFromPathComponents(pathComponents)
}

func GetRelativePathFromFile(from string, to string, options ComparePathsOptions) string {
return EnsurePathIsNonModuleName(GetRelativePathFromDirectory(GetDirectoryPath(from), to, options))
}

func ConvertToRelativePath(absoluteOrRelativePath string, options ComparePathsOptions) string {
if !IsRootedDiskPath(absoluteOrRelativePath) {
return absoluteOrRelativePath
@@ -768,6 +772,15 @@ func PathIsRelative(path string) bool {
return false
}

// EnsurePathIsNonModuleName ensures a path is either absolute (prefixed with `/` or `c:`) or dot-relative (prefixed
// with `./` or `../`) so as not to be confused with an unprefixed module name.
func EnsurePathIsNonModuleName(path string) string {
if !PathIsAbsolute(path) && !PathIsRelative(path) {
return "./" + path
}
return path
}

func IsExternalModuleNameRelative(moduleName string) bool {
// TypeScript 1.0 spec (April 2014): 11.2.1
// An external module name is "relative" if the first term is "." or "..".
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
index.ts(1,22): error TS2876: This relative import path is unsafe to rewrite because it looks like a file name, but actually resolves to "./foo.ts/index.ts".


==== index.ts (1 errors) ====
import foo = require("./foo.ts"); // Error
~~~~~~~~~~
!!! error TS2876: This relative import path is unsafe to rewrite because it looks like a file name, but actually resolves to "./foo.ts/index.ts".
import type _foo = require("./foo.ts"); // Ok

==== foo.ts/index.ts (0 errors) ====
export = {};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
index.ts(1,22): error TS2876: This relative import path is unsafe to rewrite because it looks like a file name, but actually resolves to "./foo.ts/index.ts".


==== index.ts (1 errors) ====
import foo = require("./foo.ts"); // Error
~~~~~~~~~~
!!! error TS2876: This relative import path is unsafe to rewrite because it looks like a file name, but actually resolves to "./foo.ts/index.ts".
import type _foo = require("./foo.ts"); // Ok

==== foo.ts/index.ts (0 errors) ====
export = {};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/index.ts(2,16): error TS2877: This import uses a '.ts' extension to resolve to an input TypeScript file, but will not be rewritten during emit because it is not a relative path.


==== /package.json (0 errors) ====
{
"name": "pkg",
"type": "module",
"imports": {
"#foo.ts": "./foo.ts",
"#internal/*": "./internal/*"
},
"exports": {
"./*.ts": {
"source": "./*.ts",
"default": "./*.js"
}
}
}

==== /foo.ts (0 errors) ====
export {};

==== /internal/foo.ts (0 errors) ====
export {};

==== /index.ts (1 errors) ====
import {} from "#foo.ts"; // Ok
import {} from "#internal/foo.ts"; // Error
~~~~~~~~~~~~~~~~~~
!!! error TS2877: This import uses a '.ts' extension to resolve to an input TypeScript file, but will not be rewritten during emit because it is not a relative path.
import {} from "pkg/foo.ts"; // Ok

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/index.ts(2,16): error TS2877: This import uses a '.ts' extension to resolve to an input TypeScript file, but will not be rewritten during emit because it is not a relative path.


==== /package.json (0 errors) ====
{
"name": "pkg",
"type": "module",
"imports": {
"#foo.ts": "./foo.ts",
"#internal/*": "./internal/*"
},
"exports": {
"./*.ts": {
"source": "./*.ts",
"default": "./*.js"
}
}
}

==== /foo.ts (0 errors) ====
export {};

==== /internal/foo.ts (0 errors) ====
export {};

==== /index.ts (1 errors) ====
import {} from "#foo.ts"; // Ok
import {} from "#internal/foo.ts"; // Error
~~~~~~~~~~~~~~~~~~
!!! error TS2877: This import uses a '.ts' extension to resolve to an input TypeScript file, but will not be rewritten during emit because it is not a relative path.
import {} from "pkg/foo.ts"; // Ok

This file was deleted.

Loading
Oops, something went wrong.