Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions tsc/internal/compiler/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,11 @@ func (p *Program) verifyCompilerOptions() {
}
}

// outputExtension describes what an external build emits, so tsc must not emit JavaScript itself.
if options.OutputExtension != "" && !options.NoEmit.IsTrue() && !options.EmitDeclarationOnly.IsTrue() {
createDiagnosticForOptionName(diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "outputExtension", "noEmit", "emitDeclarationOnly")
}

moduleKind := options.GetEmitModuleKind()

if options.AllowImportingTsExtensions.IsTrue() && !(options.NoEmit.IsTrue() || options.EmitDeclarationOnly.IsTrue() || options.RewriteRelativeImportExtensions.IsTrue()) {
Expand Down
1 change: 1 addition & 0 deletions tsc/internal/core/compileroptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ type CompilerOptions struct {
NoImplicitOverride Tristate `json:"noImplicitOverride,omitzero"`
NoUncheckedSideEffectImports Tristate `json:"noUncheckedSideEffectImports,omitzero"`
OutDir string `json:"outDir,omitzero"`
OutputExtension string `json:"outputExtension,omitzero"`
Paths *collections.OrderedMap[string, []string] `json:"paths,omitzero"`
PreserveConstEnums Tristate `json:"preserveConstEnums,omitzero"`
PreserveSymlinks Tristate `json:"preserveSymlinks,omitzero"`
Expand Down
4 changes: 4 additions & 0 deletions tsc/internal/diagnostics/diagnostics_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions tsc/internal/diagnostics/extraDiagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -338,5 +338,9 @@
"Diagnostic directive {0} returned by the content mapper has an invalid 'unusedExpectDirectiveIndex'.": {
"category": "Message",
"code": 100068
},
"Specify the file extension that an external build gives every source file, so that declaration file names and their relative imports match the build output.": {
"category": "Message",
"code": 100069
}
}
25 changes: 22 additions & 3 deletions tsc/internal/outputpaths/outputpaths.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func GetOutputDeclarationFileNameWorker(inputFileName string, options *core.Comp
if len(dir) == 0 {
dir = options.OutDir
}
return ChangeToDeclarationExtension(getOutputPathWithoutChangingExtension(inputFileName, dir, host), host)
return ChangeToDeclarationExtension(getOutputPathWithoutChangingExtension(inputFileName, dir, host), options, host)
}

func GetOutputExtension(fileName string, jsx core.JsxEmit) string {
Expand Down Expand Up @@ -142,11 +142,14 @@ func GetDeclarationEmitOutputFilePath(file string, options *core.CompilerOptions
} else {
path = file
}
return ChangeToDeclarationExtension(path, host)
return ChangeToDeclarationExtension(path, options, host)
}

func ChangeToDeclarationExtension(path string, host OutputPathsHost) string {
func ChangeToDeclarationExtension(path string, options *core.CompilerOptions, host OutputPathsHost) string {
if extension := tspath.GetLongestExtensionFromPath(path, host.ContentMapperExtensions(), false); extension != "" {
if options.OutputExtension != "" {
return tspath.RemoveExtension(path, extension) + DeclarationExtensionForOutput(options.OutputExtension)
}
return tspath.RemoveExtension(path, extension) + ".d" + extension + ".ts"
}
pathWithoutExtension := tspath.RemoveFileExtension(path)
Expand All @@ -155,9 +158,25 @@ func ChangeToDeclarationExtension(path string, host OutputPathsHost) string {
pathWithoutExtension = tspath.RemoveExtension(path, extension)
}
}
if options.OutputExtension != "" {
return pathWithoutExtension + DeclarationExtensionForOutput(options.OutputExtension)
}
return pathWithoutExtension + tspath.GetDeclarationEmitExtensionForPath(path)
}

// DeclarationExtensionForOutput returns the declaration extension that sits next to a JavaScript file
// with the given extension, for the values that outputExtension accepts.
func DeclarationExtensionForOutput(outputExtension string) string {
switch outputExtension {
case tspath.ExtensionMjs:
return tspath.ExtensionDmts
case tspath.ExtensionCjs:
return tspath.ExtensionDcts
default:
return tspath.ExtensionDts
}
}

func GetSourceFilePathInNewDir(fileName string, newDirPath string, currentDirectory string, commonSourceDirectory string, useCaseSensitiveFileNames bool) string {
return GetSourceFilePathInNewDirWorker(fileName, newDirPath, currentDirectory, commonSourceDirectory, useCaseSensitiveFileNames)
}
Expand Down
2 changes: 1 addition & 1 deletion tsc/internal/testutil/harnessutil/harnessutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,7 @@ func (c *CompilationResult) getOutputPath(path string, ext string) string {
}
}
if ext == tspath.GetDeclarationEmitExtensionForPath(path) {
return outputpaths.ChangeToDeclarationExtension(path, c.Program.Program())
return outputpaths.ChangeToDeclarationExtension(path, c.Program.Program().Options(), c.Program.Program())
}
return tspath.ChangeExtension(path, ext)
}
Expand Down
2 changes: 1 addition & 1 deletion tsc/internal/testutil/tsbaseline/js_emit_baseline.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ func prepareDeclarationCompilationContext(
sourceFileName = sourceFile.FileName()
}

dTsFileName := outputpaths.ChangeToDeclarationExtension(sourceFileName, result.Program.Program())
dTsFileName := outputpaths.ChangeToDeclarationExtension(sourceFileName, result.Program.Program().Options(), result.Program.Program())
return result.DTS.GetOrZero(dTsFileName)
}

