github.com/evanw/esbuild@v0.21.4/internal/graph/graph.go (about) 1 package graph 2 3 // This graph represents the set of files that the linker operates on. Each 4 // linker has a separate one of these graphs (there is one linker when code 5 // splitting is on, but one linker per entry point when code splitting is off). 6 // 7 // The input data to the linker constructor must be considered immutable because 8 // it's shared between linker invocations and is also stored in the cache for 9 // incremental builds. 10 // 11 // The linker constructor makes a shallow clone of the input data and is careful 12 // to pre-clone ahead of time the AST fields that it may modify. The Go language 13 // doesn't have any type system features for immutability so this has to be 14 // manually enforced. Please be careful. 15 16 import ( 17 "sort" 18 "sync" 19 20 "github.com/evanw/esbuild/internal/ast" 21 "github.com/evanw/esbuild/internal/helpers" 22 "github.com/evanw/esbuild/internal/js_ast" 23 "github.com/evanw/esbuild/internal/logger" 24 "github.com/evanw/esbuild/internal/runtime" 25 ) 26 27 type entryPointKind uint8 28 29 const ( 30 entryPointNone entryPointKind = iota 31 entryPointUserSpecified 32 entryPointDynamicImport 33 ) 34 35 type LinkerFile struct { 36 // This holds all entry points that can reach this file. It will be used to 37 // assign the parts in this file to a chunk. 38 EntryBits helpers.BitSet 39 40 // This is lazily-allocated because it's only needed if there are warnings 41 // logged, which should be relatively rare. 42 lazyLineColumnTracker *logger.LineColumnTracker 43 44 InputFile InputFile 45 46 // The minimum number of links in the module graph to get from an entry point 47 // to this file 48 DistanceFromEntryPoint uint32 49 50 // If "entryPointKind" is not "entryPointNone", this is the index of the 51 // corresponding entry point chunk. 52 EntryPointChunkIndex uint32 53 54 // This file is an entry point if and only if this is not "entryPointNone". 55 // Note that dynamically-imported files are allowed to also be specified by 56 // the user as top-level entry points, so some dynamically-imported files 57 // may be "entryPointUserSpecified" instead of "entryPointDynamicImport". 58 entryPointKind entryPointKind 59 60 // This is true if this file has been marked as live by the tree shaking 61 // algorithm. 62 IsLive bool 63 } 64 65 func (f *LinkerFile) IsEntryPoint() bool { 66 return f.entryPointKind != entryPointNone 67 } 68 69 func (f *LinkerFile) IsUserSpecifiedEntryPoint() bool { 70 return f.entryPointKind == entryPointUserSpecified 71 } 72 73 // Note: This is not guarded by a mutex. Make sure this isn't called from a 74 // parallel part of the code. 75 func (f *LinkerFile) LineColumnTracker() *logger.LineColumnTracker { 76 if f.lazyLineColumnTracker == nil { 77 tracker := logger.MakeLineColumnTracker(&f.InputFile.Source) 78 f.lazyLineColumnTracker = &tracker 79 } 80 return f.lazyLineColumnTracker 81 } 82 83 type EntryPoint struct { 84 // This may be an absolute path or a relative path. If absolute, it will 85 // eventually be turned into a relative path by computing the path relative 86 // to the "outbase" directory. Then this relative path will be joined onto 87 // the "outdir" directory to form the final output path for this entry point. 88 OutputPath string 89 90 // This is the source index of the entry point. This file must have a valid 91 // entry point kind (i.e. not "none"). 92 SourceIndex uint32 93 94 // Manually specified output paths are ignored when computing the default 95 // "outbase" directory, which is computed as the lowest common ancestor of 96 // all automatically generated output paths. 97 OutputPathWasAutoGenerated bool 98 } 99 100 type LinkerGraph struct { 101 Files []LinkerFile 102 entryPoints []EntryPoint 103 Symbols ast.SymbolMap 104 105 // This is for cross-module inlining of TypeScript enum constants 106 TSEnums map[ast.Ref]map[string]js_ast.TSEnumValue 107 108 // This is for cross-module inlining of detected inlinable constants 109 ConstValues map[ast.Ref]js_ast.ConstValue 110 111 // We should avoid traversing all files in the bundle, because the linker 112 // should be able to run a linking operation on a large bundle where only 113 // a few files are needed (e.g. an incremental compilation scenario). This 114 // holds all files that could possibly be reached through the entry points. 115 // If you need to iterate over all files in the linking operation, iterate 116 // over this array. This array is also sorted in a deterministic ordering 117 // to help ensure deterministic builds (source indices are random). 118 ReachableFiles []uint32 119 120 // This maps from unstable source index to stable reachable file index. This 121 // is useful as a deterministic key for sorting if you need to sort something 122 // containing a source index (such as "ast.Ref" symbol references). 123 StableSourceIndices []uint32 124 } 125 126 func CloneLinkerGraph( 127 inputFiles []InputFile, 128 reachableFiles []uint32, 129 originalEntryPoints []EntryPoint, 130 codeSplitting bool, 131 ) LinkerGraph { 132 entryPoints := append([]EntryPoint{}, originalEntryPoints...) 133 symbols := ast.NewSymbolMap(len(inputFiles)) 134 files := make([]LinkerFile, len(inputFiles)) 135 136 // Mark all entry points so we don't add them again for import() expressions 137 for _, entryPoint := range entryPoints { 138 files[entryPoint.SourceIndex].entryPointKind = entryPointUserSpecified 139 } 140 141 // Clone various things since we may mutate them later. Do this in parallel 142 // for a speedup (around ~2x faster for this function in the three.js 143 // benchmark on a 6-core laptop). 144 var dynamicImportEntryPoints []uint32 145 var dynamicImportEntryPointsMutex sync.Mutex 146 waitGroup := sync.WaitGroup{} 147 waitGroup.Add(len(reachableFiles)) 148 stableSourceIndices := make([]uint32, len(inputFiles)) 149 for stableIndex, sourceIndex := range reachableFiles { 150 // Create a way to convert source indices to a stable ordering 151 stableSourceIndices[sourceIndex] = uint32(stableIndex) 152 153 go func(sourceIndex uint32) { 154 file := &files[sourceIndex] 155 file.InputFile = inputFiles[sourceIndex] 156 157 switch repr := file.InputFile.Repr.(type) { 158 case *JSRepr: 159 // Clone the representation 160 { 161 clone := *repr 162 repr = &clone 163 file.InputFile.Repr = repr 164 } 165 166 // Clone the symbol map 167 fileSymbols := append([]ast.Symbol{}, repr.AST.Symbols...) 168 symbols.SymbolsForSource[sourceIndex] = fileSymbols 169 repr.AST.Symbols = nil 170 171 // Clone the parts 172 repr.AST.Parts = append([]js_ast.Part{}, repr.AST.Parts...) 173 for i := range repr.AST.Parts { 174 part := &repr.AST.Parts[i] 175 clone := make(map[ast.Ref]js_ast.SymbolUse, len(part.SymbolUses)) 176 for ref, uses := range part.SymbolUses { 177 clone[ref] = uses 178 } 179 part.SymbolUses = clone 180 } 181 182 // Clone the import records 183 repr.AST.ImportRecords = append([]ast.ImportRecord{}, repr.AST.ImportRecords...) 184 185 // Add dynamic imports as additional entry points if code splitting is active 186 if codeSplitting { 187 for importRecordIndex := range repr.AST.ImportRecords { 188 if record := &repr.AST.ImportRecords[importRecordIndex]; record.SourceIndex.IsValid() && record.Kind == ast.ImportDynamic { 189 dynamicImportEntryPointsMutex.Lock() 190 dynamicImportEntryPoints = append(dynamicImportEntryPoints, record.SourceIndex.GetIndex()) 191 dynamicImportEntryPointsMutex.Unlock() 192 193 // Remove import assertions for dynamic imports of additional 194 // entry points so that they don't mess with the run-time behavior. 195 // For example, "import('./foo.json', { assert: { type: 'json' } })" 196 // will likely be converted into an import of a JavaScript file and 197 // leaving the import assertion there will prevent it from working. 198 record.AssertOrWith = nil 199 } 200 } 201 } 202 203 // Clone the import map 204 namedImports := make(map[ast.Ref]js_ast.NamedImport, len(repr.AST.NamedImports)) 205 for k, v := range repr.AST.NamedImports { 206 namedImports[k] = v 207 } 208 repr.AST.NamedImports = namedImports 209 210 // Clone the export map 211 resolvedExports := make(map[string]ExportData) 212 for alias, name := range repr.AST.NamedExports { 213 resolvedExports[alias] = ExportData{ 214 Ref: name.Ref, 215 SourceIndex: sourceIndex, 216 NameLoc: name.AliasLoc, 217 } 218 } 219 220 // Clone the top-level scope so we can generate more variables 221 { 222 new := &js_ast.Scope{} 223 *new = *repr.AST.ModuleScope 224 new.Generated = append([]ast.Ref{}, new.Generated...) 225 repr.AST.ModuleScope = new 226 } 227 228 // Also associate some default metadata with the file 229 repr.Meta.ResolvedExports = resolvedExports 230 repr.Meta.IsProbablyTypeScriptType = make(map[ast.Ref]bool) 231 repr.Meta.ImportsToBind = make(map[ast.Ref]ImportData) 232 233 case *CSSRepr: 234 // Clone the representation 235 { 236 clone := *repr 237 repr = &clone 238 file.InputFile.Repr = repr 239 } 240 241 // Clone the symbol map 242 fileSymbols := append([]ast.Symbol{}, repr.AST.Symbols...) 243 symbols.SymbolsForSource[sourceIndex] = fileSymbols 244 repr.AST.Symbols = nil 245 246 // Clone the import records 247 repr.AST.ImportRecords = append([]ast.ImportRecord{}, repr.AST.ImportRecords...) 248 } 249 250 // All files start off as far as possible from an entry point 251 file.DistanceFromEntryPoint = ^uint32(0) 252 waitGroup.Done() 253 }(sourceIndex) 254 } 255 waitGroup.Wait() 256 257 // Process dynamic entry points after merging control flow again 258 stableEntryPoints := make([]int, 0, len(dynamicImportEntryPoints)) 259 for _, sourceIndex := range dynamicImportEntryPoints { 260 if otherFile := &files[sourceIndex]; otherFile.entryPointKind == entryPointNone { 261 stableEntryPoints = append(stableEntryPoints, int(stableSourceIndices[sourceIndex])) 262 otherFile.entryPointKind = entryPointDynamicImport 263 } 264 } 265 266 // Make sure to add dynamic entry points in a deterministic order 267 sort.Ints(stableEntryPoints) 268 for _, stableIndex := range stableEntryPoints { 269 entryPoints = append(entryPoints, EntryPoint{SourceIndex: reachableFiles[stableIndex]}) 270 } 271 272 // Do a final quick pass over all files 273 var tsEnums map[ast.Ref]map[string]js_ast.TSEnumValue 274 var constValues map[ast.Ref]js_ast.ConstValue 275 bitCount := uint(len(entryPoints)) 276 for _, sourceIndex := range reachableFiles { 277 file := &files[sourceIndex] 278 279 // Allocate the entry bit set now that the number of entry points is known 280 file.EntryBits = helpers.NewBitSet(bitCount) 281 282 // Merge TypeScript enums together into one big map. There likely aren't 283 // too many enum definitions relative to the overall size of the code so 284 // it should be fine to just merge them together in serial. 285 if repr, ok := file.InputFile.Repr.(*JSRepr); ok && repr.AST.TSEnums != nil { 286 if tsEnums == nil { 287 tsEnums = make(map[ast.Ref]map[string]js_ast.TSEnumValue) 288 } 289 for ref, enum := range repr.AST.TSEnums { 290 tsEnums[ref] = enum 291 } 292 } 293 294 // Also merge const values into one big map as well 295 if repr, ok := file.InputFile.Repr.(*JSRepr); ok && repr.AST.ConstValues != nil { 296 if constValues == nil { 297 constValues = make(map[ast.Ref]js_ast.ConstValue) 298 } 299 for ref, value := range repr.AST.ConstValues { 300 constValues[ref] = value 301 } 302 } 303 } 304 305 return LinkerGraph{ 306 Symbols: symbols, 307 TSEnums: tsEnums, 308 ConstValues: constValues, 309 entryPoints: entryPoints, 310 Files: files, 311 ReachableFiles: reachableFiles, 312 StableSourceIndices: stableSourceIndices, 313 } 314 } 315 316 // Prevent packages that depend on us from adding or removing entry points 317 func (g *LinkerGraph) EntryPoints() []EntryPoint { 318 return g.entryPoints 319 } 320 321 func (g *LinkerGraph) AddPartToFile(sourceIndex uint32, part js_ast.Part) uint32 { 322 // Invariant: this map is never null 323 if part.SymbolUses == nil { 324 part.SymbolUses = make(map[ast.Ref]js_ast.SymbolUse) 325 } 326 327 repr := g.Files[sourceIndex].InputFile.Repr.(*JSRepr) 328 partIndex := uint32(len(repr.AST.Parts)) 329 repr.AST.Parts = append(repr.AST.Parts, part) 330 331 // Invariant: the parts for all top-level symbols can be found in the file-level map 332 for _, declaredSymbol := range part.DeclaredSymbols { 333 if declaredSymbol.IsTopLevel { 334 // Check for an existing overlay 335 partIndices, ok := repr.Meta.TopLevelSymbolToPartsOverlay[declaredSymbol.Ref] 336 337 // If missing, initialize using the original values from the parser 338 if !ok { 339 partIndices = append(partIndices, repr.AST.TopLevelSymbolToPartsFromParser[declaredSymbol.Ref]...) 340 } 341 342 // Add this part to the overlay 343 partIndices = append(partIndices, partIndex) 344 if repr.Meta.TopLevelSymbolToPartsOverlay == nil { 345 repr.Meta.TopLevelSymbolToPartsOverlay = make(map[ast.Ref][]uint32) 346 } 347 repr.Meta.TopLevelSymbolToPartsOverlay[declaredSymbol.Ref] = partIndices 348 } 349 } 350 351 return partIndex 352 } 353 354 func (g *LinkerGraph) GenerateNewSymbol(sourceIndex uint32, kind ast.SymbolKind, originalName string) ast.Ref { 355 sourceSymbols := &g.Symbols.SymbolsForSource[sourceIndex] 356 357 ref := ast.Ref{ 358 SourceIndex: sourceIndex, 359 InnerIndex: uint32(len(*sourceSymbols)), 360 } 361 362 *sourceSymbols = append(*sourceSymbols, ast.Symbol{ 363 Kind: kind, 364 OriginalName: originalName, 365 Link: ast.InvalidRef, 366 }) 367 368 generated := &g.Files[sourceIndex].InputFile.Repr.(*JSRepr).AST.ModuleScope.Generated 369 *generated = append(*generated, ref) 370 return ref 371 } 372 373 func (g *LinkerGraph) GenerateSymbolImportAndUse( 374 sourceIndex uint32, 375 partIndex uint32, 376 ref ast.Ref, 377 useCount uint32, 378 sourceIndexToImportFrom uint32, 379 ) { 380 if useCount == 0 { 381 return 382 } 383 384 repr := g.Files[sourceIndex].InputFile.Repr.(*JSRepr) 385 part := &repr.AST.Parts[partIndex] 386 387 // Mark this symbol as used by this part 388 use := part.SymbolUses[ref] 389 use.CountEstimate += useCount 390 part.SymbolUses[ref] = use 391 392 // Uphold invariants about the CommonJS "exports" and "module" symbols 393 if ref == repr.AST.ExportsRef { 394 repr.AST.UsesExportsRef = true 395 } 396 if ref == repr.AST.ModuleRef { 397 repr.AST.UsesModuleRef = true 398 } 399 400 // Track that this specific symbol was imported 401 if sourceIndexToImportFrom != sourceIndex { 402 repr.Meta.ImportsToBind[ref] = ImportData{ 403 SourceIndex: sourceIndexToImportFrom, 404 Ref: ref, 405 } 406 } 407 408 // Pull in all parts that declare this symbol 409 targetRepr := g.Files[sourceIndexToImportFrom].InputFile.Repr.(*JSRepr) 410 for _, partIndex := range targetRepr.TopLevelSymbolToParts(ref) { 411 part.Dependencies = append(part.Dependencies, js_ast.Dependency{ 412 SourceIndex: sourceIndexToImportFrom, 413 PartIndex: partIndex, 414 }) 415 } 416 } 417 418 func (g *LinkerGraph) GenerateRuntimeSymbolImportAndUse( 419 sourceIndex uint32, 420 partIndex uint32, 421 name string, 422 useCount uint32, 423 ) { 424 if useCount == 0 { 425 return 426 } 427 428 runtimeRepr := g.Files[runtime.SourceIndex].InputFile.Repr.(*JSRepr) 429 ref := runtimeRepr.AST.NamedExports[name].Ref 430 g.GenerateSymbolImportAndUse(sourceIndex, partIndex, ref, useCount, runtime.SourceIndex) 431 }