2025-10-15 10:12:44 +03:00

483 lines
18 KiB
Go

package compiler
import (
"encoding/base64"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/binder"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/diagnostics"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/outputpaths"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/printer"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/sourcemap"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/transformers"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/transformers/declarations"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/transformers/estransforms"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/transformers/inliners"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/transformers/jsxtransforms"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/transformers/moduletransforms"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/transformers/tstransforms"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tsoptions"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
)
type EmitOnly byte
const (
EmitAll EmitOnly = iota
EmitOnlyJs
EmitOnlyDts
EmitOnlyForcedDts
)
type emitter struct {
host EmitHost
emitOnly EmitOnly
emitterDiagnostics ast.DiagnosticsCollection
writer printer.EmitTextWriter
paths *outputpaths.OutputPaths
sourceFile *ast.SourceFile
emitResult EmitResult
writeFile func(fileName string, text string, writeByteOrderMark bool, data *WriteFileData) error
}
func (e *emitter) emit() {
// !!! tracing
e.emitJSFile(e.sourceFile, e.paths.JsFilePath(), e.paths.SourceMapFilePath())
e.emitDeclarationFile(e.sourceFile, e.paths.DeclarationFilePath(), e.paths.DeclarationMapPath())
e.emitResult.Diagnostics = e.emitterDiagnostics.GetDiagnostics()
}
func (e *emitter) getDeclarationTransformers(emitContext *printer.EmitContext, declarationFilePath string, declarationMapPath string) []*declarations.DeclarationTransformer {
transform := declarations.NewDeclarationTransformer(e.host, emitContext, e.host.Options(), declarationFilePath, declarationMapPath)
return []*declarations.DeclarationTransformer{transform}
}
func getModuleTransformer(opts *transformers.TransformOptions) *transformers.Transformer {
switch opts.CompilerOptions.GetEmitModuleKind() {
case core.ModuleKindPreserve:
// `ESModuleTransformer` contains logic for preserving CJS input syntax in `--module preserve`
return moduletransforms.NewESModuleTransformer(opts)
case core.ModuleKindESNext,
core.ModuleKindES2022,
core.ModuleKindES2020,
core.ModuleKindES2015,
core.ModuleKindNode20,
core.ModuleKindNode18,
core.ModuleKindNode16,
core.ModuleKindNodeNext,
core.ModuleKindCommonJS:
return moduletransforms.NewImpliedModuleTransformer(opts)
default:
return moduletransforms.NewCommonJSModuleTransformer(opts)
}
}
func getScriptTransformers(emitContext *printer.EmitContext, host printer.EmitHost, sourceFile *ast.SourceFile) []*transformers.Transformer {
var tx []*transformers.Transformer
options := host.Options()
// JS files don't use reference calculations as they don't do import elision, no need to calculate it
importElisionEnabled := !options.VerbatimModuleSyntax.IsTrue() && !ast.IsInJSFile(sourceFile.AsNode())
var emitResolver printer.EmitResolver
var referenceResolver binder.ReferenceResolver
if importElisionEnabled || options.GetJSXTransformEnabled() || !options.GetIsolatedModules() { // full emit resolver is needed for import ellision and const enum inlining
emitResolver = host.GetEmitResolver()
emitResolver.MarkLinkedReferencesRecursively(sourceFile)
referenceResolver = emitResolver
} else {
referenceResolver = binder.NewReferenceResolver(options, binder.ReferenceResolverHooks{})
}
opts := transformers.TransformOptions{
Context: emitContext,
CompilerOptions: options,
Resolver: referenceResolver,
EmitResolver: emitResolver,
GetEmitModuleFormatOfFile: host.GetEmitModuleFormatOfFile,
}
// transform TypeScript syntax
{
// erase types
tx = append(tx, tstransforms.NewTypeEraserTransformer(&opts))
// elide imports
if importElisionEnabled {
tx = append(tx, tstransforms.NewImportElisionTransformer(&opts))
}
// transform `enum`, `namespace`, and parameter properties
tx = append(tx, tstransforms.NewRuntimeSyntaxTransformer(&opts))
}
// !!! transform legacy decorator syntax
if options.GetJSXTransformEnabled() {
tx = append(tx, jsxtransforms.NewJSXTransformer(&opts))
}
downleveler := estransforms.GetESTransformer(&opts)
if downleveler != nil {
tx = append(tx, downleveler)
}
// transform module syntax
tx = append(tx, getModuleTransformer(&opts))
// inlining (formerly done via substitutions)
if !options.GetIsolatedModules() {
tx = append(tx, inliners.NewConstEnumInliningTransformer(&opts))
}
return tx
}
func (e *emitter) emitJSFile(sourceFile *ast.SourceFile, jsFilePath string, sourceMapFilePath string) {
options := e.host.Options()
if sourceFile == nil || e.emitOnly != EmitAll && e.emitOnly != EmitOnlyJs || len(jsFilePath) == 0 {
return
}
if options.NoEmit == core.TSTrue || e.host.IsEmitBlocked(jsFilePath) {
e.emitResult.EmitSkipped = true
return
}
emitContext, putEmitContext := printer.GetEmitContext()
defer putEmitContext()
for _, transformer := range getScriptTransformers(emitContext, e.host, sourceFile) {
sourceFile = transformer.TransformSourceFile(sourceFile)
}
printerOptions := printer.PrinterOptions{
RemoveComments: options.RemoveComments.IsTrue(),
NewLine: options.NewLine,
NoEmitHelpers: options.NoEmitHelpers.IsTrue(),
SourceMap: options.SourceMap.IsTrue(),
InlineSourceMap: options.InlineSourceMap.IsTrue(),
InlineSources: options.InlineSources.IsTrue(),
// !!!
}
// create a printer to print the nodes
printer := printer.NewPrinter(printerOptions, printer.PrintHandlers{
// !!!
}, emitContext)
e.printSourceFile(jsFilePath, sourceMapFilePath, sourceFile, printer, shouldEmitSourceMaps(options, sourceFile))
}
func (e *emitter) emitDeclarationFile(sourceFile *ast.SourceFile, declarationFilePath string, declarationMapPath string) {
options := e.host.Options()
if sourceFile == nil || e.emitOnly == EmitOnlyJs || len(declarationFilePath) == 0 {
return
}
if e.emitOnly != EmitOnlyForcedDts && (options.NoEmit == core.TSTrue || e.host.IsEmitBlocked(declarationFilePath)) {
e.emitResult.EmitSkipped = true
return
}
var diags []*ast.Diagnostic
emitContext, putEmitContext := printer.GetEmitContext()
defer putEmitContext()
for _, transformer := range e.getDeclarationTransformers(emitContext, declarationFilePath, declarationMapPath) {
sourceFile = transformer.TransformSourceFile(sourceFile)
diags = append(diags, transformer.GetDiagnostics()...)
}
// !!! strada skipped emit if there were diagnostics
printerOptions := printer.PrinterOptions{
RemoveComments: options.RemoveComments.IsTrue(),
OnlyPrintJSDocStyle: true,
NewLine: options.NewLine,
NoEmitHelpers: options.NoEmitHelpers.IsTrue(),
SourceMap: options.DeclarationMap.IsTrue(),
InlineSourceMap: options.InlineSourceMap.IsTrue(),
InlineSources: options.InlineSources.IsTrue(),
// !!!
}
// create a printer to print the nodes
printer := printer.NewPrinter(printerOptions, printer.PrintHandlers{
// !!!
}, emitContext)
for _, elem := range diags {
// Add declaration transform diagnostics to emit diagnostics
e.emitterDiagnostics.Add(elem)
}
e.printSourceFile(declarationFilePath, declarationMapPath, sourceFile, printer, e.emitOnly != EmitOnlyForcedDts && shouldEmitDeclarationSourceMaps(options, sourceFile))
}
func (e *emitter) printSourceFile(jsFilePath string, sourceMapFilePath string, sourceFile *ast.SourceFile, printer_ *printer.Printer, shouldEmitSourceMaps bool) {
// !!! sourceMapGenerator
options := e.host.Options()
var sourceMapGenerator *sourcemap.Generator
if shouldEmitSourceMaps {
sourceMapGenerator = sourcemap.NewGenerator(
tspath.GetBaseFileName(tspath.NormalizeSlashes(jsFilePath)),
getSourceRoot(options),
e.getSourceMapDirectory(options, jsFilePath, sourceFile),
tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: e.host.UseCaseSensitiveFileNames(),
CurrentDirectory: e.host.GetCurrentDirectory(),
},
)
}
printer_.Write(sourceFile.AsNode(), sourceFile, e.writer, sourceMapGenerator)
sourceMapUrlPos := -1
if sourceMapGenerator != nil {
if options.SourceMap.IsTrue() || options.InlineSourceMap.IsTrue() || options.GetAreDeclarationMapsEnabled() {
e.emitResult.SourceMaps = append(e.emitResult.SourceMaps, &SourceMapEmitResult{
InputSourceFileNames: sourceMapGenerator.Sources(),
SourceMap: sourceMapGenerator.RawSourceMap(),
GeneratedFile: jsFilePath,
})
}
sourceMappingURL := e.getSourceMappingURL(
options,
sourceMapGenerator,
jsFilePath,
sourceMapFilePath,
sourceFile,
)
if len(sourceMappingURL) > 0 {
if !e.writer.IsAtStartOfLine() {
e.writer.RawWrite(core.IfElse(options.NewLine == core.NewLineKindCRLF, "\r\n", "\n"))
}
sourceMapUrlPos = e.writer.GetTextPos()
e.writer.WriteComment("//# sourceMappingURL=" + sourceMappingURL)
}
// Write the source map
if len(sourceMapFilePath) > 0 {
sourceMap := sourceMapGenerator.String()
err := e.writeText(sourceMapFilePath, sourceMap, false /*writeByteOrderMark*/, nil)
if err != nil {
e.emitterDiagnostics.Add(ast.NewCompilerDiagnostic(diagnostics.Could_not_write_file_0_Colon_1, jsFilePath, err.Error()))
} else {
e.emitResult.EmittedFiles = append(e.emitResult.EmittedFiles, sourceMapFilePath)
}
}
} else {
e.writer.WriteLine()
}
// Write the output file
text := e.writer.String()
data := &WriteFileData{
SourceMapUrlPos: sourceMapUrlPos,
Diagnostics: e.emitterDiagnostics.GetDiagnostics(),
}
err := e.writeText(jsFilePath, text, options.EmitBOM.IsTrue(), data)
skippedDtsWrite := data.SkippedDtsWrite
if err != nil {
e.emitterDiagnostics.Add(ast.NewCompilerDiagnostic(diagnostics.Could_not_write_file_0_Colon_1, jsFilePath, err.Error()))
} else if !skippedDtsWrite {
e.emitResult.EmittedFiles = append(e.emitResult.EmittedFiles, jsFilePath)
}
// Reset state
e.writer.Clear()
}
func (e *emitter) writeText(fileName string, text string, writeByteOrderMark bool, data *WriteFileData) error {
if e.writeFile != nil {
return e.writeFile(fileName, text, writeByteOrderMark, data)
}
return e.host.WriteFile(fileName, text, writeByteOrderMark)
}
func shouldEmitSourceMaps(mapOptions *core.CompilerOptions, sourceFile *ast.SourceFile) bool {
return (mapOptions.SourceMap.IsTrue() || mapOptions.InlineSourceMap.IsTrue()) &&
!tspath.FileExtensionIs(sourceFile.FileName(), tspath.ExtensionJson)
}
func shouldEmitDeclarationSourceMaps(mapOptions *core.CompilerOptions, sourceFile *ast.SourceFile) bool {
return mapOptions.DeclarationMap.IsTrue() &&
!tspath.FileExtensionIs(sourceFile.FileName(), tspath.ExtensionJson)
}
func getSourceRoot(mapOptions *core.CompilerOptions) string {
// Normalize source root and make sure it has trailing "/" so that it can be used to combine paths with the
// relative paths of the sources list in the sourcemap
sourceRoot := tspath.NormalizeSlashes(mapOptions.SourceRoot)
if len(sourceRoot) > 0 {
sourceRoot = tspath.EnsureTrailingDirectorySeparator(sourceRoot)
}
return sourceRoot
}
func (e *emitter) getSourceMapDirectory(mapOptions *core.CompilerOptions, filePath string, sourceFile *ast.SourceFile) string {
if len(mapOptions.SourceRoot) > 0 {
return e.host.CommonSourceDirectory()
}
if len(mapOptions.MapRoot) > 0 {
sourceMapDir := tspath.NormalizeSlashes(mapOptions.MapRoot)
if sourceFile != nil {
// For modules or multiple emit files the mapRoot will have directory structure like the sources
// So if src\a.ts and src\lib\b.ts are compiled together user would be moving the maps into mapRoot\a.js.map and mapRoot\lib\b.js.map
sourceMapDir = tspath.GetDirectoryPath(outputpaths.GetSourceFilePathInNewDir(
sourceFile.FileName(),
sourceMapDir,
e.host.GetCurrentDirectory(),
e.host.CommonSourceDirectory(),
e.host.UseCaseSensitiveFileNames(),
))
}
if tspath.GetRootLength(sourceMapDir) == 0 {
// The relative paths are relative to the common directory
sourceMapDir = tspath.CombinePaths(e.host.CommonSourceDirectory(), sourceMapDir)
}
return sourceMapDir
}
return tspath.GetDirectoryPath(tspath.NormalizePath(filePath))
}
func (e *emitter) getSourceMappingURL(mapOptions *core.CompilerOptions, sourceMapGenerator *sourcemap.Generator, filePath string, sourceMapFilePath string, sourceFile *ast.SourceFile) string {
if mapOptions.InlineSourceMap.IsTrue() {
// Encode the sourceMap into the sourceMap url
sourceMapText := sourceMapGenerator.String()
base64SourceMapText := base64.StdEncoding.EncodeToString([]byte(sourceMapText))
return "data:application/json;base64," + base64SourceMapText
}
sourceMapFile := tspath.GetBaseFileName(tspath.NormalizeSlashes(sourceMapFilePath))
if len(mapOptions.MapRoot) > 0 {
sourceMapDir := tspath.NormalizeSlashes(mapOptions.MapRoot)
if sourceFile != nil {
// For modules or multiple emit files the mapRoot will have directory structure like the sources
// So if src\a.ts and src\lib\b.ts are compiled together user would be moving the maps into mapRoot\a.js.map and mapRoot\lib\b.js.map
sourceMapDir = tspath.GetDirectoryPath(outputpaths.GetSourceFilePathInNewDir(
sourceFile.FileName(),
sourceMapDir,
e.host.GetCurrentDirectory(),
e.host.CommonSourceDirectory(),
e.host.UseCaseSensitiveFileNames(),
))
}
if tspath.GetRootLength(sourceMapDir) == 0 {
// The relative paths are relative to the common directory
sourceMapDir = tspath.CombinePaths(e.host.CommonSourceDirectory(), sourceMapDir)
return stringutil.EncodeURI(
tspath.GetRelativePathToDirectoryOrUrl(
tspath.GetDirectoryPath(tspath.NormalizePath(filePath)), // get the relative sourceMapDir path based on jsFilePath
tspath.CombinePaths(sourceMapDir, sourceMapFile), // this is where user expects to see sourceMap
/*isAbsolutePathAnUrl*/ true,
tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: e.host.UseCaseSensitiveFileNames(),
CurrentDirectory: e.host.GetCurrentDirectory(),
},
),
)
} else {
return stringutil.EncodeURI(tspath.CombinePaths(sourceMapDir, sourceMapFile))
}
}
return stringutil.EncodeURI(sourceMapFile)
}
type SourceFileMayBeEmittedHost interface {
Options() *core.CompilerOptions
GetProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference
IsSourceFileFromExternalLibrary(file *ast.SourceFile) bool
GetCurrentDirectory() string
UseCaseSensitiveFileNames() bool
SourceFiles() []*ast.SourceFile
}
func sourceFileMayBeEmitted(sourceFile *ast.SourceFile, host SourceFileMayBeEmittedHost, forceDtsEmit bool) bool {
// TODO: move this to outputpaths?
options := host.Options()
// Js files are emitted only if option is enabled
if options.NoEmitForJsFiles.IsTrue() && ast.IsSourceFileJS(sourceFile) {
return false
}
// Declaration files are not emitted
if sourceFile.IsDeclarationFile {
return false
}
// Source file from node_modules are not emitted
if host.IsSourceFileFromExternalLibrary(sourceFile) {
return false
}
// forcing dts emit => file needs to be emitted
if forceDtsEmit {
return true
}
// Check other conditions for file emit
// Source files from referenced projects are not emitted
if host.GetProjectReferenceFromSource(sourceFile.Path()) != nil {
return false
}
// Any non json file should be emitted
if !ast.IsJsonSourceFile(sourceFile) {
return true
}
// Json file is not emitted if outDir is not specified
if options.OutDir == "" {
return false
}
// Otherwise if rootDir or composite config file, we know common sourceDir and can check if file would be emitted in same location
if options.RootDir != "" || (options.Composite.IsTrue() && options.ConfigFilePath != "") {
commonDir := tspath.GetNormalizedAbsolutePath(outputpaths.GetCommonSourceDirectory(options, func() []string { return nil }, host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames()), host.GetCurrentDirectory())
outputPath := outputpaths.GetSourceFilePathInNewDirWorker(sourceFile.FileName(), options.OutDir, host.GetCurrentDirectory(), commonDir, host.UseCaseSensitiveFileNames())
if tspath.ComparePaths(sourceFile.FileName(), outputPath, tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(),
CurrentDirectory: host.GetCurrentDirectory(),
}) == 0 {
return false
}
}
return true
}
func getSourceFilesToEmit(host SourceFileMayBeEmittedHost, targetSourceFile *ast.SourceFile, forceDtsEmit bool) []*ast.SourceFile {
var sourceFiles []*ast.SourceFile
if targetSourceFile != nil {
sourceFiles = []*ast.SourceFile{targetSourceFile}
} else {
sourceFiles = host.SourceFiles()
}
return core.Filter(sourceFiles, func(sourceFile *ast.SourceFile) bool {
return sourceFileMayBeEmitted(sourceFile, host, forceDtsEmit)
})
}
func isSourceFileNotJson(file *ast.SourceFile) bool {
return !ast.IsJsonSourceFile(file)
}
func getDeclarationDiagnostics(host EmitHost, file *ast.SourceFile) []*ast.Diagnostic {
// TODO: use p.getSourceFilesToEmit cache
fullFiles := core.Filter(getSourceFilesToEmit(host, file, false), isSourceFileNotJson)
if !core.Some(fullFiles, func(f *ast.SourceFile) bool { return f == file }) {
return []*ast.Diagnostic{}
}
options := host.Options()
transform := declarations.NewDeclarationTransformer(host, nil, options, "", "")
transform.TransformSourceFile(file)
return transform.GetDiagnostics()
}