Skip to content

Commit e39346a

Browse files
Copilotandrewbranchsheetalkamat
authored
Port TypeScript PR #59767: Rewrite relative import extensions with flag (#1138)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com> Co-authored-by: sheetalkamat <8052792+sheetalkamat@users.noreply.github.com> Co-authored-by: Andrew Branch <andrew@wheream.io>
1 parent 877603e commit e39346a

20 files changed

+468
-110
lines changed

internal/checker/checker.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,8 +537,11 @@ type Program interface {
537537
GetSourceFileMetaData(path tspath.Path) *ast.SourceFileMetaData
538538
GetJSXRuntimeImportSpecifier(path tspath.Path) (moduleReference string, specifier *ast.Node)
539539
GetImportHelpersImportSpecifier(path tspath.Path) *ast.Node
540+
SourceFileMayBeEmitted(sourceFile *ast.SourceFile, forceDtsEmit bool) bool
540541
IsSourceFromProjectReference(path tspath.Path) bool
541542
GetSourceAndProjectReference(path tspath.Path) *tsoptions.SourceAndProjectReference
543+
GetRedirectForResolution(file ast.HasFileName) *tsoptions.ParsedCommandLine
544+
CommonSourceDirectory() string
542545
}
543546

544547
type Host interface {
@@ -14498,6 +14501,63 @@ func (c *Checker) resolveExternalModule(location *ast.Node, moduleReference stri
1449814501
tsExtension,
1449914502
)
1450014503
}
14504+
} else if c.compilerOptions.RewriteRelativeImportExtensions.IsTrue() &&
14505+
location.Flags&ast.NodeFlagsAmbient == 0 &&
14506+
!tspath.IsDeclarationFileName(moduleReference) &&
14507+
!ast.IsLiteralImportTypeNode(location) &&
14508+
!ast.IsPartOfTypeOnlyImportOrExportDeclaration(location) {
14509+
shouldRewrite := core.ShouldRewriteModuleSpecifier(moduleReference, c.compilerOptions)
14510+
if !resolvedModule.ResolvedUsingTsExtension && shouldRewrite {
14511+
relativeToSourceFile := tspath.GetRelativePathFromFile(
14512+
tspath.GetNormalizedAbsolutePath(importingSourceFile.FileName(), c.program.GetCurrentDirectory()),
14513+
resolvedModule.ResolvedFileName,
14514+
tspath.ComparePathsOptions{
14515+
UseCaseSensitiveFileNames: c.program.UseCaseSensitiveFileNames(),
14516+
CurrentDirectory: c.program.GetCurrentDirectory(),
14517+
},
14518+
)
14519+
c.error(
14520+
errorNode,
14521+
diagnostics.This_relative_import_path_is_unsafe_to_rewrite_because_it_looks_like_a_file_name_but_actually_resolves_to_0,
14522+
relativeToSourceFile,
14523+
)
14524+
} else if resolvedModule.ResolvedUsingTsExtension && !shouldRewrite && c.program.SourceFileMayBeEmitted(sourceFile, false) {
14525+
c.error(
14526+
errorNode,
14527+
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,
14528+
tspath.GetAnyExtensionFromPath(moduleReference, nil, false),
14529+
)
14530+
} else if resolvedModule.ResolvedUsingTsExtension && shouldRewrite {
14531+
if redirect := c.program.GetRedirectForResolution(sourceFile); redirect != nil {
14532+
ownRootDir := c.program.CommonSourceDirectory()
14533+
otherRootDir := redirect.CommonSourceDirectory()
14534+
14535+
compareOptions := tspath.ComparePathsOptions{
14536+
UseCaseSensitiveFileNames: c.program.UseCaseSensitiveFileNames(),
14537+
CurrentDirectory: c.program.GetCurrentDirectory(),
14538+
}
14539+
14540+
rootDirPath := tspath.GetRelativePathFromDirectory(ownRootDir, otherRootDir, compareOptions)
14541+
14542+
// Get outDir paths, defaulting to root directories if not specified
14543+
ownOutDir := c.compilerOptions.OutDir
14544+
if ownOutDir == "" {
14545+
ownOutDir = ownRootDir
14546+
}
14547+
otherOutDir := redirect.CompilerOptions().OutDir
14548+
if otherOutDir == "" {
14549+
otherOutDir = otherRootDir
14550+
}
14551+
outDirPath := tspath.GetRelativePathFromDirectory(ownOutDir, otherOutDir, compareOptions)
14552+
14553+
if rootDirPath != outDirPath {
14554+
c.error(
14555+
errorNode,
14556+
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,
14557+
)
14558+
}
14559+
}
14560+
}
1450114561
}
1450214562
}
1450314563

