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  }