1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431
|
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 {
// 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
InputFile InputFile
// 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 ast.SymbolMap
// This is for cross-module inlining of TypeScript enum constants
TSEnums map[ast.Ref]map[string]js_ast.TSEnumValue
// This is for cross-module inlining of detected inlinable constants
ConstValues map[ast.Ref]js_ast.ConstValue
// 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 "ast.Ref" symbol references).
StableSourceIndices []uint32
}
func CloneLinkerGraph(
inputFiles []InputFile,
reachableFiles []uint32,
originalEntryPoints []EntryPoint,
codeSplitting bool,
) LinkerGraph {
entryPoints := append([]EntryPoint{}, originalEntryPoints...)
symbols := 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([]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[ast.Ref]js_ast.SymbolUse, len(part.SymbolUses))
for ref, uses := range part.SymbolUses {
clone[ref] = uses
}
part.SymbolUses = clone
}
// 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()
// Remove import assertions for dynamic imports of additional
// entry points so that they don't mess with the run-time behavior.
// For example, "import('./foo.json', { assert: { type: 'json' } })"
// will likely be converted into an import of a JavaScript file and
// leaving the import assertion there will prevent it from working.
record.AssertOrWith = nil
}
}
}
// Clone the import map
namedImports := make(map[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([]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[ast.Ref]bool)
repr.Meta.ImportsToBind = make(map[ast.Ref]ImportData)
case *CSSRepr:
// Clone the representation
{
clone := *repr
repr = &clone
file.InputFile.Repr = repr
}
// Clone the symbol map
fileSymbols := append([]ast.Symbol{}, repr.AST.Symbols...)
symbols.SymbolsForSource[sourceIndex] = fileSymbols
repr.AST.Symbols = nil
// 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]})
}
// Do a final quick pass over all files
var tsEnums map[ast.Ref]map[string]js_ast.TSEnumValue
var constValues map[ast.Ref]js_ast.ConstValue
bitCount := uint(len(entryPoints))
for _, sourceIndex := range reachableFiles {
file := &files[sourceIndex]
// Allocate the entry bit set now that the number of entry points is known
file.EntryBits = helpers.NewBitSet(bitCount)
// Merge TypeScript enums together into one big map. There likely aren't
// too many enum definitions relative to the overall size of the code so
// it should be fine to just merge them together in serial.
if repr, ok := file.InputFile.Repr.(*JSRepr); ok && repr.AST.TSEnums != nil {
if tsEnums == nil {
tsEnums = make(map[ast.Ref]map[string]js_ast.TSEnumValue)
}
for ref, enum := range repr.AST.TSEnums {
tsEnums[ref] = enum
}
}
// Also merge const values into one big map as well
if repr, ok := file.InputFile.Repr.(*JSRepr); ok && repr.AST.ConstValues != nil {
if constValues == nil {
constValues = make(map[ast.Ref]js_ast.ConstValue)
}
for ref, value := range repr.AST.ConstValues {
constValues[ref] = value
}
}
}
return LinkerGraph{
Symbols: symbols,
TSEnums: tsEnums,
ConstValues: constValues,
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[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[ast.Ref][]uint32)
}
repr.Meta.TopLevelSymbolToPartsOverlay[declaredSymbol.Ref] = partIndices
}
}
return partIndex
}
func (g *LinkerGraph) GenerateNewSymbol(sourceIndex uint32, kind ast.SymbolKind, originalName string) ast.Ref {
sourceSymbols := &g.Symbols.SymbolsForSource[sourceIndex]
ref := ast.Ref{
SourceIndex: sourceIndex,
InnerIndex: uint32(len(*sourceSymbols)),
}
*sourceSymbols = append(*sourceSymbols, ast.Symbol{
Kind: kind,
OriginalName: originalName,
Link: 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 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)
}
|