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 all 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
60 changes: 60 additions & 0 deletions internal/checker/checker.go
Original file line number Diff line number Diff line change
@@ -537,8 +537,11 @@ 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
GetRedirectForResolution(file ast.HasFileName) *tsoptions.ParsedCommandLine
CommonSourceDirectory() string
}

type Host interface {
@@ -14498,6 +14501,63 @@ 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),
)
} else if resolvedModule.ResolvedUsingTsExtension && shouldRewrite {
if redirect := c.program.GetRedirectForResolution(sourceFile); redirect != nil {
ownRootDir := c.program.CommonSourceDirectory()
otherRootDir := redirect.CommonSourceDirectory()

compareOptions := tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: c.program.UseCaseSensitiveFileNames(),
CurrentDirectory: c.program.GetCurrentDirectory(),
}

rootDirPath := tspath.GetRelativePathFromDirectory(ownRootDir, otherRootDir, compareOptions)

// Get outDir paths, defaulting to root directories if not specified
ownOutDir := c.compilerOptions.OutDir
if ownOutDir == "" {
ownOutDir = ownRootDir
}
otherOutDir := redirect.CompilerOptions().OutDir
if otherOutDir == "" {
otherOutDir = otherRootDir
}
outDirPath := tspath.GetRelativePathFromDirectory(ownOutDir, otherOutDir, compareOptions)

if rootDirPath != outDirPath {
c.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,
)
}
}
}
}
}

