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
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

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
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
)
}
}
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/checker/nodebuilderimpl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions internal/compiler/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
) {
Expand Down Expand Up @@ -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(),
Expand Down
4 changes: 4 additions & 0 deletions internal/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions internal/transformers/importelision_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
6 changes: 1 addition & 5 deletions internal/transformers/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions internal/tsoptions/declscompiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 13 additions & 0 deletions internal/tspath/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 "..".
Expand Down
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