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

330 lines
12 KiB
Go

package incremental
import (
"context"
"sync/atomic"
"time"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
)
type emitUpdate struct {
pendingKind FileEmitKind
result *compiler.EmitResult
dtsErrorsFromCache bool
}
type emitFilesHandler struct {
ctx context.Context
program *Program
isForDtsErrors bool
signatures collections.SyncMap[tspath.Path, string]
emitSignatures collections.SyncMap[tspath.Path, *emitSignature]
latestChangedDtsFiles collections.SyncMap[tspath.Path, string]
deletedPendingKinds collections.Set[tspath.Path]
emitUpdates collections.SyncMap[tspath.Path, *emitUpdate]
hasEmitDiagnostics atomic.Bool
}
// Determining what all is pending to be emitted based on previous options or previous file emit flags
func (h *emitFilesHandler) getPendingEmitKindForEmitOptions(emitKind FileEmitKind, options compiler.EmitOptions) FileEmitKind {
pendingKind := getPendingEmitKind(emitKind, 0)
if options.EmitOnly == compiler.EmitOnlyDts {
pendingKind &= FileEmitKindAllDts
}
if h.isForDtsErrors {
pendingKind &= FileEmitKindDtsErrors
}
return pendingKind
}
// Emits the next affected file's emit result (EmitResult and sourceFiles emitted) or returns undefined if iteration is complete
// The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host
// in that order would be used to write the files
func (h *emitFilesHandler) emitAllAffectedFiles(options compiler.EmitOptions) *compiler.EmitResult {
// Emit all affected files
if h.program.snapshot.canUseIncrementalState() {
results := h.emitFilesIncremental(options)
if h.isForDtsErrors {
if options.TargetSourceFile != nil {
// Result from cache
diagnostics, _ := h.program.snapshot.emitDiagnosticsPerFile.Load(options.TargetSourceFile.Path())
return &compiler.EmitResult{
EmitSkipped: true,
Diagnostics: diagnostics.getDiagnostics(h.program.program, options.TargetSourceFile),
}
}
return compiler.CombineEmitResults(results)
} else {
// Combine results and update buildInfo
result := compiler.CombineEmitResults(results)
h.emitBuildInfo(options, result)
return result
}
} else if !h.isForDtsErrors {
result := h.program.program.Emit(h.ctx, h.getEmitOptions(options))
h.updateSnapshot()
h.emitBuildInfo(options, result)
return result
} else {
result := &compiler.EmitResult{
EmitSkipped: true,
Diagnostics: h.program.program.GetDeclarationDiagnostics(h.ctx, options.TargetSourceFile),
}
if len(result.Diagnostics) != 0 {
h.program.snapshot.hasEmitDiagnostics = true
}
return result
}
}
func (h *emitFilesHandler) emitBuildInfo(options compiler.EmitOptions, result *compiler.EmitResult) {
buildInfoResult := h.program.emitBuildInfo(h.ctx, options)
if buildInfoResult != nil {
result.Diagnostics = append(result.Diagnostics, buildInfoResult.Diagnostics...)
result.EmittedFiles = append(result.EmittedFiles, buildInfoResult.EmittedFiles...)
}
}
func (h *emitFilesHandler) emitFilesIncremental(options compiler.EmitOptions) []*compiler.EmitResult {
// Get all affected files
collectAllAffectedFiles(h.ctx, h.program)
if h.ctx.Err() != nil {
return nil
}
wg := core.NewWorkGroup(h.program.program.SingleThreaded())
h.program.snapshot.affectedFilesPendingEmit.Range(func(path tspath.Path, emitKind FileEmitKind) bool {
affectedFile := h.program.program.GetSourceFileByPath(path)
if affectedFile == nil || !h.program.program.SourceFileMayBeEmitted(affectedFile, false) {
h.deletedPendingKinds.Add(path)
return true
}
pendingKind := h.getPendingEmitKindForEmitOptions(emitKind, options)
if pendingKind != 0 {
wg.Queue(func() {
// Determine if we can do partial emit
var emitOnly compiler.EmitOnly
if (pendingKind & FileEmitKindAllJs) != 0 {
emitOnly = compiler.EmitOnlyJs
}
if (pendingKind & FileEmitKindAllDts) != 0 {
if emitOnly == compiler.EmitOnlyJs {
emitOnly = compiler.EmitAll
} else {
emitOnly = compiler.EmitOnlyDts
}
}
var result *compiler.EmitResult
if !h.isForDtsErrors {
result = h.program.program.Emit(h.ctx, h.getEmitOptions(compiler.EmitOptions{
TargetSourceFile: affectedFile,
EmitOnly: emitOnly,
WriteFile: options.WriteFile,
}))
} else {
result = &compiler.EmitResult{
EmitSkipped: true,
Diagnostics: h.program.program.GetDeclarationDiagnostics(h.ctx, affectedFile),
}
}
// Update the pendingEmit for the file
h.emitUpdates.Store(path, &emitUpdate{pendingKind: getPendingEmitKind(emitKind, pendingKind), result: result})
})
}
return true
})
wg.RunAndWait()
if h.ctx.Err() != nil {
return nil
}
// Get updated errors that were not included in affected files emit
h.program.snapshot.emitDiagnosticsPerFile.Range(func(path tspath.Path, diagnostics *diagnosticsOrBuildInfoDiagnosticsWithFileName) bool {
if _, ok := h.emitUpdates.Load(path); !ok {
affectedFile := h.program.program.GetSourceFileByPath(path)
if affectedFile == nil || !h.program.program.SourceFileMayBeEmitted(affectedFile, false) {
h.deletedPendingKinds.Add(path)
return true
}
pendingKind, _ := h.program.snapshot.affectedFilesPendingEmit.Load(path)
h.emitUpdates.Store(path, &emitUpdate{
pendingKind: pendingKind,
result: &compiler.EmitResult{
EmitSkipped: true,
Diagnostics: diagnostics.getDiagnostics(h.program.program, affectedFile),
},
dtsErrorsFromCache: true,
})
}
return true
})
return h.updateSnapshot()
}
func (h *emitFilesHandler) getEmitOptions(options compiler.EmitOptions) compiler.EmitOptions {
if !h.program.snapshot.options.GetEmitDeclarations() {
return options
}
canUseIncrementalState := h.program.snapshot.canUseIncrementalState()
return compiler.EmitOptions{
TargetSourceFile: options.TargetSourceFile,
EmitOnly: options.EmitOnly,
WriteFile: func(fileName string, text string, writeByteOrderMark bool, data *compiler.WriteFileData) error {
var differsOnlyInMap bool
if tspath.IsDeclarationFileName(fileName) {
if canUseIncrementalState {
var emitSignature string
info, _ := h.program.snapshot.fileInfos.Load(options.TargetSourceFile.Path())
if info.signature == info.version {
signature := h.program.snapshot.computeSignatureWithDiagnostics(options.TargetSourceFile, text, data)
// With d.ts diagnostics they are also part of the signature so emitSignature will be different from it since its just hash of d.ts
if len(data.Diagnostics) == 0 {
emitSignature = signature
}
if signature != info.version { // Update it
h.signatures.Store(options.TargetSourceFile.Path(), signature)
}
}
// Store d.ts emit hash so later can be compared to check if d.ts has changed.
// Currently we do this only for composite projects since these are the only projects that can be referenced by other projects
// and would need their d.ts change time in --build mode
if h.skipDtsOutputOfComposite(options.TargetSourceFile, fileName, text, data, emitSignature, &differsOnlyInMap) {
return nil
}
} else if len(data.Diagnostics) > 0 {
h.hasEmitDiagnostics.Store(true)
}
}
var aTime time.Time
if differsOnlyInMap {
aTime = h.program.host.GetMTime(fileName)
}
var err error
if options.WriteFile != nil {
err = options.WriteFile(fileName, text, writeByteOrderMark, data)
} else {
err = h.program.program.Host().FS().WriteFile(fileName, text, writeByteOrderMark)
}
if err == nil && differsOnlyInMap {
// Revert the time to original one
err = h.program.host.SetMTime(fileName, aTime)
}
return err
},
}
}
// Compare to existing computed signature and store it or handle the changes in d.ts map option from before
// returning undefined means that, we dont need to emit this d.ts file since its contents didnt change
func (h *emitFilesHandler) skipDtsOutputOfComposite(file *ast.SourceFile, outputFileName string, text string, data *compiler.WriteFileData, newSignature string, differsOnlyInMap *bool) bool {
if !h.program.snapshot.options.Composite.IsTrue() {
return false
}
var oldSignature string
oldSignatureFormat, ok := h.program.snapshot.emitSignatures.Load(file.Path())
if ok {
if oldSignatureFormat.signature != "" {
oldSignature = oldSignatureFormat.signature
} else {
oldSignature = oldSignatureFormat.signatureWithDifferentOptions[0]
}
}
if newSignature == "" {
newSignature = h.program.snapshot.computeHash(getTextHandlingSourceMapForSignature(text, data))
}
// Dont write dts files if they didn't change
if newSignature == oldSignature {
// If the signature was encoded as string the dts map options match so nothing to do
if oldSignatureFormat != nil && oldSignatureFormat.signature == oldSignature {
data.SkippedDtsWrite = true
return true
} else {
// Mark as differsOnlyInMap so that we can reverse the timestamp with --build so that
// the downstream projects dont detect this as change in d.ts file
*differsOnlyInMap = h.program.Options().Build.IsTrue()
}
} else {
h.latestChangedDtsFiles.Store(file.Path(), outputFileName)
}
h.emitSignatures.Store(file.Path(), &emitSignature{signature: newSignature})
return false
}
func (h *emitFilesHandler) updateSnapshot() []*compiler.EmitResult {
if h.program.snapshot.canUseIncrementalState() {
h.signatures.Range(func(file tspath.Path, signature string) bool {
info, _ := h.program.snapshot.fileInfos.Load(file)
info.signature = signature
if h.program.testingData != nil {
h.program.testingData.UpdatedSignatureKinds[file] = SignatureUpdateKindStoredAtEmit
}
h.program.snapshot.buildInfoEmitPending.Store(true)
return true
})
h.emitSignatures.Range(func(file tspath.Path, signature *emitSignature) bool {
h.program.snapshot.emitSignatures.Store(file, signature)
h.program.snapshot.buildInfoEmitPending.Store(true)
return true
})
for file := range h.deletedPendingKinds.Keys() {
h.program.snapshot.affectedFilesPendingEmit.Delete(file)
h.program.snapshot.buildInfoEmitPending.Store(true)
}
// Always use correct order when to collect the result
var results []*compiler.EmitResult
for _, file := range h.program.GetSourceFiles() {
if latestChangedDtsFile, ok := h.latestChangedDtsFiles.Load(file.Path()); ok {
h.program.snapshot.latestChangedDtsFile = latestChangedDtsFile
h.program.snapshot.buildInfoEmitPending.Store(true)
h.program.snapshot.hasChangedDtsFile = true
}
if update, ok := h.emitUpdates.Load(file.Path()); ok {
if !update.dtsErrorsFromCache {
if update.pendingKind == 0 {
h.program.snapshot.affectedFilesPendingEmit.Delete(file.Path())
} else {
h.program.snapshot.affectedFilesPendingEmit.Store(file.Path(), update.pendingKind)
}
h.program.snapshot.buildInfoEmitPending.Store(true)
}
if update.result != nil {
results = append(results, update.result)
if len(update.result.Diagnostics) != 0 {
h.program.snapshot.emitDiagnosticsPerFile.Store(file.Path(), &diagnosticsOrBuildInfoDiagnosticsWithFileName{diagnostics: update.result.Diagnostics})
}
}
}
}
return results
} else if h.hasEmitDiagnostics.Load() {
h.program.snapshot.hasEmitDiagnostics = true
}
return nil
}
func emitFiles(ctx context.Context, program *Program, options compiler.EmitOptions, isForDtsErrors bool) *compiler.EmitResult {
emitHandler := &emitFilesHandler{ctx: ctx, program: program, isForDtsErrors: isForDtsErrors}
// Single file emit - do direct from program
if !isForDtsErrors && options.TargetSourceFile != nil {
result := program.program.Emit(ctx, emitHandler.getEmitOptions(options))
if ctx.Err() != nil {
return nil
}
emitHandler.updateSnapshot()
return result
}
// Emit only affected files if using builder for emit
return emitHandler.emitAllAffectedFiles(options)
}