internal/checker/nodebuilderimpl.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1087,7 +1087,7 @@ func (b *nodeBuilderImpl) getSpecifierForModuleSymbol(symbol *ast.Symbol, overri
10871087
}
10881088
if isBundle {
10891089
// !!! relies on option cloning and specifier host implementation
1090-
// specifierCompilerOptions = &core.CompilerOptions{BaseUrl: host.GetCommonSourceDirectory()}
1090+
// specifierCompilerOptions = &core.CompilerOptions{BaseUrl: host.CommonSourceDirectory()}
10911091
// TODO: merge with b.ch.compilerOptions
10921092
specifierPref = modulespecifiers.ImportModuleSpecifierPreferenceNonRelative
10931093
endingPref = modulespecifiers.ImportModuleSpecifierEndingPreferenceMinimal

internal/compiler/program.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ func (p *Program) GetResolvedProjectReferenceFor(path tspath.Path) (*tsoptions.P
119119
return p.projectReferenceFileMapper.getResolvedReferenceFor(path)
120120
}
121121

122+
func (p *Program) GetRedirectForResolution(file ast.HasFileName) *tsoptions.ParsedCommandLine {
123+
return p.projectReferenceFileMapper.getRedirectForResolution(file)
124+
}
125+
122126
func (p *Program) ForEachResolvedProjectReference(
123127
fn func(path tspath.Path, config *tsoptions.ParsedCommandLine) bool,
124128
) {
@@ -874,6 +878,10 @@ func (p *Program) GetImportHelpersImportSpecifier(path tspath.Path) *ast.Node {
874878
return p.importHelpersImportSpecifiers[path]
875879
}
876880

881+
func (p *Program) SourceFileMayBeEmitted(sourceFile *ast.SourceFile, forceDtsEmit bool) bool {
882+
return sourceFileMayBeEmitted(sourceFile, &emitHost{program: p}, forceDtsEmit)
883+
}
884+
877885
var plainJSErrors = collections.NewSetFromItems(
878886
// binder errors
879887
diagnostics.Cannot_redeclare_block_scoped_variable_0.Code(),

internal/core/core.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,3 +559,7 @@ func IndexAfter(s string, pattern string, startIndex int) int {
559559
return matched + startIndex
560560
}
561561
}
562+
563+
func ShouldRewriteModuleSpecifier(specifier string, compilerOptions *CompilerOptions) bool {
564+
return compilerOptions.RewriteRelativeImportExtensions.IsTrue() && tspath.PathIsRelative(specifier) && !tspath.IsDeclarationFileName(specifier) && tspath.HasTSFileExtension(specifier)
565+
}

internal/execute/tscprojectreferences_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,108 @@ func TestProjectReferences(t *testing.T) {
192192
}, "/home/src/workspaces/solution"),
193193
commandLineArgs: []string{"--p", "project", "--pretty", "false"},
194194
},
195+
{
196+
subScenario: "rewriteRelativeImportExtensionsProjectReferences1",
197+
sys: newTestSys(FileMap{
198+
"/home/src/workspaces/packages/common/tsconfig.json": `{
199+
"compilerOptions": {
200+
"composite": true,
201+
"rootDir": "src",
202+
"outDir": "dist",
203+
"module": "nodenext"
204+
}
205+
}`,
206+
"/home/src/workspaces/packages/common/package.json": `{
207+
"name": "common",
208+
"version": "1.0.0",
209+
"type": "module",
210+
"exports": {
211+
".": {
212+
"source": "./src/index.ts",
213+
"default": "./dist/index.js"
214+
}
215+
}
216+
}`,
217+
"/home/src/workspaces/packages/common/src/index.ts": "export {};",
218+
"/home/src/workspaces/packages/common/dist/index.d.ts": "export {};",
219+
"/home/src/workspaces/packages/main/tsconfig.json": `{
220+
"compilerOptions": {
221+
"module": "nodenext",
222+
"rewriteRelativeImportExtensions": true,
223+
"rootDir": "src",
224+
"outDir": "dist"
225+
},
226+
"references": [
227+
{ "path": "../common" }
228+
]
229+
}`,
230+
"/home/src/workspaces/packages/main/package.json": `{ "type": "module" }`,
231+
"/home/src/workspaces/packages/main/src/index.ts": `import {} from "../../common/src/index.ts";`,
232+
}, "/home/src/workspaces"),
233+
commandLineArgs: []string{"-p", "packages/main", "--pretty", "false"},
234+
},
235+
{
236+
subScenario: "rewriteRelativeImportExtensionsProjectReferences2",
237+
sys: newTestSys(FileMap{
238+
"/home/src/workspaces/solution/src/tsconfig-base.json": `{
239+
"compilerOptions": {
240+
"module": "nodenext",
241+
"composite": true,
242+
"rootDir": ".",
243+
"outDir": "../dist",
244+
"rewriteRelativeImportExtensions": true
245+
}
246+
}`,
247+
"/home/src/workspaces/solution/src/compiler/tsconfig.json": `{
248+
"extends": "../tsconfig-base.json",
249+
"compilerOptions": {}
250+
}`,
251+
"/home/src/workspaces/solution/src/compiler/parser.ts": "export {};",
252+
"/home/src/workspaces/solution/dist/compiler/parser.d.ts": "export {};",
253+
"/home/src/workspaces/solution/src/services/tsconfig.json": `{
254+
"extends": "../tsconfig-base.json",
255+
"compilerOptions": {},
256+
"references": [
257+
{ "path": "../compiler" }
258+
]
259+
}`,
260+
"/home/src/workspaces/solution/src/services/services.ts": `import {} from "../compiler/parser.ts";`,
261+
}, "/home/src/workspaces/solution"),
262+
commandLineArgs: []string{"--p", "src/services", "--pretty", "false"},
263+
},
264+
{
265+
subScenario: "rewriteRelativeImportExtensionsProjectReferences3",
266+
sys: newTestSys(FileMap{
267+
"/home/src/workspaces/solution/src/tsconfig-base.json": `{
268+
"compilerOptions": {
269+
"module": "nodenext",
270+
"composite": true,
271+
"rewriteRelativeImportExtensions": true
272+
}
273+
}`,
274+
"/home/src/workspaces/solution/src/compiler/tsconfig.json": `{
275+
"extends": "../tsconfig-base.json",
276+
"compilerOptions": {
277+
"rootDir": ".",
278+
"outDir": "../../dist/compiler"
279+
}
280+
}`,
281+
"/home/src/workspaces/solution/src/compiler/parser.ts": "export {};",
282+
"/home/src/workspaces/solution/dist/compiler/parser.d.ts": "export {};",
283+
"/home/src/workspaces/solution/src/services/tsconfig.json": `{
284+
"extends": "../tsconfig-base.json",
285+
"compilerOptions": {
286+
"rootDir": ".",
287+
"outDir": "../../dist/services"
288+
},
289+
"references": [
290+
{ "path": "../compiler" }
291+
]
292+
}`,
293+
"/home/src/workspaces/solution/src/services/services.ts": `import {} from "../compiler/parser.ts";`,
294+
}, "/home/src/workspaces/solution"),
295+
commandLineArgs: []string{"--p", "src/services", "--pretty", "false"},
296+
},
195297
}
196298

