330 lines
12 KiB
Go
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)
|
|
}
|