2 changes: 1 addition & 1 deletion internal/checker/nodebuilderimpl.go
Original file line number Diff line number Diff line change
@@ -1087,7 +1087,7 @@ func (b *nodeBuilderImpl) getSpecifierForModuleSymbol(symbol *ast.Symbol, overri
}
if isBundle {
// !!! relies on option cloning and specifier host implementation
// specifierCompilerOptions = &core.CompilerOptions{BaseUrl: host.GetCommonSourceDirectory()}
// specifierCompilerOptions = &core.CompilerOptions{BaseUrl: host.CommonSourceDirectory()}
// TODO: merge with b.ch.compilerOptions
specifierPref = modulespecifiers.ImportModuleSpecifierPreferenceNonRelative
endingPref = modulespecifiers.ImportModuleSpecifierEndingPreferenceMinimal
8 changes: 8 additions & 0 deletions internal/compiler/program.go
Original file line number Diff line number Diff line change
@@ -122,6 +122,10 @@ func (p *Program) GetResolvedProjectReferenceFor(path tspath.Path) (*tsoptions.P
return p.projectReferenceFileMapper.getResolvedReferenceFor(path)
}

func (p *Program) GetRedirectForResolution(file ast.HasFileName) *tsoptions.ParsedCommandLine {
return p.projectReferenceFileMapper.getRedirectForResolution(file)
}

func (p *Program) ForEachResolvedProjectReference(
fn func(path tspath.Path, config *tsoptions.ParsedCommandLine) bool,
) {
@@ -884,6 +888,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)
}
102 changes: 102 additions & 0 deletions internal/execute/tscprojectreferences_test.go
Original file line number Diff line number Diff line change
@@ -192,6 +192,108 @@ func TestProjectReferences(t *testing.T) {
}, "/home/src/workspaces/solution"),
commandLineArgs: []string{"--p", "project", "--pretty", "false"},
},
{
subScenario: "rewriteRelativeImportExtensionsProjectReferences1",
sys: newTestSys(FileMap{
"/home/src/workspaces/packages/common/tsconfig.json": `{
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "dist",
"module": "nodenext"
}
}`,
"/home/src/workspaces/packages/common/package.json": `{
"name": "common",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"source": "./src/index.ts",
"default": "./dist/index.js"
}
}
}`,
"/home/src/workspaces/packages/common/src/index.ts": "export {};",
"/home/src/workspaces/packages/common/dist/index.d.ts": "export {};",
"/home/src/workspaces/packages/main/tsconfig.json": `{
"compilerOptions": {
"module": "nodenext",
"rewriteRelativeImportExtensions": true,
"rootDir": "src",
"outDir": "dist"
},
"references": [
{ "path": "../common" }
]
}`,
"/home/src/workspaces/packages/main/package.json": `{ "type": "module" }`,
"/home/src/workspaces/packages/main/src/index.ts": `import {} from "../../common/src/index.ts";`,
}, "/home/src/workspaces"),
commandLineArgs: []string{"-p", "packages/main", "--pretty", "false"},
},
{
subScenario: "rewriteRelativeImportExtensionsProjectReferences2",
sys: newTestSys(FileMap{
"/home/src/workspaces/solution/src/tsconfig-base.json": `{
"compilerOptions": {
"module": "nodenext",
"composite": true,
"rootDir": ".",
"outDir": "../dist",
"rewriteRelativeImportExtensions": true
}
}`,
"/home/src/workspaces/solution/src/compiler/tsconfig.json": `{
"extends": "../tsconfig-base.json",
"compilerOptions": {}
}`,
"/home/src/workspaces/solution/src/compiler/parser.ts": "export {};",
"/home/src/workspaces/solution/dist/compiler/parser.d.ts": "export {};",
"/home/src/workspaces/solution/src/services/tsconfig.json": `{
"extends": "../tsconfig-base.json",
"compilerOptions": {},
"references": [
{ "path": "../compiler" }
]
}`,
"/home/src/workspaces/solution/src/services/services.ts": `import {} from "../compiler/parser.ts";`,
}, "/home/src/workspaces/solution"),
commandLineArgs: []string{"--p", "src/services", "--pretty", "false"},
},
{
subScenario: "rewriteRelativeImportExtensionsProjectReferences3",
sys: newTestSys(FileMap{
"/home/src/workspaces/solution/src/tsconfig-base.json": `{
"compilerOptions": {
"module": "nodenext",
"composite": true,
"rewriteRelativeImportExtensions": true
}
}`,
"/home/src/workspaces/solution/src/compiler/tsconfig.json": `{
"extends": "../tsconfig-base.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "../../dist/compiler"
}
}`,
"/home/src/workspaces/solution/src/compiler/parser.ts": "export {};",
"/home/src/workspaces/solution/dist/compiler/parser.d.ts": "export {};",
"/home/src/workspaces/solution/src/services/tsconfig.json": `{
"extends": "../tsconfig-base.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "../../dist/services"
},
"references": [
{ "path": "../compiler" }
]
}`,
"/home/src/workspaces/solution/src/services/services.ts": `import {} from "../compiler/parser.ts";`,
}, "/home/src/workspaces/solution"),
commandLineArgs: []string{"--p", "src/services", "--pretty", "false"},
},
}

for _, c := range cases {
10 changes: 10 additions & 0 deletions internal/transformers/importelision_test.go
Original file line number Diff line number Diff line change
@@ -27,6 +27,16 @@ type fakeProgram struct {
getSourceFileForResolvedModule func(FileName string) *ast.SourceFile
}

// GetRedirectForResolution implements checker.Program.
func (p *fakeProgram) GetRedirectForResolution(file ast.HasFileName) *tsoptions.ParsedCommandLine {
panic("unimplemented")
}

// SourceFileMayBeEmitted implements checker.Program.
func (p *fakeProgram) SourceFileMayBeEmitted(sourceFile *ast.SourceFile, forceDtsEmit bool) bool {
panic("unimplemented")
}

// GetEmitSyntaxForUsageLocation implements checker.Program.
func (p *fakeProgram) GetEmitSyntaxForUsageLocation(sourceFile ast.HasFileName, usageLocation *ast.StringLiteralLike) core.ResolutionMode {
panic("unimplemented")
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
Loading
Oops, something went wrong.