197299
for _, c := range cases {

internal/transformers/importelision_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ type fakeProgram struct {
2727
getSourceFileForResolvedModule func(FileName string) *ast.SourceFile
2828
}
2929

30+
// GetRedirectForResolution implements checker.Program.
31+
func (p *fakeProgram) GetRedirectForResolution(file ast.HasFileName) *tsoptions.ParsedCommandLine {
32+
panic("unimplemented")
33+
}
34+
35+
// SourceFileMayBeEmitted implements checker.Program.
36+
func (p *fakeProgram) SourceFileMayBeEmitted(sourceFile *ast.SourceFile, forceDtsEmit bool) bool {
37+
panic("unimplemented")
38+
}
39+
3040
// GetEmitSyntaxForUsageLocation implements checker.Program.
3141
func (p *fakeProgram) GetEmitSyntaxForUsageLocation(sourceFile ast.HasFileName, usageLocation *ast.StringLiteralLike) core.ResolutionMode {
3242
panic("unimplemented")

internal/transformers/utilities.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ func tryRenameExternalModule(factory *printer.NodeFactory, moduleName *ast.Liter
336336
}
337337

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

353-
func shouldRewriteModuleSpecifier(specifier string, compilerOptions *core.CompilerOptions) bool {
354-
return compilerOptions.RewriteRelativeImportExtensions.IsTrue() && tspath.PathIsRelative(specifier) && !tspath.IsDeclarationFileName(specifier) && tspath.HasTSFileExtension(specifier)
355-
}
356-
357353
func singleOrMany(nodes []*ast.Node, factory *printer.NodeFactory) *ast.Node {
358354
if len(nodes) == 1 {
359355
return nodes[0]

internal/tsoptions/declscompiler.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -810,8 +810,8 @@ var commonOptionsWithBuild = []*CommandLineOption{
810810
AffectsSemanticDiagnostics: true,
811811
AffectsBuildInfo: true,
812812
Category: diagnostics.Modules,
813-
// description: diagnostics.Rewrite_ts_tsx_mts_and_cts_file_extensions_in_relative_import_paths_to_their_JavaScript_equivalent_in_output_files,
814-
DefaultValueDescription: false,
813+
Description: diagnostics.Rewrite_ts_tsx_mts_and_cts_file_extensions_in_relative_import_paths_to_their_JavaScript_equivalent_in_output_files,
814+
DefaultValueDescription: false,
815815
},
816816
{
817817
Name: "resolvePackageJsonExports",

internal/tspath/path.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,10 @@ func GetRelativePathFromDirectory(fromDirectory string, to string, options Compa
631631
return GetPathFromPathComponents(pathComponents)
632632
}
633633

634+
func GetRelativePathFromFile(from string, to string, options ComparePathsOptions) string {
635+
return EnsurePathIsNonModuleName(GetRelativePathFromDirectory(GetDirectoryPath(from), to, options))
636+
}
637+
634638
func ConvertToRelativePath(absoluteOrRelativePath string, options ComparePathsOptions) string {
635639
if !IsRootedDiskPath(absoluteOrRelativePath) {
636640
return absoluteOrRelativePath
@@ -768,6 +772,15 @@ func PathIsRelative(path string) bool {
768772
return false
769773
}
770774

775+
// EnsurePathIsNonModuleName ensures a path is either absolute (prefixed with `/` or `c:`) or dot-relative (prefixed
776+
// with `./` or `../`) so as not to be confused with an unprefixed module name.
777+
func EnsurePathIsNonModuleName(path string) string {
778+
if !PathIsAbsolute(path) && !PathIsRelative(path) {
779+
return "./" + path
780+
}
781+
return path
782+
}
783+
771784
func IsExternalModuleNameRelative(moduleName string) bool {
772785
// TypeScript 1.0 spec (April 2014): 11.2.1
773786
// An external module name is "relative" if the first term is "." or "..".
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
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".
2+
3+
4+
==== index.ts (1 errors) ====
5+
import foo = require("./foo.ts"); // Error
6+
~~~~~~~~~~
7+
!!! 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".
8+
import type _foo = require("./foo.ts"); // Ok
9+
10+
==== foo.ts/index.ts (0 errors) ====
11+
export = {};
12+

testdata/baselines/reference/submodule/conformance/cjsErrors(module=node18).errors.txt.diff

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
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".
2+
3+
4+
==== index.ts (1 errors) ====
5+
import foo = require("./foo.ts"); // Error
6+
~~~~~~~~~~
7+
!!! 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".
8+
import type _foo = require("./foo.ts"); // Ok
9+
10+
==== foo.ts/index.ts (0 errors) ====
11+
export = {};
12+

testdata/baselines/reference/submodule/conformance/cjsErrors(module=nodenext).errors.txt.diff

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/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.
2+
3+
4+
==== /package.json (0 errors) ====
5+
{
6+
"name": "pkg",
7+
"type": "module",
8+
"imports": {
9+
"#foo.ts": "./foo.ts",
10+
"#internal/*": "./internal/*"
11+
},
12+
"exports": {
13+
"./*.ts": {
14+
"source": "./*.ts",
15+
"default": "./*.js"
16+
}
17+
}
18+
}
19+
20+
==== /foo.ts (0 errors) ====
21+
export {};
22+
23+
==== /internal/foo.ts (0 errors) ====
24+
export {};
25+
26+
==== /index.ts (1 errors) ====
27+
import {} from "#foo.ts"; // Ok
28+
import {} from "#internal/foo.ts"; // Error
29+
~~~~~~~~~~~~~~~~~~
30+
!!! 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.
31+
import {} from "pkg/foo.ts"; // Ok

0 commit comments

Comments
 (0)