Expand Down
27 changes: 26 additions & 1 deletion tsc/internal/transformers/declarations/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -1592,7 +1592,32 @@ func (tx *DeclarationTransformer) rewriteModuleSpecifier(parent *ast.Node, input
return nil
}
tx.resultHasExternalModuleIndicator = tx.resultHasExternalModuleIndicator || (parent.Kind != ast.KindModuleDeclaration && parent.Kind != ast.KindImportType)
return input
return tx.rewriteSpecifierToOutputExtension(input)
}

// rewriteSpecifierToOutputExtension rewrites a relative specifier that resolved to a source file by its
// source extension (`./a.ts`, `./b.vue`) to the extension the external build gives that file, so the
// specifier resolves against the declaration file emitted next to the build output.
func (tx *DeclarationTransformer) rewriteSpecifierToOutputExtension(input *ast.Node) *ast.Node {
outputExtension := tx.compilerOptions.OutputExtension
if outputExtension == "" || !ast.IsStringLiteral(input) || !tspath.PathIsRelative(input.Text()) {
return input
}
resolved := tx.host.GetResolvedModuleFromModuleSpecifier(tx.state.currentSourceFile, input)
if resolved == nil || !resolved.IsResolved() || !(resolved.ResolvedUsingTsExtension || resolved.ResolvedUsingExtraExtensions) {
return input
}
extension := tspath.GetLongestExtensionFromPath(input.Text(), tx.host.ContentMapperExtensions(), !tx.host.UseCaseSensitiveFileNames())
if extension == "" {
extension = tspath.TryExtractTSExtension(input.Text())
}
if extension == "" {
return input
}
updated := tx.Factory().NewStringLiteral(tspath.RemoveExtension(input.Text(), extension)+outputExtension, input.AsStringLiteral().TokenFlags)
tx.EmitContext().SetOriginal(updated, input)
tx.EmitContext().AssignCommentAndSourceMapRanges(updated, input)
return updated
}

func (tx *DeclarationTransformer) preserveJsDoc(updated *ast.Node, original *ast.Node) {
Expand Down
1 change: 1 addition & 0 deletions tsc/internal/tsoptions/commandlineoption.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ var commandLineOptionEnumMap = map[string]*collections.OrderedMap[string, any]{
"moduleDetection": moduleDetectionOptionMap,
"jsx": jsxOptionMap,
"newLine": newLineOptionMap,
"outputExtension": outputExtensionOptionMap,
"watchFile": watchFileEnumMap,
"watchDirectory": watchDirectoryEnumMap,
"fallbackPolling": fallbackEnumMap,
Expand Down
9 changes: 9 additions & 0 deletions tsc/internal/tsoptions/declscompiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,15 @@ var optionsForCompiler = []*CommandLineOption{
transpileOptionValue: core.TSUnknown,
Description: diagnostics.Specify_the_output_directory_for_generated_declaration_files,
},
{
Name: "outputExtension",
Kind: CommandLineOptionTypeEnum, // outputExtensionOptionMap,
AffectsEmit: true,
AffectsBuildInfo: true,
AffectsDeclarationPath: true,
Category: diagnostics.Emit,
Description: diagnostics.Specify_the_file_extension_that_an_external_build_gives_every_source_file_so_that_declaration_file_names_and_their_relative_imports_match_the_build_output,
},
{
Name: "skipLibCheck",
Kind: CommandLineOptionTypeBoolean,
Expand Down
6 changes: 6 additions & 0 deletions tsc/internal/tsoptions/enummaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,12 @@ var newLineOptionMap = collections.NewOrderedMapFromList([]collections.MapEntry[
{Key: "lf", Value: core.NewLineKindLF},
})

var outputExtensionOptionMap = collections.NewOrderedMapFromList([]collections.MapEntry[string, any]{
{Key: ".js", Value: ".js"},
{Key: ".mjs", Value: ".mjs"},
{Key: ".cjs", Value: ".cjs"},
})

var targetToLibMap = map[core.ScriptTarget]string{
core.ScriptTargetESNext: "lib.esnext.full.d.ts",
core.ScriptTargetES2025: "lib.es2025.full.d.ts",
Expand Down
2 changes: 2 additions & 0 deletions tsc/internal/tsoptions/parsinghelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,8 @@ func parseCompilerOptions(key string, value any, allOptions *core.CompilerOption
allOptions.NoUncheckedSideEffectImports = ParseTristate(value)
case "outFile":
allOptions.OutFile = ParseString(value)
case "outputExtension":
allOptions.OutputExtension = ParseString(value)
case "noResolve":
allOptions.NoResolve = ParseTristate(value)
case "paths":
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//// [/other.p] (ScriptKind: ScriptKindTS, ContentMapper: [.y.z .p])
--- Original ---
export declare const other: number;
--- Transformed ---
const __VERSION = "1.0.0";
export declare const other: number;

//// [/component.y.z] (ScriptKind: ScriptKindTS, ContentMapper: [.y.z .p])
--- Original ---
import { helper } from "./helper.ts";
import { other } from "./other.p";
export interface ComponentProps {
label: typeof helper;
count: typeof other;
}
export declare const component: ComponentProps;
--- Transformed ---
const __VERSION = "1.0.0";
import { helper } from "./helper.ts";
import { other } from "./other.p";
export interface ComponentProps {
label: typeof helper;
count: typeof other;
}
export declare const component: ComponentProps;

=== Diagnostics ===

<no content>
59 changes: 59 additions & 0 deletions tsc/testdata/baselines/reference/compiler/outputExtension.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions tsc/testdata/baselines/reference/compiler/outputExtension.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading