package graph // This graph represents the set of files that the linker operates on. Each // linker has a separate one of these graphs (there is one linker when code // splitting is on, but one linker per entry point when code splitting is off). // // The input data to the linker constructor must be considered immutable because // it's shared between linker invocations and is also stored in the cache for // incremental builds. // // The linker constructor makes a shallow clone of the input data and is careful // to pre-clone ahead of time the AST fields that it may modify. The Go language // doesn't have any type system features for immutability so this has to be // manually enforced. Please be careful. import ( "sort" "sync" "github.com/evanw/esbuild/internal/ast" "github.com/evanw/esbuild/internal/helpers" "github.com/evanw/esbuild/internal/js_ast" "github.com/evanw/esbuild/internal/logger" "github.com/evanw/esbuild/internal/runtime" ) type entryPointKind uint8 const ( entryPointNone entryPointKind = iota entryPointUserSpecified entryPointDynamicImport ) type LinkerFile struct { InputFile InputFile // This holds all entry points that can reach this file. It will be used to // assign the parts in this file to a chunk. EntryBits helpers.BitSet // This is lazily-allocated because it's only needed if there are warnings // logged, which should be relatively rare. lazyLineColumnTracker *logger.LineColumnTracker // The minimum number of links in the module graph to get from an entry point // to this file DistanceFromEntryPoint uint32 // If "entryPointKind" is not "entryPointNone", this is the index of the // corresponding entry point chunk. EntryPointChunkIndex uint32 // This file is an entry point if and only if this is not "entryPointNone". // Note that dynamically-imported files are allowed to also be specified by // the user as top-level entry points, so some dynamically-imported files // may be "entryPointUserSpecified" instead of "entryPointDynamicImport". entryPointKind entryPointKind // This is true if this file has been marked as live by the tree shaking // algorithm. IsLive bool } func (f *LinkerFile) IsEntryPoint() bool { return f.entryPointKind != entryPointNone } func (f *LinkerFile) IsUserSpecifiedEntryPoint() bool { return f.entryPointKind == entryPointUserSpecified } // Note: This is not guarded by a mutex. Make sure this isn't called from a // parallel part of the code. func (f *LinkerFile) LineColumnTracker() *logger.LineColumnTracker { if f.lazyLineColumnTracker == nil { tracker := logger.MakeLineColumnTracker(&f.InputFile.Source) f.lazyLineColumnTracker = &tracker } return f.lazyLineColumnTracker } type EntryPoint struct { // This may be an absolute path or a relative path. If absolute, it will // eventually be turned into a relative path by computing the path relative // to the "outbase" directory. Then this relative path will be joined onto // the "outdir" directory to form the final output path for this entry point. OutputPath string // This is the source index of the entry point. This file must have a valid // entry point kind (i.e. not "none"). SourceIndex uint32 // Manually specified output paths are ignored when computing the default // "outbase" directory, which is computed as the lowest common ancestor of // all automatically generated output paths. OutputPathWasAutoGenerated bool } type LinkerGraph struct { Files []LinkerFile entryPoints []EntryPoint Symbols js_ast.SymbolMap // We should avoid traversing all files in the bundle, because the linker // should be able to run a linking operation on a large bundle where only // a few files are needed (e.g. an incremental compilation scenario). This // holds all files that could possibly be reached through the entry points. // If you need to iterate over all files in the linking operation, iterate // over this array. This array is also sorted in a deterministic ordering // to help ensure deterministic builds (source indices are random). ReachableFiles []uint32 // This maps from unstable source index to stable reachable file index. This // is useful as a deterministic key for sorting if you need to sort something // containing a source index (such as "js_ast.Ref" symbol references). StableSourceIndices []uint32 } func CloneLinkerGraph( inputFiles []InputFile, reachableFiles []uint32, originalEntryPoints []EntryPoint, codeSplitting bool, ) LinkerGraph { entryPoints := append([]EntryPoint{}, originalEntryPoints...) symbols := js_ast.NewSymbolMap(len(inputFiles)) files := make([]LinkerFile, len(inputFiles)) // Mark all entry points so we don't add them again for import() expressions for _, entryPoint := range entryPoints { files[entryPoint.SourceIndex].entryPointKind = entryPointUserSpecified } // Clone various things since we may mutate them later. Do this in parallel // for a speedup (around ~2x faster for this function in the three.js // benchmark on a 6-core laptop). var dynamicImportEntryPoints []uint32 var dynamicImportEntryPointsMutex sync.Mutex waitGroup := sync.WaitGroup{} waitGroup.Add(len(reachableFiles)) stableSourceIndices := make([]uint32, len(inputFiles)) for stableIndex, sourceIndex := range reachableFiles { // Create a way to convert source indices to a stable ordering stableSourceIndices[sourceIndex] = uint32(stableIndex) go func(sourceIndex uint32) { file := &files[sourceIndex] file.InputFile = inputFiles[sourceIndex] switch repr := file.InputFile.Repr.(type) { case *JSRepr: // Clone the representation { clone := *repr repr = &clone file.InputFile.Repr = repr } // Clone the symbol map fileSymbols := append([]js_ast.Symbol{}, repr.AST.Symbols...) symbols.SymbolsForSource[sourceIndex] = fileSymbols repr.AST.Symbols = nil // Clone the parts repr.AST.Parts = append([]js_ast.Part{}, repr.AST.Parts...) for i := range repr.AST.Parts { part := &repr.AST.Parts[i] clone := make(map[js_ast.Ref]js_ast.SymbolUse, len(part.SymbolUses)) for ref, uses := range part.SymbolUses { clone[ref] = uses } part.SymbolUses = clone part.Dependencies = append([]js_ast.Dependency{}, part.Dependencies...) } // Clone the import records repr.AST.ImportRecords = append([]ast.ImportRecord{}, repr.AST.ImportRecords...) // Add dynamic imports as additional entry points if code splitting is active if codeSplitting { for importRecordIndex := range repr.AST.ImportRecords { if record := &repr.AST.ImportRecords[importRecordIndex]; record.SourceIndex.IsValid() && record.Kind == ast.ImportDynamic { dynamicImportEntryPointsMutex.Lock() dynamicImportEntryPoints = append(dynamicImportEntryPoints, record.SourceIndex.GetIndex()) dynamicImportEntryPointsMutex.Unlock() } } } // Clone the import map namedImports := make(map[js_ast.Ref]js_ast.NamedImport, len(repr.AST.NamedImports)) for k, v := range repr.AST.NamedImports { namedImports[k] = v } repr.AST.NamedImports = namedImports // Clone the export map resolvedExports := make(map[string]ExportData) for alias, name := range repr.AST.NamedExports { resolvedExports[alias] = ExportData{ Ref: name.Ref, SourceIndex: sourceIndex, NameLoc: name.AliasLoc, } } // Clone the top-level scope so we can generate more variables { new := &js_ast.Scope{} *new = *repr.AST.ModuleScope new.Generated = append([]js_ast.Ref{}, new.Generated...) repr.AST.ModuleScope = new } // Also associate some default metadata with the file repr.Meta.ResolvedExports = resolvedExports repr.Meta.IsProbablyTypeScriptType = make(map[js_ast.Ref]bool) repr.Meta.ImportsToBind = make(map[js_ast.Ref]ImportData) case *CSSRepr: // Clone the representation { clone := *repr repr = &clone file.InputFile.Repr = repr } // Clone the import records repr.AST.ImportRecords = append([]ast.ImportRecord{}, repr.AST.ImportRecords...) } // All files start off as far as possible from an entry point file.DistanceFromEntryPoint = ^uint32(0) waitGroup.Done() }(sourceIndex) } waitGroup.Wait() // Process dynamic entry points after merging control flow again stableEntryPoints := make([]int, 0, len(dynamicImportEntryPoints)) for _, sourceIndex := range dynamicImportEntryPoints { if otherFile := &files[sourceIndex]; otherFile.entryPointKind == entryPointNone { stableEntryPoints = append(stableEntryPoints, int(stableSourceIndices[sourceIndex])) otherFile.entryPointKind = entryPointDynamicImport } } // Make sure to add dynamic entry points in a deterministic order sort.Ints(stableEntryPoints) for _, stableIndex := range stableEntryPoints { entryPoints = append(entryPoints, EntryPoint{SourceIndex: reachableFiles[stableIndex]}) } // Allocate the entry bit set now that the number of entry points is known bitCount := uint(len(entryPoints)) for _, sourceIndex := range reachableFiles { files[sourceIndex].EntryBits = helpers.NewBitSet(bitCount) } return LinkerGraph{ Symbols: symbols, entryPoints: entryPoints, Files: files, ReachableFiles: reachableFiles, StableSourceIndices: stableSourceIndices, } } // Prevent packages that depend on us from adding or removing entry points func (g *LinkerGraph) EntryPoints() []EntryPoint { return g.entryPoints } func (g *LinkerGraph) AddPartToFile(sourceIndex uint32, part js_ast.Part) uint32 { // Invariant: this map is never null if part.SymbolUses == nil { part.SymbolUses = make(map[js_ast.Ref]js_ast.SymbolUse) } repr := g.Files[sourceIndex].InputFile.Repr.(*JSRepr) partIndex := uint32(len(repr.AST.Parts)) repr.AST.Parts = append(repr.AST.Parts, part) // Invariant: the parts for all top-level symbols can be found in the file-level map for _, declaredSymbol := range part.DeclaredSymbols { if declaredSymbol.IsTopLevel { // Check for an existing overlay partIndices, ok := repr.Meta.TopLevelSymbolToPartsOverlay[declaredSymbol.Ref] // If missing, initialize using the original values from the parser if !ok { partIndices = append(partIndices, repr.AST.TopLevelSymbolToPartsFromParser[declaredSymbol.Ref]...) } // Add this part to the overlay partIndices = append(partIndices, partIndex) if repr.Meta.TopLevelSymbolToPartsOverlay == nil { repr.Meta.TopLevelSymbolToPartsOverlay = make(map[js_ast.Ref][]uint32) } repr.Meta.TopLevelSymbolToPartsOverlay[declaredSymbol.Ref] = partIndices } } return partIndex } func (g *LinkerGraph) GenerateNewSymbol(sourceIndex uint32, kind js_ast.SymbolKind, originalName string) js_ast.Ref { sourceSymbols := &g.Symbols.SymbolsForSource[sourceIndex] ref := js_ast.Ref{ SourceIndex: sourceIndex, InnerIndex: uint32(len(*sourceSymbols)), } *sourceSymbols = append(*sourceSymbols, js_ast.Symbol{ Kind: kind, OriginalName: originalName, Link: js_ast.InvalidRef, }) generated := &g.Files[sourceIndex].InputFile.Repr.(*JSRepr).AST.ModuleScope.Generated *generated = append(*generated, ref) return ref } func (g *LinkerGraph) GenerateSymbolImportAndUse( sourceIndex uint32, partIndex uint32, ref js_ast.Ref, useCount uint32, sourceIndexToImportFrom uint32, ) { if useCount == 0 { return } repr := g.Files[sourceIndex].InputFile.Repr.(*JSRepr) part := &repr.AST.Parts[partIndex] // Mark this symbol as used by this part use := part.SymbolUses[ref] use.CountEstimate += useCount part.SymbolUses[ref] = use // Uphold invariants about the CommonJS "exports" and "module" symbols if ref == repr.AST.ExportsRef { repr.AST.UsesExportsRef = true } if ref == repr.AST.ModuleRef { repr.AST.UsesModuleRef = true } // Track that this specific symbol was imported if sourceIndexToImportFrom != sourceIndex { repr.Meta.ImportsToBind[ref] = ImportData{ SourceIndex: sourceIndexToImportFrom, Ref: ref, } } // Pull in all parts that declare this symbol targetRepr := g.Files[sourceIndexToImportFrom].InputFile.Repr.(*JSRepr) for _, partIndex := range targetRepr.TopLevelSymbolToParts(ref) { part.Dependencies = append(part.Dependencies, js_ast.Dependency{ SourceIndex: sourceIndexToImportFrom, PartIndex: partIndex, }) } } func (g *LinkerGraph) GenerateRuntimeSymbolImportAndUse( sourceIndex uint32, partIndex uint32, name string, useCount uint32, ) { if useCount == 0 { return } runtimeRepr := g.Files[runtime.SourceIndex].InputFile.Repr.(*JSRepr) ref := runtimeRepr.AST.NamedExports[name].Ref g.GenerateSymbolImportAndUse(sourceIndex, partIndex, ref, useCount, runtime.SourceIndex) }