package bundler import ( "bytes" "encoding/base64" "encoding/binary" "fmt" "hash" "math/rand" "sort" "strings" "sync" "time" "github.com/evanw/esbuild/internal/ast" "github.com/evanw/esbuild/internal/compat" "github.com/evanw/esbuild/internal/config" "github.com/evanw/esbuild/internal/css_ast" "github.com/evanw/esbuild/internal/css_printer" "github.com/evanw/esbuild/internal/fs" "github.com/evanw/esbuild/internal/graph" "github.com/evanw/esbuild/internal/helpers" "github.com/evanw/esbuild/internal/js_ast" "github.com/evanw/esbuild/internal/js_lexer" "github.com/evanw/esbuild/internal/js_printer" "github.com/evanw/esbuild/internal/logger" "github.com/evanw/esbuild/internal/renamer" "github.com/evanw/esbuild/internal/resolver" "github.com/evanw/esbuild/internal/runtime" "github.com/evanw/esbuild/internal/sourcemap" "github.com/evanw/esbuild/internal/xxhash" ) type linkerContext struct { options *config.Options log logger.Log fs fs.FS res resolver.Resolver graph graph.LinkerGraph // This helps avoid an infinite loop when matching imports to exports cycleDetector []importTracker // We may need to refer to the CommonJS "module" symbol for exports unboundModuleRef js_ast.Ref // This represents the parallel computation of source map related data. // Calling this will block until the computation is done. The resulting value // is shared between threads and must be treated as immutable. dataForSourceMaps func() []dataForSourceMap // The unique key prefix is a random string that is unique to every linking // operation. It is used as a prefix for the unique keys assigned to every // chunk. These unique keys are used to identify each chunk before the final // output paths have been computed. uniqueKeyPrefix string uniqueKeyPrefixBytes []byte // This is just "uniqueKeyPrefix" in byte form } type partRange struct { sourceIndex uint32 partIndexBegin uint32 partIndexEnd uint32 } type chunkInfo struct { // This is a random string and is used to represent the output path of this // chunk before the final output path has been computed. uniqueKey string filesWithPartsInChunk map[uint32]bool filesInChunkInOrder []uint32 partsInChunkInOrder []partRange entryBits helpers.BitSet // This information is only useful if "isEntryPoint" is true isEntryPoint bool sourceIndex uint32 // An index into "c.sources" entryPointBit uint // An index into "c.graph.EntryPoints" // For code splitting crossChunkImports []chunkImport // This is the representation-specific information chunkRepr chunkRepr // This is the final path of this chunk relative to the output directory, but // without the substitution of the final hash (since it hasn't been computed). finalTemplate []config.PathTemplate // This is the final path of this chunk relative to the output directory. It // is the substitution of the final hash into "finalTemplate". finalRelPath string // When this chunk is initially generated in isolation, the output pieces // will contain slices of the output with the unique keys of other chunks // omitted. outputPieces []outputPiece // This contains the hash for just this chunk without including information // from the hashes of other chunks. Later on in the linking process, the // final hash for this chunk will be constructed by merging the isolated // hashes of all transitive dependencies of this chunk. This is separated // into two phases like this to handle cycles in the chunk import graph. waitForIsolatedHash func() []byte // Other fields relating to the output file for this chunk jsonMetadataChunkCallback func(finalOutputSize int) []byte outputSourceMap sourcemap.SourceMapPieces isExecutable bool } type chunkImport struct { chunkIndex uint32 importKind ast.ImportKind } // This is a chunk of source code followed by a reference to another chunk. For // example, the file "@import 'CHUNK0001'; body { color: black; }" would be // represented by two pieces, one with the data "@import '" and another with the // data "'; body { color: black; }". The first would have the chunk index 1 and // the second would have an invalid chunk index. type outputPiece struct { data []byte // Note: This may be invalid. For example, the chunk may not contain any // imports, in which case there is one piece with data and no chunk index. chunkIndex ast.Index32 } type chunkRepr interface{ isChunk() } func (*chunkReprJS) isChunk() {} func (*chunkReprCSS) isChunk() {} type chunkReprJS struct { // For code splitting crossChunkPrefixStmts []js_ast.Stmt crossChunkSuffixStmts []js_ast.Stmt exportsToOtherChunks map[js_ast.Ref]string importsFromOtherChunks map[uint32]crossChunkImportItemArray } type chunkReprCSS struct { } // Returns a log where "log.HasErrors()" only returns true if any errors have // been logged since this call. This is useful when there have already been // errors logged by other linkers that share the same log. func wrappedLog(log logger.Log) logger.Log { var mutex sync.Mutex var hasErrors bool addMsg := log.AddMsg log.AddMsg = func(msg logger.Msg) { if msg.Kind == logger.Error { mutex.Lock() defer mutex.Unlock() hasErrors = true } addMsg(msg) } log.HasErrors = func() bool { mutex.Lock() defer mutex.Unlock() return hasErrors } return log } func newLinkerContext( options *config.Options, log logger.Log, fs fs.FS, res resolver.Resolver, inputFiles []graph.InputFile, entryPoints []graph.EntryPoint, reachableFiles []uint32, dataForSourceMaps func() []dataForSourceMap, ) linkerContext { log = wrappedLog(log) c := linkerContext{ options: options, log: log, fs: fs, res: res, dataForSourceMaps: dataForSourceMaps, graph: graph.MakeLinkerGraph( inputFiles, reachableFiles, entryPoints, options.CodeSplitting, ), } for _, entryPoint := range entryPoints { if repr, ok := c.graph.Files[entryPoint.SourceIndex].InputFile.Repr.(*graph.JSRepr); ok { // Loaders default to CommonJS when they are the entry point and the output // format is not ESM-compatible since that avoids generating the ESM-to-CJS // machinery. if repr.AST.HasLazyExport && (c.options.Mode == config.ModePassThrough || (c.options.Mode == config.ModeConvertFormat && !c.options.OutputFormat.KeepES6ImportExportSyntax())) { repr.AST.ExportsKind = js_ast.ExportsCommonJS } // Entry points with ES6 exports must generate an exports object when // targeting non-ES6 formats. Note that the IIFE format only needs this // when the global name is present, since that's the only way the exports // can actually be observed externally. if repr.AST.ExportKeyword.Len > 0 && (options.OutputFormat == config.FormatCommonJS || (options.OutputFormat == config.FormatIIFE && len(options.GlobalName) > 0)) { repr.AST.UsesExportsRef = true repr.Meta.ForceIncludeExportsForEntryPoint = true } } } // Allocate a new unbound symbol called "module" in case we need it later if c.options.OutputFormat == config.FormatCommonJS { c.unboundModuleRef = c.graph.GenerateNewSymbol(runtime.SourceIndex, js_ast.SymbolUnbound, "module") } else { c.unboundModuleRef = js_ast.InvalidRef } return c } func (c *linkerContext) generateUniqueKeyPrefix() bool { var data [12]byte rand.Seed(time.Now().UnixNano()) if _, err := rand.Read(data[:]); err != nil { c.log.AddError(nil, logger.Loc{}, fmt.Sprintf("Failed to read from randomness source: %s", err.Error())) return false } // This is 16 bytes and shouldn't generate escape characters when put into strings c.uniqueKeyPrefix = base64.URLEncoding.EncodeToString(data[:]) c.uniqueKeyPrefixBytes = []byte(c.uniqueKeyPrefix) return true } func (c *linkerContext) link() []graph.OutputFile { if !c.generateUniqueKeyPrefix() { return nil } c.scanImportsAndExports() // Stop now if there were errors if c.log.HasErrors() { return []graph.OutputFile{} } c.treeShakingAndCodeSplitting() if c.options.Mode == config.ModePassThrough { for _, entryPoint := range c.graph.EntryPoints() { c.preventExportsFromBeingRenamed(entryPoint.SourceIndex) } } chunks := c.computeChunks() c.computeCrossChunkDependencies(chunks) // Make sure calls to "js_ast.FollowSymbols()" in parallel goroutines after this // won't hit concurrent map mutation hazards js_ast.FollowAllSymbols(c.graph.Symbols) return c.generateChunksInParallel(chunks) } // Currently the automatic chunk generation algorithm should by construction // never generate chunks that import each other since files are allocated to // chunks based on which entry points they are reachable from. // // This will change in the future when we allow manual chunk labels. But before // we allow manual chunk labels, we'll need to rework module initialization to // allow code splitting chunks to be lazily-initialized. // // Since that work hasn't been finished yet, cycles in the chunk import graph // can cause initialization bugs. So let's forbid these cycles for now to guard // against code splitting bugs that could cause us to generate buggy chunks. func (c *linkerContext) enforceNoCyclicChunkImports(chunks []chunkInfo) { var validate func(int, []int) validate = func(chunkIndex int, path []int) { for _, otherChunkIndex := range path { if chunkIndex == otherChunkIndex { c.log.AddError(nil, logger.Loc{}, "Internal error: generated chunks contain a circular import") return } } path = append(path, chunkIndex) for _, chunkImport := range chunks[chunkIndex].crossChunkImports { // Ignore cycles caused by dynamic "import()" expressions. These are fine // because they don't necessarily cause initialization order issues and // they don't indicate a bug in our chunk generation algorithm. They arise // normally in real code (e.g. two files that import each other). if chunkImport.importKind != ast.ImportDynamic { validate(int(chunkImport.chunkIndex), path) } } } path := make([]int, 0, len(chunks)) for i := range chunks { validate(i, path) } } func (c *linkerContext) generateChunksInParallel(chunks []chunkInfo) []graph.OutputFile { // Generate each chunk on a separate goroutine generateWaitGroup := sync.WaitGroup{} generateWaitGroup.Add(len(chunks)) for chunkIndex := range chunks { switch chunks[chunkIndex].chunkRepr.(type) { case *chunkReprJS: go c.generateChunkJS(chunks, chunkIndex, &generateWaitGroup) case *chunkReprCSS: go c.generateChunkCSS(chunks, chunkIndex, &generateWaitGroup) } } c.enforceNoCyclicChunkImports(chunks) generateWaitGroup.Wait() // Compute the final hashes of each chunk. This can technically be done in // parallel but it probably doesn't matter so much because we're not hashing // that much data. visited := make([]uint32, len(chunks)) var finalBytes []byte for chunkIndex := range chunks { chunk := &chunks[chunkIndex] var hashSubstitution *string // Only wait for the hash if necessary if config.HasPlaceholder(chunk.finalTemplate, config.HashPlaceholder) { // Compute the final hash using the isolated hashes of the dependencies hash := xxhash.New() appendIsolatedHashesForImportedChunks(hash, chunks, uint32(chunkIndex), visited, ^uint32(chunkIndex)) finalBytes = hash.Sum(finalBytes[:0]) finalString := hashForFileName(finalBytes) hashSubstitution = &finalString } // Render the last remaining placeholder in the template chunk.finalRelPath = config.TemplateToString(config.SubstituteTemplate(chunk.finalTemplate, config.PathPlaceholders{ Hash: hashSubstitution, })) } // Generate the final output files by joining file pieces together var resultsWaitGroup sync.WaitGroup results := make([][]graph.OutputFile, len(chunks)) resultsWaitGroup.Add(len(chunks)) for chunkIndex, chunk := range chunks { go func(chunkIndex int, chunk chunkInfo) { var outputFiles []graph.OutputFile // Each file may optionally contain additional files to be copied to the // output directory. This is used by the "file" loader. for _, sourceIndex := range chunk.filesInChunkInOrder { outputFiles = append(outputFiles, c.graph.Files[sourceIndex].InputFile.AdditionalFiles...) } // Path substitution for the chunk itself finalRelDir := c.fs.Dir(chunk.finalRelPath) outputContentsJoiner, outputSourceMapShifts := c.substituteFinalPaths(chunks, chunk.outputPieces, func(finalRelPathForImport string) string { return c.pathBetweenChunks(finalRelDir, finalRelPathForImport) }) // Generate the optional source map for this chunk if c.options.SourceMap != config.SourceMapNone && chunk.outputSourceMap.Suffix != nil { outputSourceMap := chunk.outputSourceMap.Finalize(outputSourceMapShifts) finalRelPathForSourceMap := chunk.finalRelPath + ".map" // Potentially write a trailing source map comment switch c.options.SourceMap { case config.SourceMapLinkedWithComment: importPath := c.pathBetweenChunks(finalRelDir, finalRelPathForSourceMap) importPath = strings.TrimPrefix(importPath, "./") outputContentsJoiner.EnsureNewlineAtEnd() outputContentsJoiner.AddString("//# sourceMappingURL=") outputContentsJoiner.AddString(importPath) outputContentsJoiner.AddString("\n") case config.SourceMapInline, config.SourceMapInlineAndExternal: outputContentsJoiner.EnsureNewlineAtEnd() outputContentsJoiner.AddString("//# sourceMappingURL=data:application/json;base64,") outputContentsJoiner.AddString(base64.StdEncoding.EncodeToString(outputSourceMap)) outputContentsJoiner.AddString("\n") } // Potentially write the external source map file switch c.options.SourceMap { case config.SourceMapLinkedWithComment, config.SourceMapInlineAndExternal, config.SourceMapExternalWithoutComment: outputFiles = append(outputFiles, graph.OutputFile{ AbsPath: c.fs.Join(c.options.AbsOutputDir, finalRelPathForSourceMap), Contents: outputSourceMap, JSONMetadataChunk: fmt.Sprintf( "{\n \"imports\": [],\n \"exports\": [],\n \"inputs\": {},\n \"bytes\": %d\n }", len(outputSourceMap)), }) } } // Finalize the output contents outputContents := outputContentsJoiner.Done() // Path substitution for the JSON metadata var jsonMetadataChunk string if c.options.NeedsMetafile { jsonMetadataChunkPieces := c.breakOutputIntoPieces(chunk.jsonMetadataChunkCallback(len(outputContents)), uint32(len(chunks))) jsonMetadataChunkBytes, _ := c.substituteFinalPaths(chunks, jsonMetadataChunkPieces, func(finalRelPathForImport string) string { return c.res.PrettyPath(logger.Path{Text: c.fs.Join(c.options.AbsOutputDir, finalRelPathForImport), Namespace: "file"}) }) jsonMetadataChunk = string(jsonMetadataChunkBytes.Done()) } // Generate the output file for this chunk outputFiles = append(outputFiles, graph.OutputFile{ AbsPath: c.fs.Join(c.options.AbsOutputDir, chunk.finalRelPath), Contents: outputContents, JSONMetadataChunk: jsonMetadataChunk, IsExecutable: chunk.isExecutable, }) results[chunkIndex] = outputFiles resultsWaitGroup.Done() }(chunkIndex, chunk) } resultsWaitGroup.Wait() // Merge the output files from the different goroutines together in order outputFilesLen := 0 for _, result := range results { outputFilesLen += len(result) } outputFiles := make([]graph.OutputFile, 0, outputFilesLen) for _, result := range results { outputFiles = append(outputFiles, result...) } return outputFiles } // Given a set of output pieces (i.e. a buffer already divided into the spans // between import paths), substitute the final import paths in and then join // everything into a single byte buffer. func (c *linkerContext) substituteFinalPaths( chunks []chunkInfo, pieces []outputPiece, modifyPath func(string) string, ) (j helpers.Joiner, shifts []sourcemap.SourceMapShift) { var shift sourcemap.SourceMapShift shifts = make([]sourcemap.SourceMapShift, 0, len(pieces)) shifts = append(shifts, shift) for _, piece := range pieces { var dataOffset sourcemap.LineColumnOffset j.AddBytes(piece.data) dataOffset.AdvanceBytes(piece.data) shift.Before.Add(dataOffset) shift.After.Add(dataOffset) if piece.chunkIndex.IsValid() { chunk := chunks[piece.chunkIndex.GetIndex()] importPath := modifyPath(chunk.finalRelPath) j.AddString(importPath) shift.Before.AdvanceString(chunk.uniqueKey) shift.After.AdvanceString(importPath) shifts = append(shifts, shift) } } return } func (c *linkerContext) pathBetweenChunks(fromRelDir string, toRelPath string) string { // Join with the public path if it has been configured if c.options.PublicPath != "" { return joinWithPublicPath(c.options.PublicPath, toRelPath) } // Otherwise, return a relative path relPath, ok := c.fs.Rel(fromRelDir, toRelPath) if !ok { c.log.AddError(nil, logger.Loc{}, fmt.Sprintf("Cannot traverse from directory %q to chunk %q", fromRelDir, toRelPath)) return "" } // Make sure to always use forward slashes, even on Windows relPath = strings.ReplaceAll(relPath, "\\", "/") // Make sure the relative path doesn't start with a name, since that could // be interpreted as a package path instead of a relative path if !strings.HasPrefix(relPath, "./") && !strings.HasPrefix(relPath, "../") { relPath = "./" + relPath } return relPath } // Returns the path of this file relative to "outbase", which is then ready to // be joined with the absolute output directory path. The directory and name // components are returned separately for convenience. // // This makes sure to have the directory end in a slash so that it can be // substituted into a path template without necessarily having a "/" after it. // Extra slashes should get cleaned up automatically when we join it with the // output directory. func (c *linkerContext) pathRelativeToOutbase( sourceIndex uint32, entryPointBit uint, stdExt string, avoidIndex bool, ) (relDir string, baseName string, baseExt string) { file := &c.graph.Files[sourceIndex] relDir = "/" baseExt = stdExt // If the output path was configured explicitly, use it verbatim if c.options.AbsOutputFile != "" { baseName = c.fs.Base(c.options.AbsOutputFile) // Strip off the extension ext := c.fs.Ext(baseName) baseName = baseName[:len(baseName)-len(ext)] // Use the extension from the explicit output file path. However, don't do // that if this is a CSS chunk but the entry point file is not CSS. In that // case use the standard extension. This happens when importing CSS into JS. if _, ok := file.InputFile.Repr.(*graph.CSSRepr); ok || stdExt != c.options.OutputExtensionCSS { baseExt = ext } return } absPath := file.InputFile.Source.KeyPath.Text isCustomOutputPath := false if outPath := c.graph.EntryPoints()[entryPointBit].OutputPath; outPath != "" { // Use the configured output path if present absPath = outPath if !c.fs.IsAbs(absPath) { absPath = c.fs.Join(c.options.AbsOutputBase, absPath) } isCustomOutputPath = true } else if file.InputFile.Source.KeyPath.Namespace != "file" { // Come up with a path for virtual paths (i.e. non-file-system paths) dir, base, _ := logger.PlatformIndependentPathDirBaseExt(absPath) if avoidIndex && base == "index" { _, base, _ = logger.PlatformIndependentPathDirBaseExt(dir) } baseName = sanitizeFilePathForVirtualModulePath(base) return } else { // Heuristic: If the file is named something like "index.js", then use // the name of the parent directory instead. This helps avoid the // situation where many chunks are named "index" because of people // dynamically-importing npm packages that make use of node's implicit // "index" file name feature. if avoidIndex { base := c.fs.Base(absPath) base = base[:len(base)-len(c.fs.Ext(base))] if base == "index" { absPath = c.fs.Dir(absPath) } } } // Try to get a relative path to the base directory relPath, ok := c.fs.Rel(c.options.AbsOutputBase, absPath) if !ok { // This can fail in some situations such as on different drives on // Windows. In that case we just use the file name. baseName = c.fs.Base(absPath) } else { // Now we finally have a relative path relDir = c.fs.Dir(relPath) + "/" baseName = c.fs.Base(relPath) // Use platform-independent slashes relDir = strings.ReplaceAll(relDir, "\\", "/") // Replace leading "../" so we don't try to write outside of the output // directory. This normally can't happen because "AbsOutputBase" is // automatically computed to contain all entry point files, but it can // happen if someone sets it manually via the "outbase" API option. // // Note that we can't just strip any leading "../" because that could // cause two separate entry point paths to collide. For example, there // could be both "src/index.js" and "../src/index.js" as entry points. dotDotCount := 0 for strings.HasPrefix(relDir[dotDotCount*3:], "../") { dotDotCount++ } if dotDotCount > 0 { // The use of "_.._" here is somewhat arbitrary but it is unlikely to // collide with a folder named by a human and it works on Windows // (Windows doesn't like names that end with a "."). And not starting // with a "." means that it will not be hidden on Unix. relDir = strings.Repeat("_.._/", dotDotCount) + relDir[dotDotCount*3:] } relDir = "/" + relDir } // Strip the file extension if the output path is an input file if !isCustomOutputPath { ext := c.fs.Ext(baseName) baseName = baseName[:len(baseName)-len(ext)] } return } func (c *linkerContext) computeCrossChunkDependencies(chunks []chunkInfo) { jsChunks := 0 for _, chunk := range chunks { if _, ok := chunk.chunkRepr.(*chunkReprJS); ok { jsChunks++ } } if jsChunks < 2 { // No need to compute cross-chunk dependencies if there can't be any return } type chunkMeta struct { imports map[js_ast.Ref]bool exports map[js_ast.Ref]bool dynamicImports map[int]bool } chunkMetas := make([]chunkMeta, len(chunks)) // For each chunk, see what symbols it uses from other chunks. Do this in // parallel because it's the most expensive part of this function. waitGroup := sync.WaitGroup{} waitGroup.Add(len(chunks)) for chunkIndex, chunk := range chunks { go func(chunkIndex int, chunk chunkInfo) { chunkMeta := &chunkMetas[chunkIndex] imports := make(map[js_ast.Ref]bool) chunkMeta.imports = imports chunkMeta.exports = make(map[js_ast.Ref]bool) // Go over each file in this chunk for sourceIndex := range chunk.filesWithPartsInChunk { // Go over each part in this file that's marked for inclusion in this chunk switch repr := c.graph.Files[sourceIndex].InputFile.Repr.(type) { case *graph.JSRepr: for partIndex, partMeta := range repr.AST.Parts { if !partMeta.IsLive { continue } part := &repr.AST.Parts[partIndex] // Rewrite external dynamic imports to point to the chunk for that entry point for _, importRecordIndex := range part.ImportRecordIndices { record := &repr.AST.ImportRecords[importRecordIndex] if record.SourceIndex.IsValid() && c.isExternalDynamicImport(record, sourceIndex) { otherChunkIndex := c.graph.Files[record.SourceIndex.GetIndex()].EntryPointChunkIndex record.Path.Text = chunks[otherChunkIndex].uniqueKey record.SourceIndex = ast.Index32{} // Track this cross-chunk dynamic import so we make sure to // include its hash when we're calculating the hashes of all // dependencies of this chunk. if int(otherChunkIndex) != chunkIndex { if chunkMeta.dynamicImports == nil { chunkMeta.dynamicImports = make(map[int]bool) } chunkMeta.dynamicImports[int(otherChunkIndex)] = true } } } // Remember what chunk each top-level symbol is declared in. Symbols // with multiple declarations such as repeated "var" statements with // the same name should already be marked as all being in a single // chunk. In that case this will overwrite the same value below which // is fine. for _, declared := range part.DeclaredSymbols { if declared.IsTopLevel { c.graph.Symbols.Get(declared.Ref).ChunkIndex = ast.MakeIndex32(uint32(chunkIndex)) } } // Record each symbol used in this part. This will later be matched up // with our map of which chunk a given symbol is declared in to // determine if the symbol needs to be imported from another chunk. for ref := range part.SymbolUses { symbol := c.graph.Symbols.Get(ref) // Ignore unbound symbols, which don't have declarations if symbol.Kind == js_ast.SymbolUnbound { continue } // Ignore symbols that are going to be replaced by undefined if symbol.ImportItemStatus == js_ast.ImportItemMissing { continue } // If this is imported from another file, follow the import // reference and reference the symbol in that file instead if importData, ok := repr.Meta.ImportsToBind[ref]; ok { ref = importData.Ref symbol = c.graph.Symbols.Get(ref) } else if repr.Meta.Wrap == graph.WrapCJS && ref != repr.AST.WrapperRef { // The only internal symbol that wrapped CommonJS files export // is the wrapper itself. continue } // If this is an ES6 import from a CommonJS file, it will become a // property access off the namespace symbol instead of a bare // identifier. In that case we want to pull in the namespace symbol // instead. The namespace symbol stores the result of "require()". if symbol.NamespaceAlias != nil { ref = symbol.NamespaceAlias.NamespaceRef } // We must record this relationship even for symbols that are not // imports. Due to code splitting, the definition of a symbol may // be moved to a separate chunk than the use of a symbol even if // the definition and use of that symbol are originally from the // same source file. imports[ref] = true } } } } // Include the exports if this is an entry point chunk if chunk.isEntryPoint { if repr, ok := c.graph.Files[chunk.sourceIndex].InputFile.Repr.(*graph.JSRepr); ok { if repr.Meta.Wrap != graph.WrapCJS { for _, alias := range repr.Meta.SortedAndFilteredExportAliases { export := repr.Meta.ResolvedExports[alias] targetRef := export.Ref // If this is an import, then target what the import points to if importData, ok := c.graph.Files[export.SourceIndex].InputFile.Repr.(*graph.JSRepr).Meta.ImportsToBind[targetRef]; ok { targetRef = importData.Ref } imports[targetRef] = true } } // Ensure "exports" is included if the current output format needs it if repr.Meta.ForceIncludeExportsForEntryPoint { imports[repr.AST.ExportsRef] = true } // Include the wrapper if present if repr.Meta.Wrap != graph.WrapNone { imports[repr.AST.WrapperRef] = true } } } waitGroup.Done() }(chunkIndex, chunk) } waitGroup.Wait() // Mark imported symbols as exported in the chunk from which they are declared for chunkIndex := range chunks { chunk := &chunks[chunkIndex] chunkRepr, ok := chunk.chunkRepr.(*chunkReprJS) if !ok { continue } chunkMeta := chunkMetas[chunkIndex] // Find all uses in this chunk of symbols from other chunks chunkRepr.importsFromOtherChunks = make(map[uint32]crossChunkImportItemArray) for importRef := range chunkMeta.imports { // Ignore uses that aren't top-level symbols if otherChunkIndex := c.graph.Symbols.Get(importRef).ChunkIndex; otherChunkIndex.IsValid() { if otherChunkIndex := otherChunkIndex.GetIndex(); otherChunkIndex != uint32(chunkIndex) { chunkRepr.importsFromOtherChunks[otherChunkIndex] = append(chunkRepr.importsFromOtherChunks[otherChunkIndex], crossChunkImportItem{ref: importRef}) chunkMetas[otherChunkIndex].exports[importRef] = true } } } // If this is an entry point, make sure we import all chunks belonging to // this entry point, even if there are no imports. We need to make sure // these chunks are evaluated for their side effects too. if chunk.isEntryPoint { for otherChunkIndex, otherChunk := range chunks { if _, ok := otherChunk.chunkRepr.(*chunkReprJS); ok && chunkIndex != otherChunkIndex && otherChunk.entryBits.HasBit(chunk.entryPointBit) { imports := chunkRepr.importsFromOtherChunks[uint32(otherChunkIndex)] chunkRepr.importsFromOtherChunks[uint32(otherChunkIndex)] = imports } } } // Make sure we also track dynamic cross-chunk imports. These need to be // tracked so we count them as dependencies of this chunk for the purpose // of hash calculation. if chunkMeta.dynamicImports != nil { sortedDynamicImports := make([]int, 0, len(chunkMeta.dynamicImports)) for chunkIndex := range chunkMeta.dynamicImports { sortedDynamicImports = append(sortedDynamicImports, chunkIndex) } sort.Ints(sortedDynamicImports) for _, chunkIndex := range sortedDynamicImports { chunk.crossChunkImports = append(chunk.crossChunkImports, chunkImport{ importKind: ast.ImportDynamic, chunkIndex: uint32(chunkIndex), }) } } } // Generate cross-chunk exports. These must be computed before cross-chunk // imports because of export alias renaming, which must consider all export // aliases simultaneously to avoid collisions. for chunkIndex := range chunks { chunk := &chunks[chunkIndex] chunkRepr, ok := chunk.chunkRepr.(*chunkReprJS) if !ok { continue } chunkRepr.exportsToOtherChunks = make(map[js_ast.Ref]string) switch c.options.OutputFormat { case config.FormatESModule: r := renamer.ExportRenamer{} var items []js_ast.ClauseItem for _, export := range c.sortedCrossChunkExportItems(chunkMetas[chunkIndex].exports) { var alias string if c.options.MinifyIdentifiers { alias = r.NextMinifiedName() } else { alias = r.NextRenamedName(c.graph.Symbols.Get(export.Ref).OriginalName) } items = append(items, js_ast.ClauseItem{Name: js_ast.LocRef{Ref: export.Ref}, Alias: alias}) chunkRepr.exportsToOtherChunks[export.Ref] = alias } if len(items) > 0 { chunkRepr.crossChunkSuffixStmts = []js_ast.Stmt{{Data: &js_ast.SExportClause{ Items: items, }}} } default: panic("Internal error") } } // Generate cross-chunk imports. These must be computed after cross-chunk // exports because the export aliases must already be finalized so they can // be embedded in the generated import statements. for chunkIndex := range chunks { chunk := &chunks[chunkIndex] chunkRepr, ok := chunk.chunkRepr.(*chunkReprJS) if !ok { continue } var crossChunkPrefixStmts []js_ast.Stmt for _, crossChunkImport := range c.sortedCrossChunkImports(chunks, chunkRepr.importsFromOtherChunks) { switch c.options.OutputFormat { case config.FormatESModule: var items []js_ast.ClauseItem for _, item := range crossChunkImport.sortedImportItems { items = append(items, js_ast.ClauseItem{Name: js_ast.LocRef{Ref: item.ref}, Alias: item.exportAlias}) } importRecordIndex := uint32(len(chunk.crossChunkImports)) chunk.crossChunkImports = append(chunk.crossChunkImports, chunkImport{ importKind: ast.ImportStmt, chunkIndex: crossChunkImport.chunkIndex, }) if len(items) > 0 { // "import {a, b} from './chunk.js'" crossChunkPrefixStmts = append(crossChunkPrefixStmts, js_ast.Stmt{Data: &js_ast.SImport{ Items: &items, ImportRecordIndex: importRecordIndex, }}) } else { // "import './chunk.js'" crossChunkPrefixStmts = append(crossChunkPrefixStmts, js_ast.Stmt{Data: &js_ast.SImport{ ImportRecordIndex: importRecordIndex, }}) } default: panic("Internal error") } } chunkRepr.crossChunkPrefixStmts = crossChunkPrefixStmts } } type crossChunkImport struct { chunkIndex uint32 sortedImportItems crossChunkImportItemArray } // This type is just so we can use Go's native sort function type crossChunkImportArray []crossChunkImport func (a crossChunkImportArray) Len() int { return len(a) } func (a crossChunkImportArray) Swap(i int, j int) { a[i], a[j] = a[j], a[i] } func (a crossChunkImportArray) Less(i int, j int) bool { return a[i].chunkIndex < a[j].chunkIndex } // Sort cross-chunk imports by chunk name for determinism func (c *linkerContext) sortedCrossChunkImports(chunks []chunkInfo, importsFromOtherChunks map[uint32]crossChunkImportItemArray) crossChunkImportArray { result := make(crossChunkImportArray, 0, len(importsFromOtherChunks)) for otherChunkIndex, importItems := range importsFromOtherChunks { // Sort imports from a single chunk by alias for determinism otherChunk := &chunks[otherChunkIndex] exportsToOtherChunks := otherChunk.chunkRepr.(*chunkReprJS).exportsToOtherChunks for i, item := range importItems { importItems[i].exportAlias = exportsToOtherChunks[item.ref] } sort.Sort(importItems) result = append(result, crossChunkImport{ chunkIndex: otherChunkIndex, sortedImportItems: importItems, }) } sort.Sort(result) return result } type crossChunkImportItem struct { ref js_ast.Ref exportAlias string } // This type is just so we can use Go's native sort function type crossChunkImportItemArray []crossChunkImportItem func (a crossChunkImportItemArray) Len() int { return len(a) } func (a crossChunkImportItemArray) Swap(i int, j int) { a[i], a[j] = a[j], a[i] } func (a crossChunkImportItemArray) Less(i int, j int) bool { return a[i].exportAlias < a[j].exportAlias } // Sort cross-chunk exports by chunk name for determinism func (c *linkerContext) sortedCrossChunkExportItems(exportRefs map[js_ast.Ref]bool) renamer.StableRefArray { result := make(renamer.StableRefArray, 0, len(exportRefs)) for ref := range exportRefs { result = append(result, renamer.StableRef{ StableSourceIndex: c.graph.StableSourceIndices[ref.SourceIndex], Ref: ref, }) } sort.Sort(result) return result } func (c *linkerContext) scanImportsAndExports() { // Step 1: Figure out what modules must be CommonJS for _, sourceIndex := range c.graph.ReachableFiles { file := &c.graph.Files[sourceIndex] switch repr := file.InputFile.Repr.(type) { case *graph.CSSRepr: // Inline URLs for non-CSS files into the CSS file var additionalFiles []graph.OutputFile for importRecordIndex := range repr.AST.ImportRecords { if record := &repr.AST.ImportRecords[importRecordIndex]; record.SourceIndex.IsValid() { otherFile := &c.graph.Files[record.SourceIndex.GetIndex()] if otherRepr, ok := otherFile.InputFile.Repr.(*graph.JSRepr); ok { record.Path.Text = otherRepr.AST.URLForCSS record.Path.Namespace = "" record.SourceIndex = ast.Index32{} // Copy the additional files to the output directory additionalFiles = append(additionalFiles, otherFile.InputFile.AdditionalFiles...) } } } file.InputFile.AdditionalFiles = additionalFiles case *graph.JSRepr: for importRecordIndex := range repr.AST.ImportRecords { record := &repr.AST.ImportRecords[importRecordIndex] if !record.SourceIndex.IsValid() { continue } otherFile := &c.graph.Files[record.SourceIndex.GetIndex()] otherRepr := otherFile.InputFile.Repr.(*graph.JSRepr) switch record.Kind { case ast.ImportStmt: // Importing using ES6 syntax from a file without any ES6 syntax // causes that module to be considered CommonJS-style, even if it // doesn't have any CommonJS exports. // // That means the ES6 imports will become undefined instead of // causing errors. This is for compatibility with older CommonJS- // style bundlers. // // We emit a warning in this case but try to avoid turning the module // into a CommonJS module if possible. This is possible with named // imports (the module stays an ECMAScript module but the imports are // rewritten with undefined) but is not possible with star or default // imports: // // import * as ns from './empty-file' // import defVal from './empty-file' // console.log(ns, defVal) // // In that case the module *is* considered a CommonJS module because // the namespace object must be created. if (record.ContainsImportStar || record.ContainsDefaultAlias) && otherRepr.AST.ExportsKind == js_ast.ExportsNone && !otherRepr.AST.HasLazyExport { otherRepr.Meta.Wrap = graph.WrapCJS otherRepr.AST.ExportsKind = js_ast.ExportsCommonJS } case ast.ImportRequire: // Files that are imported with require() must be CommonJS modules if otherRepr.AST.ExportsKind == js_ast.ExportsESM { otherRepr.Meta.Wrap = graph.WrapESM } else { otherRepr.Meta.Wrap = graph.WrapCJS otherRepr.AST.ExportsKind = js_ast.ExportsCommonJS } case ast.ImportDynamic: if !c.options.CodeSplitting { // If we're not splitting, then import() is just a require() that // returns a promise, so the imported file must be a CommonJS module if otherRepr.AST.ExportsKind == js_ast.ExportsESM { otherRepr.Meta.Wrap = graph.WrapESM } else { otherRepr.Meta.Wrap = graph.WrapCJS otherRepr.AST.ExportsKind = js_ast.ExportsCommonJS } } } } // If the output format doesn't have an implicit CommonJS wrapper, any file // that uses CommonJS features will need to be wrapped, even though the // resulting wrapper won't be invoked by other files. An exception is made // for entry point files in CommonJS format (or when in pass-through mode). if repr.AST.ExportsKind == js_ast.ExportsCommonJS && (!file.IsEntryPoint() || c.options.OutputFormat == config.FormatIIFE || c.options.OutputFormat == config.FormatESModule) { repr.Meta.Wrap = graph.WrapCJS } } } // Step 2: Propagate dynamic export status for export star statements that // are re-exports from a module whose exports are not statically analyzable. // In this case the export star must be evaluated at run time instead of at // bundle time. for _, sourceIndex := range c.graph.ReachableFiles { repr, ok := c.graph.Files[sourceIndex].InputFile.Repr.(*graph.JSRepr) if !ok { continue } if repr.Meta.Wrap != graph.WrapNone { c.recursivelyWrapDependencies(sourceIndex) } if len(repr.AST.ExportStarImportRecords) > 0 { visited := make(map[uint32]bool) c.hasDynamicExportsDueToExportStar(sourceIndex, visited) } // Even if the output file is CommonJS-like, we may still need to wrap // CommonJS-style files. Any file that imports a CommonJS-style file will // cause that file to need to be wrapped. This is because the import // method, whatever it is, will need to invoke the wrapper. Note that // this can include entry points (e.g. an entry point that imports a file // that imports that entry point). for _, record := range repr.AST.ImportRecords { if record.SourceIndex.IsValid() { otherRepr := c.graph.Files[record.SourceIndex.GetIndex()].InputFile.Repr.(*graph.JSRepr) if otherRepr.AST.ExportsKind == js_ast.ExportsCommonJS { c.recursivelyWrapDependencies(record.SourceIndex.GetIndex()) } } } } // Step 3: Resolve "export * from" statements. This must be done after we // discover all modules that can have dynamic exports because export stars // are ignored for those modules. exportStarStack := make([]uint32, 0, 32) for _, sourceIndex := range c.graph.ReachableFiles { repr, ok := c.graph.Files[sourceIndex].InputFile.Repr.(*graph.JSRepr) if !ok { continue } // Expression-style loaders defer code generation until linking. Code // generation is done here because at this point we know that the // "ExportsKind" field has its final value and will not be changed. if repr.AST.HasLazyExport { c.generateCodeForLazyExport(sourceIndex) } // Propagate exports for export star statements if len(repr.AST.ExportStarImportRecords) > 0 { c.addExportsForExportStar(repr.Meta.ResolvedExports, sourceIndex, exportStarStack) } // Add an empty part for the namespace export that we can fill in later repr.Meta.NSExportPartIndex = c.graph.AddPartToFile(sourceIndex, js_ast.Part{ CanBeRemovedIfUnused: true, }) // Also add a special export so import stars can bind to it. This must be // done in this step because it must come after CommonJS module discovery // but before matching imports with exports. repr.Meta.ResolvedExportStar = &graph.ExportData{ Ref: repr.AST.ExportsRef, SourceIndex: sourceIndex, } repr.AST.TopLevelSymbolToParts[repr.AST.ExportsRef] = []uint32{repr.Meta.NSExportPartIndex} } // Step 4: Match imports with exports. This must be done after we process all // export stars because imports can bind to export star re-exports. for _, sourceIndex := range c.graph.ReachableFiles { file := &c.graph.Files[sourceIndex] repr, ok := file.InputFile.Repr.(*graph.JSRepr) if !ok { continue } if len(repr.AST.NamedImports) > 0 { c.matchImportsWithExportsForFile(uint32(sourceIndex)) } // If we're exporting as CommonJS and this file doesn't need a wrapper, // then we'll be using the actual CommonJS "exports" and/or "module" // symbols. In that case make sure to mark them as such so they don't // get minified. if (c.options.OutputFormat == config.FormatPreserve || c.options.OutputFormat == config.FormatCommonJS) && repr.Meta.Wrap == graph.WrapNone && file.IsEntryPoint() { exportsRef := js_ast.FollowSymbols(c.graph.Symbols, repr.AST.ExportsRef) moduleRef := js_ast.FollowSymbols(c.graph.Symbols, repr.AST.ModuleRef) c.graph.Symbols.Get(exportsRef).Kind = js_ast.SymbolUnbound c.graph.Symbols.Get(moduleRef).Kind = js_ast.SymbolUnbound } // Create the wrapper part for wrapped files. This is needed by a later step. c.createWrapperForFile(uint32(sourceIndex)) } // Step 5: Create namespace exports for every file. This is always necessary // for CommonJS files, and is also necessary for other files if they are // imported using an import star statement. waitGroup := sync.WaitGroup{} for _, sourceIndex := range c.graph.ReachableFiles { repr, ok := c.graph.Files[sourceIndex].InputFile.Repr.(*graph.JSRepr) if !ok { continue } // This is the slowest step and is also parallelizable, so do this in parallel. waitGroup.Add(1) go func(sourceIndex uint32, repr *graph.JSRepr) { // Now that all exports have been resolved, sort and filter them to create // something we can iterate over later. aliases := make([]string, 0, len(repr.Meta.ResolvedExports)) nextAlias: for alias, export := range repr.Meta.ResolvedExports { // Re-exporting multiple symbols with the same name causes an ambiguous // export. These names cannot be used and should not end up in generated code. otherRepr := c.graph.Files[export.SourceIndex].InputFile.Repr.(*graph.JSRepr) if len(export.PotentiallyAmbiguousExportStarRefs) > 0 { mainRef := export.Ref if imported, ok := otherRepr.Meta.ImportsToBind[export.Ref]; ok { mainRef = imported.Ref } for _, ambiguousExport := range export.PotentiallyAmbiguousExportStarRefs { ambiguousRepr := c.graph.Files[ambiguousExport.SourceIndex].InputFile.Repr.(*graph.JSRepr) ambiguousRef := ambiguousExport.Ref if imported, ok := ambiguousRepr.Meta.ImportsToBind[ambiguousExport.Ref]; ok { ambiguousRef = imported.Ref } if mainRef != ambiguousRef { continue nextAlias } } } // Ignore re-exported imports in TypeScript files that failed to be // resolved. These are probably just type-only imports so the best thing to // do is to silently omit them from the export list. if otherRepr.Meta.IsProbablyTypeScriptType[export.Ref] { continue } aliases = append(aliases, alias) } sort.Strings(aliases) repr.Meta.SortedAndFilteredExportAliases = aliases // Export creation uses "sortedAndFilteredExportAliases" so this must // come second after we fill in that array c.createExportsForFile(uint32(sourceIndex)) waitGroup.Done() }(sourceIndex, repr) } waitGroup.Wait() // Step 6: Bind imports to exports. This adds non-local dependencies on the // parts that declare the export to all parts that use the import. Also // generate wrapper parts for wrapped files. for _, sourceIndex := range c.graph.ReachableFiles { file := &c.graph.Files[sourceIndex] repr, ok := file.InputFile.Repr.(*graph.JSRepr) if !ok { continue } // Pre-generate symbols for re-exports CommonJS symbols in case they // are necessary later. This is done now because the symbols map cannot be // mutated later due to parallelism. if file.IsEntryPoint() && c.options.OutputFormat == config.FormatESModule { copies := make([]js_ast.Ref, len(repr.Meta.SortedAndFilteredExportAliases)) for i, alias := range repr.Meta.SortedAndFilteredExportAliases { copies[i] = c.graph.GenerateNewSymbol(sourceIndex, js_ast.SymbolOther, "export_"+alias) } repr.Meta.CJSExportCopies = copies } // Use "init_*" for ESM wrappers instead of "require_*" if repr.Meta.Wrap == graph.WrapESM { c.graph.Symbols.Get(repr.AST.WrapperRef).OriginalName = "init_" + file.InputFile.Source.IdentifierName } // If this isn't CommonJS, then rename the unused "exports" and "module" // variables to avoid them causing the identically-named variables in // actual CommonJS files from being renamed. This is purely about // aesthetics and is not about correctness. This is done here because by // this point, we know the CommonJS status will not change further. if repr.Meta.Wrap != graph.WrapCJS && repr.AST.ExportsKind != js_ast.ExportsCommonJS && (!file.IsEntryPoint() || c.options.OutputFormat != config.FormatCommonJS) { name := file.InputFile.Source.IdentifierName c.graph.Symbols.Get(repr.AST.ExportsRef).OriginalName = name + "_exports" c.graph.Symbols.Get(repr.AST.ModuleRef).OriginalName = name + "_module" } // Include the "__export" symbol from the runtime if it was used in the // previous step. The previous step can't do this because it's running in // parallel and can't safely mutate the "importsToBind" map of another file. if repr.Meta.NeedsExportSymbolFromRuntime || repr.Meta.NeedsMarkAsModuleSymbolFromRuntime { runtimeRepr := c.graph.Files[runtime.SourceIndex].InputFile.Repr.(*graph.JSRepr) if repr.Meta.NeedsExportSymbolFromRuntime { exportRef := runtimeRepr.AST.ModuleScope.Members["__export"].Ref c.graph.GenerateSymbolImportAndUse(sourceIndex, repr.Meta.NSExportPartIndex, exportRef, 1, runtime.SourceIndex) } if repr.Meta.NeedsMarkAsModuleSymbolFromRuntime { markAsModuleRef := runtimeRepr.AST.ModuleScope.Members["__markAsModule"].Ref c.graph.GenerateSymbolImportAndUse(sourceIndex, repr.Meta.NSExportPartIndex, markAsModuleRef, 1, runtime.SourceIndex) } } for importRef, importData := range repr.Meta.ImportsToBind { resolvedRepr := c.graph.Files[importData.SourceIndex].InputFile.Repr.(*graph.JSRepr) partsDeclaringSymbol := resolvedRepr.AST.TopLevelSymbolToParts[importData.Ref] for _, partIndex := range repr.AST.NamedImports[importRef].LocalPartsWithUses { part := &repr.AST.Parts[partIndex] // Depend on the file containing the imported symbol for _, resolvedPartIndex := range partsDeclaringSymbol { part.Dependencies = append(part.Dependencies, js_ast.Dependency{ SourceIndex: importData.SourceIndex, PartIndex: resolvedPartIndex, }) } // Also depend on any files that re-exported this symbol in between the // file containing the import and the file containing the imported symbol part.Dependencies = append(part.Dependencies, importData.ReExports...) } // Merge these symbols so they will share the same name js_ast.MergeSymbols(c.graph.Symbols, importRef, importData.Ref) } // If this is an entry point, depend on all exports so they are included if file.IsEntryPoint() { var dependencies []js_ast.Dependency for _, alias := range repr.Meta.SortedAndFilteredExportAliases { export := repr.Meta.ResolvedExports[alias] targetSourceIndex := export.SourceIndex targetRef := export.Ref // If this is an import, then target what the import points to targetRepr := c.graph.Files[targetSourceIndex].InputFile.Repr.(*graph.JSRepr) if importData, ok := targetRepr.Meta.ImportsToBind[targetRef]; ok { targetSourceIndex = importData.SourceIndex targetRef = importData.Ref targetRepr = c.graph.Files[targetSourceIndex].InputFile.Repr.(*graph.JSRepr) dependencies = append(dependencies, importData.ReExports...) } // Pull in all declarations of this symbol for _, partIndex := range targetRepr.AST.TopLevelSymbolToParts[targetRef] { dependencies = append(dependencies, js_ast.Dependency{ SourceIndex: targetSourceIndex, PartIndex: partIndex, }) } } // Ensure "exports" is included if the current output format needs it if repr.Meta.ForceIncludeExportsForEntryPoint { dependencies = append(dependencies, js_ast.Dependency{ SourceIndex: sourceIndex, PartIndex: repr.Meta.NSExportPartIndex, }) } // Include the wrapper if present if repr.Meta.Wrap != graph.WrapNone { dependencies = append(dependencies, js_ast.Dependency{ SourceIndex: sourceIndex, PartIndex: repr.Meta.WrapperPartIndex.GetIndex(), }) } // Represent these constraints with a dummy part entryPointPartIndex := c.graph.AddPartToFile(sourceIndex, js_ast.Part{ Dependencies: dependencies, CanBeRemovedIfUnused: false, }) repr.Meta.EntryPointPartIndex = ast.MakeIndex32(entryPointPartIndex) } // Encode import-specific constraints in the dependency graph for partIndex, part := range repr.AST.Parts { toModuleUses := uint32(0) // Imports of wrapped files must depend on the wrapper for _, importRecordIndex := range part.ImportRecordIndices { record := &repr.AST.ImportRecords[importRecordIndex] // Don't follow external imports (this includes import() expressions) if !record.SourceIndex.IsValid() || c.isExternalDynamicImport(record, sourceIndex) { // This is an external import, so it needs the "__toModule" wrapper as // long as it's not a bare "require()" if record.Kind != ast.ImportRequire && (!c.options.OutputFormat.KeepES6ImportExportSyntax() || (record.Kind == ast.ImportDynamic && c.options.UnsupportedJSFeatures.Has(compat.DynamicImport))) { record.WrapWithToModule = true toModuleUses++ } continue } otherSourceIndex := record.SourceIndex.GetIndex() otherRepr := c.graph.Files[otherSourceIndex].InputFile.Repr.(*graph.JSRepr) if otherRepr.Meta.Wrap != graph.WrapNone { // Depend on the automatically-generated require wrapper symbol wrapperRef := otherRepr.AST.WrapperRef c.graph.GenerateSymbolImportAndUse(sourceIndex, uint32(partIndex), wrapperRef, 1, otherSourceIndex) // This is an ES6 import of a CommonJS module, so it needs the // "__toModule" wrapper as long as it's not a bare "require()" if record.Kind != ast.ImportRequire && otherRepr.AST.ExportsKind == js_ast.ExportsCommonJS { record.WrapWithToModule = true toModuleUses++ } // If this is an ESM wrapper, also depend on the exports object // since the final code will contain an inline reference to it. // This must be done for "require()" and "import()" expressions // but does not need to be done for "import" statements since // those just cause us to reference the exports directly. if otherRepr.Meta.Wrap == graph.WrapESM && record.Kind != ast.ImportStmt { c.graph.GenerateSymbolImportAndUse(sourceIndex, uint32(partIndex), otherRepr.AST.ExportsRef, 1, otherSourceIndex) } } else if record.Kind == ast.ImportStmt && otherRepr.AST.ExportsKind == js_ast.ExportsESMWithDynamicFallback { // This is an import of a module that has a dynamic export fallback // object. In that case we need to depend on that object in case // something ends up needing to use it later. This could potentially // be omitted in some cases with more advanced analysis if this // dynamic export fallback object doesn't end up being needed. c.graph.GenerateSymbolImportAndUse(sourceIndex, uint32(partIndex), otherRepr.AST.ExportsRef, 1, otherSourceIndex) } } // If there's an ES6 import of a non-ES6 module, then we're going to need the // "__toModule" symbol from the runtime to wrap the result of "require()" c.graph.GenerateRuntimeSymbolImportAndUse(sourceIndex, uint32(partIndex), "__toModule", toModuleUses) // If there's an ES6 export star statement of a non-ES6 module, then we're // going to need the "__reExport" symbol from the runtime reExportUses := uint32(0) for _, importRecordIndex := range repr.AST.ExportStarImportRecords { record := &repr.AST.ImportRecords[importRecordIndex] // Is this export star evaluated at run time? happensAtRunTime := !record.SourceIndex.IsValid() && (!file.IsEntryPoint() || !c.options.OutputFormat.KeepES6ImportExportSyntax()) if record.SourceIndex.IsValid() { otherSourceIndex := record.SourceIndex.GetIndex() otherRepr := c.graph.Files[otherSourceIndex].InputFile.Repr.(*graph.JSRepr) if otherSourceIndex != sourceIndex && otherRepr.AST.ExportsKind.IsDynamic() { happensAtRunTime = true } if otherRepr.AST.ExportsKind == js_ast.ExportsESMWithDynamicFallback { // This looks like "__reExport(exports_a, exports_b)". Make sure to // pull in the "exports_b" symbol into this export star. This matters // in code splitting situations where the "export_b" symbol might live // in a different chunk than this export star. c.graph.GenerateSymbolImportAndUse(sourceIndex, uint32(partIndex), otherRepr.AST.ExportsRef, 1, otherSourceIndex) } } if happensAtRunTime { // Depend on this file's "exports" object for the first argument to "__reExport" c.graph.GenerateSymbolImportAndUse(sourceIndex, uint32(partIndex), repr.AST.ExportsRef, 1, sourceIndex) record.CallsRunTimeReExportFn = true repr.AST.UsesExportsRef = true reExportUses++ } } c.graph.GenerateRuntimeSymbolImportAndUse(sourceIndex, uint32(partIndex), "__reExport", reExportUses) } } } func (c *linkerContext) generateCodeForLazyExport(sourceIndex uint32) { file := &c.graph.Files[sourceIndex] repr := file.InputFile.Repr.(*graph.JSRepr) // Grab the lazy expression if len(repr.AST.Parts) < 1 { panic("Internal error") } part := &repr.AST.Parts[0] if len(part.Stmts) != 1 { panic("Internal error") } lazy, ok := part.Stmts[0].Data.(*js_ast.SLazyExport) if !ok { panic("Internal error") } // Use "module.exports = value" for CommonJS-style modules if repr.AST.ExportsKind == js_ast.ExportsCommonJS { part.Stmts = []js_ast.Stmt{js_ast.AssignStmt( js_ast.Expr{Loc: lazy.Value.Loc, Data: &js_ast.EDot{ Target: js_ast.Expr{Loc: lazy.Value.Loc, Data: &js_ast.EIdentifier{Ref: repr.AST.ModuleRef}}, Name: "exports", NameLoc: lazy.Value.Loc, }}, lazy.Value, )} c.graph.GenerateSymbolImportAndUse(sourceIndex, 0, repr.AST.ModuleRef, 1, sourceIndex) return } // Otherwise, generate ES6 export statements. These are added as additional // parts so they can be tree shaken individually. part.Stmts = nil type prevExport struct { ref js_ast.Ref partIndex uint32 } generateExport := func(name string, alias string, value js_ast.Expr) prevExport { // Generate a new symbol ref := c.graph.GenerateNewSymbol(sourceIndex, js_ast.SymbolOther, name) // Generate an ES6 export var stmt js_ast.Stmt if alias == "default" { stmt = js_ast.Stmt{Loc: value.Loc, Data: &js_ast.SExportDefault{ DefaultName: js_ast.LocRef{Loc: value.Loc, Ref: ref}, Value: js_ast.ExprOrStmt{Expr: &value}, }} } else { stmt = js_ast.Stmt{Loc: value.Loc, Data: &js_ast.SLocal{ IsExport: true, Decls: []js_ast.Decl{{ Binding: js_ast.Binding{Loc: value.Loc, Data: &js_ast.BIdentifier{Ref: ref}}, Value: &value, }}, }} } // Link the export into the graph for tree shaking partIndex := c.graph.AddPartToFile(sourceIndex, js_ast.Part{ Stmts: []js_ast.Stmt{stmt}, DeclaredSymbols: []js_ast.DeclaredSymbol{{Ref: ref, IsTopLevel: true}}, CanBeRemovedIfUnused: true, }) c.graph.GenerateSymbolImportAndUse(sourceIndex, partIndex, repr.AST.ModuleRef, 1, sourceIndex) repr.Meta.ResolvedExports[alias] = graph.ExportData{Ref: ref, SourceIndex: sourceIndex} return prevExport{ref: ref, partIndex: partIndex} } // Unwrap JSON objects into separate top-level variables var prevExports []js_ast.Ref jsonValue := lazy.Value if object, ok := jsonValue.Data.(*js_ast.EObject); ok { clone := *object clone.Properties = append(make([]js_ast.Property, 0, len(clone.Properties)), clone.Properties...) for i, property := range clone.Properties { if str, ok := property.Key.Data.(*js_ast.EString); ok && (!file.IsEntryPoint() || js_lexer.IsIdentifierUTF16(str.Value)) { name := js_lexer.UTF16ToString(str.Value) exportRef := generateExport(name, name, *property.Value).ref prevExports = append(prevExports, exportRef) clone.Properties[i].Value = &js_ast.Expr{Loc: property.Key.Loc, Data: &js_ast.EIdentifier{Ref: exportRef}} } } jsonValue.Data = &clone } // Generate the default export finalExportPartIndex := generateExport(file.InputFile.Source.IdentifierName+"_default", "default", jsonValue).partIndex // The default export depends on all of the previous exports for _, exportRef := range prevExports { c.graph.GenerateSymbolImportAndUse(sourceIndex, finalExportPartIndex, exportRef, 1, sourceIndex) } } func (c *linkerContext) createExportsForFile(sourceIndex uint32) { //////////////////////////////////////////////////////////////////////////////// // WARNING: This method is run in parallel over all files. Do not mutate data // for other files within this method or you will create a data race. //////////////////////////////////////////////////////////////////////////////// file := &c.graph.Files[sourceIndex] repr := file.InputFile.Repr.(*graph.JSRepr) // Generate a getter per export properties := []js_ast.Property{} nsExportDependencies := []js_ast.Dependency{} nsExportSymbolUses := make(map[js_ast.Ref]js_ast.SymbolUse) for _, alias := range repr.Meta.SortedAndFilteredExportAliases { export := repr.Meta.ResolvedExports[alias] // If this is an export of an import, reference the symbol that the import // was eventually resolved to. We need to do this because imports have // already been resolved by this point, so we can't generate a new import // and have that be resolved later. if importData, ok := c.graph.Files[export.SourceIndex].InputFile.Repr.(*graph.JSRepr).Meta.ImportsToBind[export.Ref]; ok { export.Ref = importData.Ref export.SourceIndex = importData.SourceIndex nsExportDependencies = append(nsExportDependencies, importData.ReExports...) } // Exports of imports need EImportIdentifier in case they need to be re- // written to a property access later on var value js_ast.Expr if c.graph.Symbols.Get(export.Ref).NamespaceAlias != nil { value = js_ast.Expr{Data: &js_ast.EImportIdentifier{Ref: export.Ref}} } else { value = js_ast.Expr{Data: &js_ast.EIdentifier{Ref: export.Ref}} } // Add a getter property var getter js_ast.Expr body := js_ast.FnBody{Stmts: []js_ast.Stmt{{Loc: value.Loc, Data: &js_ast.SReturn{Value: &value}}}} if c.options.UnsupportedJSFeatures.Has(compat.Arrow) { getter = js_ast.Expr{Data: &js_ast.EFunction{Fn: js_ast.Fn{Body: body}}} } else { getter = js_ast.Expr{Data: &js_ast.EArrow{PreferExpr: true, Body: body}} } properties = append(properties, js_ast.Property{ Key: js_ast.Expr{Data: &js_ast.EString{Value: js_lexer.StringToUTF16(alias)}}, Value: &getter, }) nsExportSymbolUses[export.Ref] = js_ast.SymbolUse{CountEstimate: 1} // Make sure the part that declares the export is included for _, partIndex := range c.graph.Files[export.SourceIndex].InputFile.Repr.(*graph.JSRepr).AST.TopLevelSymbolToParts[export.Ref] { // Use a non-local dependency since this is likely from a different // file if it came in through an export star nsExportDependencies = append(nsExportDependencies, js_ast.Dependency{ SourceIndex: export.SourceIndex, PartIndex: partIndex, }) } } // Prefix this part with "var exports = {}" if this isn't a CommonJS module declaredSymbols := []js_ast.DeclaredSymbol{} var nsExportStmts []js_ast.Stmt if repr.AST.ExportsKind != js_ast.ExportsCommonJS && (!file.IsEntryPoint() || c.options.OutputFormat != config.FormatCommonJS) { nsExportStmts = append(nsExportStmts, js_ast.Stmt{Data: &js_ast.SLocal{Decls: []js_ast.Decl{{ Binding: js_ast.Binding{Data: &js_ast.BIdentifier{Ref: repr.AST.ExportsRef}}, Value: &js_ast.Expr{Data: &js_ast.EObject{}}, }}}}) declaredSymbols = append(declaredSymbols, js_ast.DeclaredSymbol{ Ref: repr.AST.ExportsRef, IsTopLevel: true, }) } // If this file was originally ESM but is now in CommonJS, add a call to // "__markAsModule" which sets the "__esModule" property to true. This must // be done before any to "require()" or circular imports of multiple modules // that have been each converted from ESM to CommonJS may not work correctly. if repr.AST.ExportKeyword.Len > 0 && (repr.AST.ExportsKind == js_ast.ExportsCommonJS || (file.IsEntryPoint() && c.options.OutputFormat == config.FormatCommonJS)) { runtimeRepr := c.graph.Files[runtime.SourceIndex].InputFile.Repr.(*graph.JSRepr) markAsModuleRef := runtimeRepr.AST.ModuleScope.Members["__markAsModule"].Ref nsExportStmts = append(nsExportStmts, js_ast.Stmt{Data: &js_ast.SExpr{Value: js_ast.Expr{Data: &js_ast.ECall{ Target: js_ast.Expr{Data: &js_ast.EIdentifier{Ref: markAsModuleRef}}, Args: []js_ast.Expr{{Data: &js_ast.EIdentifier{Ref: repr.AST.ExportsRef}}}, }}}}) // Make sure this file depends on the "__markAsModule" symbol for _, partIndex := range runtimeRepr.AST.TopLevelSymbolToParts[markAsModuleRef] { nsExportDependencies = append(nsExportDependencies, js_ast.Dependency{ SourceIndex: runtime.SourceIndex, PartIndex: partIndex, }) } // Pull in the "__markAsModule" symbol later. Also make sure the "exports" // variable is marked as used because we used it above. repr.Meta.NeedsMarkAsModuleSymbolFromRuntime = true repr.AST.UsesExportsRef = true } // "__export(exports, { foo: () => foo })" exportRef := js_ast.InvalidRef if len(properties) > 0 { runtimeRepr := c.graph.Files[runtime.SourceIndex].InputFile.Repr.(*graph.JSRepr) exportRef = runtimeRepr.AST.ModuleScope.Members["__export"].Ref nsExportStmts = append(nsExportStmts, js_ast.Stmt{Data: &js_ast.SExpr{Value: js_ast.Expr{Data: &js_ast.ECall{ Target: js_ast.Expr{Data: &js_ast.EIdentifier{Ref: exportRef}}, Args: []js_ast.Expr{ {Data: &js_ast.EIdentifier{Ref: repr.AST.ExportsRef}}, {Data: &js_ast.EObject{ Properties: properties, }}, }, }}}}) // Make sure this file depends on the "__export" symbol for _, partIndex := range runtimeRepr.AST.TopLevelSymbolToParts[exportRef] { nsExportDependencies = append(nsExportDependencies, js_ast.Dependency{ SourceIndex: runtime.SourceIndex, PartIndex: partIndex, }) } // Make sure the CommonJS closure, if there is one, includes "exports" repr.AST.UsesExportsRef = true } // No need to generate a part if it'll be empty if len(nsExportStmts) > 0 { // Initialize the part that was allocated for us earlier. The information // here will be used after this during tree shaking. repr.AST.Parts[repr.Meta.NSExportPartIndex] = js_ast.Part{ Stmts: nsExportStmts, SymbolUses: nsExportSymbolUses, Dependencies: nsExportDependencies, DeclaredSymbols: declaredSymbols, // This can be removed if nothing uses it CanBeRemovedIfUnused: true, // Make sure this is trimmed if unused even if tree shaking is disabled ForceTreeShaking: true, } // Pull in the "__export" symbol if it was used if exportRef != js_ast.InvalidRef { repr.Meta.NeedsExportSymbolFromRuntime = true } } } func (c *linkerContext) createWrapperForFile(sourceIndex uint32) { repr := c.graph.Files[sourceIndex].InputFile.Repr.(*graph.JSRepr) switch repr.Meta.Wrap { // If this is a CommonJS file, we're going to need to generate a wrapper // for the CommonJS closure. That will end up looking something like this: // // var require_foo = __commonJS((exports, module) => { // ... // }); // // However, that generation is special-cased for various reasons and is // done later on. Still, we're going to need to ensure that this file // both depends on the "__commonJS" symbol and declares the "require_foo" // symbol. Instead of special-casing this during the reachablity analysis // below, we just append a dummy part to the end of the file with these // dependencies and let the general-purpose reachablity analysis take care // of it. case graph.WrapCJS: runtimeRepr := c.graph.Files[runtime.SourceIndex].InputFile.Repr.(*graph.JSRepr) commonJSRef := runtimeRepr.AST.NamedExports["__commonJS"].Ref commonJSParts := runtimeRepr.AST.TopLevelSymbolToParts[commonJSRef] // Generate the dummy part dependencies := make([]js_ast.Dependency, len(commonJSParts)) for i, partIndex := range commonJSParts { dependencies[i] = js_ast.Dependency{ SourceIndex: runtime.SourceIndex, PartIndex: partIndex, } } partIndex := c.graph.AddPartToFile(sourceIndex, js_ast.Part{ SymbolUses: map[js_ast.Ref]js_ast.SymbolUse{ repr.AST.WrapperRef: {CountEstimate: 1}, }, DeclaredSymbols: []js_ast.DeclaredSymbol{ {Ref: repr.AST.ExportsRef, IsTopLevel: true}, {Ref: repr.AST.ModuleRef, IsTopLevel: true}, {Ref: repr.AST.WrapperRef, IsTopLevel: true}, }, Dependencies: dependencies, }) repr.Meta.WrapperPartIndex = ast.MakeIndex32(partIndex) c.graph.GenerateSymbolImportAndUse(sourceIndex, partIndex, commonJSRef, 1, runtime.SourceIndex) // If this is a lazily-initialized ESM file, we're going to need to // generate a wrapper for the ESM closure. That will end up looking // something like this: // // var init_foo = __esm(() => { // ... // }); // // This depends on the "__esm" symbol and declares the "init_foo" symbol // for similar reasons to the CommonJS closure above. case graph.WrapESM: runtimeRepr := c.graph.Files[runtime.SourceIndex].InputFile.Repr.(*graph.JSRepr) esmRef := runtimeRepr.AST.NamedExports["__esm"].Ref esmParts := runtimeRepr.AST.TopLevelSymbolToParts[esmRef] // Generate the dummy part dependencies := make([]js_ast.Dependency, len(esmParts)) for i, partIndex := range esmParts { dependencies[i] = js_ast.Dependency{ SourceIndex: runtime.SourceIndex, PartIndex: partIndex, } } partIndex := c.graph.AddPartToFile(sourceIndex, js_ast.Part{ SymbolUses: map[js_ast.Ref]js_ast.SymbolUse{ repr.AST.WrapperRef: {CountEstimate: 1}, }, DeclaredSymbols: []js_ast.DeclaredSymbol{ {Ref: repr.AST.WrapperRef, IsTopLevel: true}, }, Dependencies: dependencies, }) repr.Meta.WrapperPartIndex = ast.MakeIndex32(partIndex) c.graph.GenerateSymbolImportAndUse(sourceIndex, partIndex, esmRef, 1, runtime.SourceIndex) } } func (c *linkerContext) matchImportsWithExportsForFile(sourceIndex uint32) { file := &c.graph.Files[sourceIndex] repr := file.InputFile.Repr.(*graph.JSRepr) // Sort imports for determinism. Otherwise our unit tests will randomly // fail sometimes when error messages are reordered. sortedImportRefs := make([]int, 0, len(repr.AST.NamedImports)) for ref := range repr.AST.NamedImports { sortedImportRefs = append(sortedImportRefs, int(ref.InnerIndex)) } sort.Ints(sortedImportRefs) // Pair imports with their matching exports for _, innerIndex := range sortedImportRefs { // Re-use memory for the cycle detector c.cycleDetector = c.cycleDetector[:0] importRef := js_ast.Ref{SourceIndex: sourceIndex, InnerIndex: uint32(innerIndex)} result, reExports := c.matchImportWithExport(importTracker{sourceIndex: sourceIndex, importRef: importRef}, nil) switch result.kind { case matchImportIgnore: case matchImportNormal: repr.Meta.ImportsToBind[importRef] = graph.ImportData{ ReExports: reExports, SourceIndex: result.sourceIndex, Ref: result.ref, } case matchImportNamespace: c.graph.Symbols.Get(importRef).NamespaceAlias = &js_ast.NamespaceAlias{ NamespaceRef: result.namespaceRef, Alias: result.alias, } case matchImportNormalAndNamespace: repr.Meta.ImportsToBind[importRef] = graph.ImportData{ ReExports: reExports, SourceIndex: result.sourceIndex, Ref: result.ref, } c.graph.Symbols.Get(importRef).NamespaceAlias = &js_ast.NamespaceAlias{ NamespaceRef: result.namespaceRef, Alias: result.alias, } case matchImportCycle: namedImport := repr.AST.NamedImports[importRef] c.log.AddRangeError(&file.InputFile.Source, js_lexer.RangeOfIdentifier(file.InputFile.Source, namedImport.AliasLoc), fmt.Sprintf("Detected cycle while resolving import %q", namedImport.Alias)) case matchImportProbablyTypeScriptType: repr.Meta.IsProbablyTypeScriptType[importRef] = true case matchImportAmbiguous: namedImport := repr.AST.NamedImports[importRef] r := js_lexer.RangeOfIdentifier(file.InputFile.Source, namedImport.AliasLoc) var notes []logger.MsgData // Provide the locations of both ambiguous exports if possible if result.nameLoc.Start != 0 && result.otherNameLoc.Start != 0 { a := c.graph.Files[result.sourceIndex].InputFile.Source b := c.graph.Files[result.otherSourceIndex].InputFile.Source notes = []logger.MsgData{ logger.RangeData(&a, js_lexer.RangeOfIdentifier(a, result.nameLoc), "One matching export is here"), logger.RangeData(&b, js_lexer.RangeOfIdentifier(b, result.otherNameLoc), "Another matching export is here"), } } symbol := c.graph.Symbols.Get(importRef) if symbol.ImportItemStatus == js_ast.ImportItemGenerated { // This is a warning instead of an error because although it appears // to be a named import, it's actually an automatically-generated // named import that was originally a property access on an import // star namespace object. Normally this property access would just // resolve to undefined at run-time instead of failing at binding- // time, so we emit a warning and rewrite the value to the literal // "undefined" instead of emitting an error. symbol.ImportItemStatus = js_ast.ImportItemMissing msg := fmt.Sprintf("Import %q will always be undefined because there are multiple matching exports", namedImport.Alias) c.log.AddRangeWarningWithNotes(&file.InputFile.Source, r, msg, notes) } else { msg := fmt.Sprintf("Ambiguous import %q has multiple matching exports", namedImport.Alias) c.log.AddRangeErrorWithNotes(&file.InputFile.Source, r, msg, notes) } } } } type matchImportKind uint8 const ( // The import is either external or undefined matchImportIgnore matchImportKind = iota // "sourceIndex" and "ref" are in use matchImportNormal // "namespaceRef" and "alias" are in use matchImportNamespace // Both "matchImportNormal" and "matchImportNamespace" matchImportNormalAndNamespace // The import could not be evaluated due to a cycle matchImportCycle // The import is missing but came from a TypeScript file matchImportProbablyTypeScriptType // The import resolved to multiple symbols via "export * from" matchImportAmbiguous ) type matchImportResult struct { kind matchImportKind namespaceRef js_ast.Ref alias string sourceIndex uint32 nameLoc logger.Loc // Optional, goes with sourceIndex, ignore if zero otherSourceIndex uint32 otherNameLoc logger.Loc // Optional, goes with otherSourceIndex, ignore if zero ref js_ast.Ref } func (c *linkerContext) matchImportWithExport( tracker importTracker, reExportsIn []js_ast.Dependency, ) (result matchImportResult, reExports []js_ast.Dependency) { var ambiguousResults []matchImportResult reExports = reExportsIn loop: for { // Make sure we avoid infinite loops trying to resolve cycles: // // // foo.js // export {a as b} from './foo.js' // export {b as c} from './foo.js' // export {c as a} from './foo.js' // // This uses a O(n^2) array scan instead of a O(n) map because the vast // majority of cases have one or two elements and Go arrays are cheap to // reuse without allocating. for _, previousTracker := range c.cycleDetector { if tracker == previousTracker { result = matchImportResult{kind: matchImportCycle} break loop } } c.cycleDetector = append(c.cycleDetector, tracker) // Resolve the import by one step nextTracker, status, potentiallyAmbiguousExportStarRefs := c.advanceImportTracker(tracker) switch status { case importCommonJS, importCommonJSWithoutExports, importExternal, importDisabled: if status == importExternal && c.options.OutputFormat.KeepES6ImportExportSyntax() { // Imports from external modules should not be converted to CommonJS // if the output format preserves the original ES6 import statements break } // If it's a CommonJS or external file, rewrite the import to a // property access. Don't do this if the namespace reference is invalid // though. This is the case for star imports, where the import is the // namespace. trackerFile := &c.graph.Files[tracker.sourceIndex] namedImport := trackerFile.InputFile.Repr.(*graph.JSRepr).AST.NamedImports[tracker.importRef] if namedImport.NamespaceRef != js_ast.InvalidRef { if result.kind == matchImportNormal { result.kind = matchImportNormalAndNamespace result.namespaceRef = namedImport.NamespaceRef result.alias = namedImport.Alias } else { result = matchImportResult{ kind: matchImportNamespace, namespaceRef: namedImport.NamespaceRef, alias: namedImport.Alias, } } } // Warn about importing from a file that is known to not have any exports if status == importCommonJSWithoutExports { source := trackerFile.InputFile.Source symbol := c.graph.Symbols.Get(tracker.importRef) symbol.ImportItemStatus = js_ast.ImportItemMissing c.log.AddRangeWarning(&source, js_lexer.RangeOfIdentifier(source, namedImport.AliasLoc), fmt.Sprintf("Import %q will always be undefined because the file %q has no exports", namedImport.Alias, c.graph.Files[nextTracker.sourceIndex].InputFile.Source.PrettyPath)) } case importDynamicFallback: // If it's a file with dynamic export fallback, rewrite the import to a property access trackerFile := &c.graph.Files[tracker.sourceIndex] namedImport := trackerFile.InputFile.Repr.(*graph.JSRepr).AST.NamedImports[tracker.importRef] if result.kind == matchImportNormal { result.kind = matchImportNormalAndNamespace result.namespaceRef = nextTracker.importRef result.alias = namedImport.Alias } else { result = matchImportResult{ kind: matchImportNamespace, namespaceRef: nextTracker.importRef, alias: namedImport.Alias, } } case importNoMatch: symbol := c.graph.Symbols.Get(tracker.importRef) trackerFile := &c.graph.Files[tracker.sourceIndex] source := trackerFile.InputFile.Source namedImport := trackerFile.InputFile.Repr.(*graph.JSRepr).AST.NamedImports[tracker.importRef] r := js_lexer.RangeOfIdentifier(source, namedImport.AliasLoc) // Report mismatched imports and exports if symbol.ImportItemStatus == js_ast.ImportItemGenerated { // This is a warning instead of an error because although it appears // to be a named import, it's actually an automatically-generated // named import that was originally a property access on an import // star namespace object. Normally this property access would just // resolve to undefined at run-time instead of failing at binding- // time, so we emit a warning and rewrite the value to the literal // "undefined" instead of emitting an error. symbol.ImportItemStatus = js_ast.ImportItemMissing c.log.AddRangeWarning(&source, r, fmt.Sprintf( "Import %q will always be undefined because there is no matching export", namedImport.Alias)) } else { c.log.AddRangeError(&source, r, fmt.Sprintf("No matching export in %q for import %q", c.graph.Files[nextTracker.sourceIndex].InputFile.Source.PrettyPath, namedImport.Alias)) } case importProbablyTypeScriptType: // Omit this import from any namespace export code we generate for // import star statements (i.e. "import * as ns from 'path'") result = matchImportResult{kind: matchImportProbablyTypeScriptType} case importFound: // If there are multiple ambiguous results due to use of "export * from" // statements, trace them all to see if they point to different things. for _, ambiguousTracker := range potentiallyAmbiguousExportStarRefs { // If this is a re-export of another import, follow the import if _, ok := c.graph.Files[ambiguousTracker.SourceIndex].InputFile.Repr.(*graph.JSRepr).AST.NamedImports[ambiguousTracker.Ref]; ok { // Save and restore the cycle detector to avoid mixing information oldCycleDetector := c.cycleDetector ambiguousResult, newReExportFiles := c.matchImportWithExport(importTracker{ sourceIndex: ambiguousTracker.SourceIndex, importRef: ambiguousTracker.Ref, }, reExports) c.cycleDetector = oldCycleDetector ambiguousResults = append(ambiguousResults, ambiguousResult) reExports = newReExportFiles } else { ambiguousResults = append(ambiguousResults, matchImportResult{ kind: matchImportNormal, sourceIndex: ambiguousTracker.SourceIndex, ref: ambiguousTracker.Ref, nameLoc: ambiguousTracker.NameLoc, }) } } // Defer the actual binding of this import until after we generate // namespace export code for all files. This has to be done for all // import-to-export matches, not just the initial import to the final // export, since all imports and re-exports must be merged together // for correctness. result = matchImportResult{ kind: matchImportNormal, sourceIndex: nextTracker.sourceIndex, ref: nextTracker.importRef, nameLoc: nextTracker.nameLoc, } // Depend on the statement(s) that declared this import symbol in the // original file for _, resolvedPartIndex := range c.graph.Files[tracker.sourceIndex].InputFile.Repr.(*graph.JSRepr).AST.TopLevelSymbolToParts[tracker.importRef] { reExports = append(reExports, js_ast.Dependency{ SourceIndex: tracker.sourceIndex, PartIndex: resolvedPartIndex, }) } // If this is a re-export of another import, continue for another // iteration of the loop to resolve that import as well if _, ok := c.graph.Files[nextTracker.sourceIndex].InputFile.Repr.(*graph.JSRepr).AST.NamedImports[nextTracker.importRef]; ok { tracker = nextTracker continue } default: panic("Internal error") } // Stop now if we didn't explicitly "continue" above break } // If there is a potential ambiguity, all results must be the same for _, ambiguousResult := range ambiguousResults { if ambiguousResult != result { if result.kind == matchImportNormal && ambiguousResult.kind == matchImportNormal && result.nameLoc.Start != 0 && ambiguousResult.nameLoc.Start != 0 { return matchImportResult{ kind: matchImportAmbiguous, sourceIndex: result.sourceIndex, nameLoc: result.nameLoc, otherSourceIndex: ambiguousResult.sourceIndex, otherNameLoc: ambiguousResult.nameLoc, }, nil } return matchImportResult{kind: matchImportAmbiguous}, nil } } return } func (c *linkerContext) recursivelyWrapDependencies(sourceIndex uint32) { repr := c.graph.Files[sourceIndex].InputFile.Repr.(*graph.JSRepr) if repr.Meta.DidWrapDependencies { return } repr.Meta.DidWrapDependencies = true // Never wrap the runtime file since it always comes first if sourceIndex == runtime.SourceIndex { return } // This module must be wrapped if repr.Meta.Wrap == graph.WrapNone { if repr.AST.ExportsKind == js_ast.ExportsCommonJS { repr.Meta.Wrap = graph.WrapCJS } else { repr.Meta.Wrap = graph.WrapESM } } // All dependencies must also be wrapped for _, record := range repr.AST.ImportRecords { if record.SourceIndex.IsValid() { c.recursivelyWrapDependencies(record.SourceIndex.GetIndex()) } } } func (c *linkerContext) hasDynamicExportsDueToExportStar(sourceIndex uint32, visited map[uint32]bool) bool { // Terminate the traversal now if this file already has dynamic exports repr := c.graph.Files[sourceIndex].InputFile.Repr.(*graph.JSRepr) if repr.AST.ExportsKind == js_ast.ExportsCommonJS || repr.AST.ExportsKind == js_ast.ExportsESMWithDynamicFallback { return true } // Avoid infinite loops due to cycles in the export star graph if visited[sourceIndex] { return false } visited[sourceIndex] = true // Scan over the export star graph for _, importRecordIndex := range repr.AST.ExportStarImportRecords { record := &repr.AST.ImportRecords[importRecordIndex] // This file has dynamic exports if the exported imports are from a file // that either has dynamic exports directly or transitively by itself // having an export star from a file with dynamic exports. if (!record.SourceIndex.IsValid() && (!c.graph.Files[sourceIndex].IsEntryPoint() || !c.options.OutputFormat.KeepES6ImportExportSyntax())) || (record.SourceIndex.IsValid() && record.SourceIndex.GetIndex() != sourceIndex && c.hasDynamicExportsDueToExportStar(record.SourceIndex.GetIndex(), visited)) { repr.AST.ExportsKind = js_ast.ExportsESMWithDynamicFallback return true } } return false } func (c *linkerContext) addExportsForExportStar( resolvedExports map[string]graph.ExportData, sourceIndex uint32, sourceIndexStack []uint32, ) { // Avoid infinite loops due to cycles in the export star graph for _, prevSourceIndex := range sourceIndexStack { if prevSourceIndex == sourceIndex { return } } sourceIndexStack = append(sourceIndexStack, sourceIndex) repr := c.graph.Files[sourceIndex].InputFile.Repr.(*graph.JSRepr) for _, importRecordIndex := range repr.AST.ExportStarImportRecords { record := &repr.AST.ImportRecords[importRecordIndex] if !record.SourceIndex.IsValid() { // This will be resolved at run time instead continue } otherSourceIndex := record.SourceIndex.GetIndex() // Export stars from a CommonJS module don't work because they can't be // statically discovered. Just silently ignore them in this case. // // We could attempt to check whether the imported file still has ES6 // exports even though it still uses CommonJS features. However, when // doing this we'd also have to rewrite any imports of these export star // re-exports as property accesses off of a generated require() call. otherRepr := c.graph.Files[otherSourceIndex].InputFile.Repr.(*graph.JSRepr) if otherRepr.AST.ExportsKind == js_ast.ExportsCommonJS { // All exports will be resolved at run time instead continue } // Accumulate this file's exports nextExport: for alias, name := range otherRepr.AST.NamedExports { // ES6 export star statements ignore exports named "default" if alias == "default" { continue } // This export star is shadowed if any file in the stack has a matching real named export for _, prevSourceIndex := range sourceIndexStack { prevRepr := c.graph.Files[prevSourceIndex].InputFile.Repr.(*graph.JSRepr) if _, ok := prevRepr.AST.NamedExports[alias]; ok { continue nextExport } } if existing, ok := resolvedExports[alias]; !ok { // Initialize the re-export resolvedExports[alias] = graph.ExportData{ Ref: name.Ref, SourceIndex: otherSourceIndex, NameLoc: name.AliasLoc, } // Make sure the symbol is marked as imported so that code splitting // imports it correctly if it ends up being shared with another chunk repr.Meta.ImportsToBind[name.Ref] = graph.ImportData{ Ref: name.Ref, SourceIndex: otherSourceIndex, } } else if existing.SourceIndex != otherSourceIndex { // Two different re-exports colliding makes it potentially ambiguous existing.PotentiallyAmbiguousExportStarRefs = append(existing.PotentiallyAmbiguousExportStarRefs, graph.ImportData{ SourceIndex: otherSourceIndex, Ref: name.Ref, NameLoc: name.AliasLoc, }) resolvedExports[alias] = existing } } // Search further through this file's export stars c.addExportsForExportStar(resolvedExports, otherSourceIndex, sourceIndexStack) } } type importTracker struct { sourceIndex uint32 nameLoc logger.Loc // Optional, goes with sourceIndex, ignore if zero importRef js_ast.Ref } type importStatus uint8 const ( // The imported file has no matching export importNoMatch importStatus = iota // The imported file has a matching export importFound // The imported file is CommonJS and has unknown exports importCommonJS // The import is missing but there is a dynamic fallback object importDynamicFallback // The import was treated as a CommonJS import but the file is known to have no exports importCommonJSWithoutExports // The imported file was disabled by mapping it to false in the "browser" // field of package.json importDisabled // The imported file is external and has unknown exports importExternal // This is a missing re-export in a TypeScript file, so it's probably a type importProbablyTypeScriptType ) func (c *linkerContext) advanceImportTracker(tracker importTracker) (importTracker, importStatus, []graph.ImportData) { file := &c.graph.Files[tracker.sourceIndex] repr := file.InputFile.Repr.(*graph.JSRepr) namedImport := repr.AST.NamedImports[tracker.importRef] // Is this an external file? record := &repr.AST.ImportRecords[namedImport.ImportRecordIndex] if !record.SourceIndex.IsValid() { return importTracker{}, importExternal, nil } // Is this a disabled file? otherSourceIndex := record.SourceIndex.GetIndex() if c.graph.Files[otherSourceIndex].InputFile.Source.KeyPath.IsDisabled() { return importTracker{sourceIndex: otherSourceIndex, importRef: js_ast.InvalidRef}, importDisabled, nil } // Is this a named import of a file without any exports? otherRepr := c.graph.Files[otherSourceIndex].InputFile.Repr.(*graph.JSRepr) if !namedImport.AliasIsStar && !otherRepr.AST.HasLazyExport && // CommonJS exports otherRepr.AST.ExportKeyword.Len == 0 && namedImport.Alias != "default" && // ESM exports !otherRepr.AST.UsesExportsRef && !otherRepr.AST.UsesModuleRef { // Just warn about it and replace the import with "undefined" return importTracker{sourceIndex: otherSourceIndex, importRef: js_ast.InvalidRef}, importCommonJSWithoutExports, nil } // Is this a CommonJS file? if otherRepr.AST.ExportsKind == js_ast.ExportsCommonJS { return importTracker{sourceIndex: otherSourceIndex, importRef: js_ast.InvalidRef}, importCommonJS, nil } // Match this import star with an export star from the imported file if matchingExport := otherRepr.Meta.ResolvedExportStar; namedImport.AliasIsStar && matchingExport != nil { // Check to see if this is a re-export of another import return importTracker{ sourceIndex: matchingExport.SourceIndex, importRef: matchingExport.Ref, nameLoc: matchingExport.NameLoc, }, importFound, matchingExport.PotentiallyAmbiguousExportStarRefs } // Match this import up with an export from the imported file if matchingExport, ok := otherRepr.Meta.ResolvedExports[namedImport.Alias]; ok { // Check to see if this is a re-export of another import return importTracker{ sourceIndex: matchingExport.SourceIndex, importRef: matchingExport.Ref, nameLoc: matchingExport.NameLoc, }, importFound, matchingExport.PotentiallyAmbiguousExportStarRefs } // Is this a file with dynamic exports? if otherRepr.AST.ExportsKind == js_ast.ExportsESMWithDynamicFallback { return importTracker{sourceIndex: otherSourceIndex, importRef: otherRepr.AST.ExportsRef}, importDynamicFallback, nil } // Missing re-exports in TypeScript files are indistinguishable from types if file.InputFile.Loader.IsTypeScript() && namedImport.IsExported { return importTracker{}, importProbablyTypeScriptType, nil } return importTracker{sourceIndex: otherSourceIndex}, importNoMatch, nil } func (c *linkerContext) treeShakingAndCodeSplitting() { // Tree shaking: Each entry point marks all files reachable from itself for _, entryPoint := range c.graph.EntryPoints() { c.markFileLiveForTreeShaking(entryPoint.SourceIndex) } // Code splitting: Determine which entry points can reach which files. This // has to happen after tree shaking because there is an implicit dependency // between live parts within the same file. All liveness has to be computed // first before determining which entry points can reach which files. for i, entryPoint := range c.graph.EntryPoints() { c.markFileReachableForCodeSplitting(entryPoint.SourceIndex, uint(i), 0) } } func (c *linkerContext) markFileReachableForCodeSplitting(sourceIndex uint32, entryPointBit uint, distanceFromEntryPoint uint32) { file := &c.graph.Files[sourceIndex] if !file.IsLive { return } traverseAgain := false // Track the minimum distance to an entry point if distanceFromEntryPoint < file.DistanceFromEntryPoint { file.DistanceFromEntryPoint = distanceFromEntryPoint traverseAgain = true } distanceFromEntryPoint++ // Don't mark this file more than once if file.EntryBits.HasBit(entryPointBit) && !traverseAgain { return } file.EntryBits.SetBit(entryPointBit) switch repr := file.InputFile.Repr.(type) { case *graph.JSRepr: // If the JavaScript stub for a CSS file is included, also include the CSS file if repr.CSSSourceIndex.IsValid() { c.markFileReachableForCodeSplitting(repr.CSSSourceIndex.GetIndex(), entryPointBit, distanceFromEntryPoint) } // Traverse into all imported files for _, record := range repr.AST.ImportRecords { if record.SourceIndex.IsValid() && !c.isExternalDynamicImport(&record, sourceIndex) { c.markFileReachableForCodeSplitting(record.SourceIndex.GetIndex(), entryPointBit, distanceFromEntryPoint) } } // Traverse into all dependencies of all parts in this file for _, part := range repr.AST.Parts { for _, dependency := range part.Dependencies { if dependency.SourceIndex != sourceIndex { c.markFileReachableForCodeSplitting(dependency.SourceIndex, entryPointBit, distanceFromEntryPoint) } } } case *graph.CSSRepr: // Traverse into all dependencies for _, record := range repr.AST.ImportRecords { if record.SourceIndex.IsValid() { c.markFileReachableForCodeSplitting(record.SourceIndex.GetIndex(), entryPointBit, distanceFromEntryPoint) } } } } func (c *linkerContext) markFileLiveForTreeShaking(sourceIndex uint32) { file := &c.graph.Files[sourceIndex] // Don't mark this file more than once if file.IsLive { return } file.IsLive = true switch repr := file.InputFile.Repr.(type) { case *graph.JSRepr: isTreeShakingEnabled := config.IsTreeShakingEnabled(c.options.Mode, c.options.OutputFormat) // If the JavaScript stub for a CSS file is included, also include the CSS file if repr.CSSSourceIndex.IsValid() { c.markFileLiveForTreeShaking(repr.CSSSourceIndex.GetIndex()) } for partIndex, part := range repr.AST.Parts { canBeRemovedIfUnused := part.CanBeRemovedIfUnused // Also include any statement-level imports for _, importRecordIndex := range part.ImportRecordIndices { record := &repr.AST.ImportRecords[importRecordIndex] if record.Kind != ast.ImportStmt { continue } if record.SourceIndex.IsValid() { otherSourceIndex := record.SourceIndex.GetIndex() // Don't include this module for its side effects if it can be // considered to have no side effects if otherFile := &c.graph.Files[otherSourceIndex]; otherFile.InputFile.SideEffects.Kind != graph.HasSideEffects && !c.options.IgnoreDCEAnnotations { continue } // Otherwise, include this module for its side effects c.markFileLiveForTreeShaking(otherSourceIndex) } // If we get here then the import was included for its side effects, so // we must also keep this part canBeRemovedIfUnused = false } // Include all parts in this file with side effects, or just include // everything if tree-shaking is disabled. Note that we still want to // perform tree-shaking on the runtime even if tree-shaking is disabled. if !canBeRemovedIfUnused || (!part.ForceTreeShaking && !isTreeShakingEnabled && file.IsEntryPoint()) { c.markPartLiveForTreeShaking(sourceIndex, uint32(partIndex)) } } case *graph.CSSRepr: // Include all "@import" rules for _, record := range repr.AST.ImportRecords { if record.SourceIndex.IsValid() { c.markFileLiveForTreeShaking(record.SourceIndex.GetIndex()) } } } } func (c *linkerContext) isExternalDynamicImport(record *ast.ImportRecord, sourceIndex uint32) bool { return record.Kind == ast.ImportDynamic && c.graph.Files[record.SourceIndex.GetIndex()].IsEntryPoint() && record.SourceIndex.GetIndex() != sourceIndex } func (c *linkerContext) markPartLiveForTreeShaking(sourceIndex uint32, partIndex uint32) { file := &c.graph.Files[sourceIndex] repr := file.InputFile.Repr.(*graph.JSRepr) part := &repr.AST.Parts[partIndex] // Don't mark this part more than once if part.IsLive { return } part.IsLive = true // Include the file containing this part c.markFileLiveForTreeShaking(sourceIndex) // Also include any dependencies for _, dep := range part.Dependencies { c.markPartLiveForTreeShaking(dep.SourceIndex, dep.PartIndex) } } func sanitizeFilePathForVirtualModulePath(path string) string { // Convert it to a safe file path. See: https://stackoverflow.com/a/31976060 sb := strings.Builder{} needsGap := false for _, c := range path { switch c { case 0: // These characters are forbidden on Unix and Windows case '<', '>', ':', '"', '|', '?', '*': // These characters are forbidden on Windows default: if c < 0x20 { // These characters are forbidden on Windows break } // Turn runs of invalid characters into a '_' if needsGap { sb.WriteByte('_') needsGap = false } sb.WriteRune(c) continue } if sb.Len() > 0 { needsGap = true } } // Make sure the name isn't empty if sb.Len() == 0 { return "_" } // Note: An extension will be added to this base name, so there is no need to // avoid forbidden file names such as ".." since ".js" is a valid file name. return sb.String() } func (c *linkerContext) computeChunks() []chunkInfo { jsChunks := make(map[string]chunkInfo) cssChunks := make(map[string]chunkInfo) // Create chunks for entry points for i, entryPoint := range c.graph.EntryPoints() { file := &c.graph.Files[entryPoint.SourceIndex] // Create a chunk for the entry point here to ensure that the chunk is // always generated even if the resulting file is empty entryBits := helpers.NewBitSet(uint(len(c.graph.EntryPoints()))) entryBits.SetBit(uint(i)) info := chunkInfo{ entryBits: entryBits, isEntryPoint: true, sourceIndex: entryPoint.SourceIndex, entryPointBit: uint(i), filesWithPartsInChunk: make(map[uint32]bool), } switch file.InputFile.Repr.(type) { case *graph.JSRepr: info.chunkRepr = &chunkReprJS{} jsChunks[entryBits.String()] = info case *graph.CSSRepr: info.chunkRepr = &chunkReprCSS{} cssChunks[entryBits.String()] = info } } // Figure out which files are in which chunk for _, sourceIndex := range c.graph.ReachableFiles { file := &c.graph.Files[sourceIndex] if !file.IsLive { // Ignore this file if it's not included in the bundle continue } key := file.EntryBits.String() var chunk chunkInfo var ok bool switch file.InputFile.Repr.(type) { case *graph.JSRepr: chunk, ok = jsChunks[key] if !ok { chunk.entryBits = file.EntryBits chunk.filesWithPartsInChunk = make(map[uint32]bool) chunk.chunkRepr = &chunkReprJS{} jsChunks[key] = chunk } case *graph.CSSRepr: chunk, ok = cssChunks[key] if !ok { chunk.entryBits = file.EntryBits chunk.filesWithPartsInChunk = make(map[uint32]bool) chunk.chunkRepr = &chunkReprCSS{} // Check whether this is the CSS file to go with a JS entry point if jsChunk, ok := jsChunks[key]; ok && jsChunk.isEntryPoint { chunk.isEntryPoint = true chunk.sourceIndex = jsChunk.sourceIndex chunk.entryPointBit = jsChunk.entryPointBit } cssChunks[key] = chunk } } chunk.filesWithPartsInChunk[uint32(sourceIndex)] = true } // Sort the chunks for determinism. This matters because we use chunk indices // as sorting keys in a few places. sortedChunks := make([]chunkInfo, 0, len(jsChunks)+len(cssChunks)) sortedKeys := make([]string, 0, len(jsChunks)+len(cssChunks)) for key := range jsChunks { sortedKeys = append(sortedKeys, key) } sort.Strings(sortedKeys) for _, key := range sortedKeys { sortedChunks = append(sortedChunks, jsChunks[key]) } sortedKeys = sortedKeys[:0] for key := range cssChunks { sortedKeys = append(sortedKeys, key) } sort.Strings(sortedKeys) for _, key := range sortedKeys { sortedChunks = append(sortedChunks, cssChunks[key]) } // Map from the entry point file to this chunk. We will need this later if // a file contains a dynamic import to this entry point, since we'll need // to look up the path for this chunk to use with the import. for chunkIndex, chunk := range sortedChunks { if chunk.isEntryPoint { file := &c.graph.Files[chunk.sourceIndex] // JS entry points that import CSS files generate two chunks, a JS chunk // and a CSS chunk. Don't link the CSS chunk to the JS file since the CSS // chunk is secondary (the JS chunk is primary). if _, ok := chunk.chunkRepr.(*chunkReprCSS); ok { if _, ok := file.InputFile.Repr.(*graph.JSRepr); ok { continue } } file.EntryPointChunkIndex = uint32(chunkIndex) } } // Determine the order of files within the chunk ahead of time. This may // generate additional CSS chunks from JS chunks that import CSS files. { for chunkIndex, chunk := range sortedChunks { js, jsParts, css := c.chunkFileOrder(&chunk) switch chunk.chunkRepr.(type) { case *chunkReprJS: sortedChunks[chunkIndex].filesInChunkInOrder = js sortedChunks[chunkIndex].partsInChunkInOrder = jsParts // If JS files include CSS files, make a sibling chunk for the CSS if len(css) > 0 { sortedChunks = append(sortedChunks, chunkInfo{ filesInChunkInOrder: css, entryBits: chunk.entryBits, isEntryPoint: chunk.isEntryPoint, sourceIndex: chunk.sourceIndex, entryPointBit: chunk.entryPointBit, filesWithPartsInChunk: make(map[uint32]bool), chunkRepr: &chunkReprCSS{}, }) } case *chunkReprCSS: sortedChunks[chunkIndex].filesInChunkInOrder = css } } } // Assign general information to each chunk for chunkIndex := range sortedChunks { chunk := &sortedChunks[chunkIndex] // Assign a unique key to each chunk. This key encodes the index directly so // we can easily recover it later without needing to look it up in a map. The // last 8 numbers of the key are the chunk index. chunk.uniqueKey = fmt.Sprintf("%s%08d", c.uniqueKeyPrefix, chunkIndex) // Determine the standard file extension var stdExt string switch chunk.chunkRepr.(type) { case *chunkReprJS: stdExt = c.options.OutputExtensionJS case *chunkReprCSS: stdExt = c.options.OutputExtensionCSS } // Compute the template substitutions var dir, base, ext string var template []config.PathTemplate if chunk.isEntryPoint { if c.graph.Files[chunk.sourceIndex].IsUserSpecifiedEntryPoint() { dir, base, ext = c.pathRelativeToOutbase(chunk.sourceIndex, chunk.entryPointBit, stdExt, false /* avoidIndex */) template = c.options.EntryPathTemplate } else { dir, base, ext = c.pathRelativeToOutbase(chunk.sourceIndex, chunk.entryPointBit, stdExt, true /* avoidIndex */) template = c.options.ChunkPathTemplate } } else { dir = "/" base = "chunk" ext = stdExt template = c.options.ChunkPathTemplate } // Determine the output path template template = append(append(make([]config.PathTemplate, 0, len(template)+1), template...), config.PathTemplate{Data: ext}) chunk.finalTemplate = config.SubstituteTemplate(template, config.PathPlaceholders{ Dir: &dir, Name: &base, }) } return sortedChunks } type chunkOrder struct { sourceIndex uint32 distance uint32 tieBreaker uint32 } // This type is just so we can use Go's native sort function type chunkOrderArray []chunkOrder func (a chunkOrderArray) Len() int { return len(a) } func (a chunkOrderArray) Swap(i int, j int) { a[i], a[j] = a[j], a[i] } func (a chunkOrderArray) Less(i int, j int) bool { ai := a[i] aj := a[j] return ai.distance < aj.distance || (ai.distance == aj.distance && ai.tieBreaker < aj.tieBreaker) } func appendOrExtendPartRange(ranges []partRange, sourceIndex uint32, partIndex uint32) []partRange { if i := len(ranges) - 1; i >= 0 { if r := &ranges[i]; r.sourceIndex == sourceIndex && r.partIndexEnd == partIndex { r.partIndexEnd = partIndex + 1 return ranges } } return append(ranges, partRange{ sourceIndex: sourceIndex, partIndexBegin: partIndex, partIndexEnd: partIndex + 1, }) } func (c *linkerContext) shouldIncludePart(repr *graph.JSRepr, part js_ast.Part) bool { // As an optimization, ignore parts containing a single import statement to // an internal non-wrapped file. These will be ignored anyway and it's a // performance hit to spin up a goroutine only to discover this later. if len(part.Stmts) == 1 { if s, ok := part.Stmts[0].Data.(*js_ast.SImport); ok { record := &repr.AST.ImportRecords[s.ImportRecordIndex] if record.SourceIndex.IsValid() && c.graph.Files[record.SourceIndex.GetIndex()].InputFile.Repr.(*graph.JSRepr).Meta.Wrap == graph.WrapNone { return false } } } return true } func (c *linkerContext) chunkFileOrder(chunk *chunkInfo) (js []uint32, jsParts []partRange, css []uint32) { sorted := make(chunkOrderArray, 0, len(chunk.filesWithPartsInChunk)) // Attach information to the files for use with sorting for sourceIndex := range chunk.filesWithPartsInChunk { file := &c.graph.Files[sourceIndex] sorted = append(sorted, chunkOrder{ sourceIndex: sourceIndex, distance: file.DistanceFromEntryPoint, tieBreaker: c.graph.StableSourceIndices[sourceIndex], }) } // Sort so files closest to an entry point come first. If two files are // equidistant to an entry point, then break the tie by sorting on the // stable source index derived from the DFS over all entry points. sort.Sort(sorted) visited := make(map[uint32]bool) jsPartsPrefix := []partRange{} // Traverse the graph using this stable order and linearize the files with // dependencies before dependents var visit func(uint32) visit = func(sourceIndex uint32) { if visited[sourceIndex] { return } visited[sourceIndex] = true file := &c.graph.Files[sourceIndex] isFileInThisChunk := chunk.entryBits.Equals(file.EntryBits) switch repr := file.InputFile.Repr.(type) { case *graph.JSRepr: // Wrapped files can't be split because they are all inside the wrapper canFileBeSplit := repr.Meta.Wrap == graph.WrapNone // Make sure the generated call to "__export(exports, ...)" comes first // before anything else in this file if canFileBeSplit && isFileInThisChunk && repr.AST.Parts[repr.Meta.NSExportPartIndex].IsLive { jsParts = appendOrExtendPartRange(jsParts, sourceIndex, repr.Meta.NSExportPartIndex) } for partIndex, part := range repr.AST.Parts { isPartInThisChunk := isFileInThisChunk && repr.AST.Parts[partIndex].IsLive // Also traverse any files imported by this part for _, importRecordIndex := range part.ImportRecordIndices { record := &repr.AST.ImportRecords[importRecordIndex] if record.SourceIndex.IsValid() && (record.Kind == ast.ImportStmt || isPartInThisChunk) { if c.isExternalDynamicImport(record, sourceIndex) { // Don't follow import() dependencies continue } visit(record.SourceIndex.GetIndex()) } } // Then include this part after the files it imports if isPartInThisChunk { isFileInThisChunk = true if canFileBeSplit && uint32(partIndex) != repr.Meta.NSExportPartIndex && c.shouldIncludePart(repr, part) { if sourceIndex == runtime.SourceIndex { jsPartsPrefix = appendOrExtendPartRange(jsPartsPrefix, sourceIndex, uint32(partIndex)) } else { jsParts = appendOrExtendPartRange(jsParts, sourceIndex, uint32(partIndex)) } } } } if isFileInThisChunk { js = append(js, sourceIndex) // CommonJS files are all-or-nothing so all parts must be contiguous if !canFileBeSplit { jsPartsPrefix = append(jsPartsPrefix, partRange{ sourceIndex: sourceIndex, partIndexBegin: 0, partIndexEnd: uint32(len(repr.AST.Parts)), }) } } case *graph.CSSRepr: if isFileInThisChunk { // All imported files come first for _, record := range repr.AST.ImportRecords { if record.SourceIndex.IsValid() { visit(record.SourceIndex.GetIndex()) } } // Then this file comes afterward css = append(css, sourceIndex) } } } // Always put the runtime code first before anything else visit(runtime.SourceIndex) for _, data := range sorted { visit(data.sourceIndex) } jsParts = append(jsPartsPrefix, jsParts...) return } func (c *linkerContext) shouldRemoveImportExportStmt( sourceIndex uint32, stmtList *stmtList, loc logger.Loc, namespaceRef js_ast.Ref, importRecordIndex uint32, ) bool { repr := c.graph.Files[sourceIndex].InputFile.Repr.(*graph.JSRepr) record := &repr.AST.ImportRecords[importRecordIndex] // Is this an external import? if !record.SourceIndex.IsValid() { // Keep the "import" statement if "import" statements are supported if c.options.OutputFormat.KeepES6ImportExportSyntax() { return false } // Otherwise, replace this statement with a call to "require()" stmtList.insideWrapperPrefix = append(stmtList.insideWrapperPrefix, js_ast.Stmt{ Loc: loc, Data: &js_ast.SLocal{Decls: []js_ast.Decl{{ Binding: js_ast.Binding{Loc: loc, Data: &js_ast.BIdentifier{Ref: namespaceRef}}, Value: &js_ast.Expr{Loc: record.Range.Loc, Data: &js_ast.ERequire{ImportRecordIndex: importRecordIndex}}, }}}, }) return true } // We don't need a call to "require()" if this is a self-import inside a // CommonJS-style module, since we can just reference the exports directly. if repr.AST.ExportsKind == js_ast.ExportsCommonJS && js_ast.FollowSymbols(c.graph.Symbols, namespaceRef) == repr.AST.ExportsRef { return true } otherFile := &c.graph.Files[record.SourceIndex.GetIndex()] otherRepr := otherFile.InputFile.Repr.(*graph.JSRepr) switch otherRepr.Meta.Wrap { case graph.WrapNone: // Remove the statement entirely if this module is not wrapped case graph.WrapCJS: // Replace the statement with a call to "require()" stmtList.insideWrapperPrefix = append(stmtList.insideWrapperPrefix, js_ast.Stmt{ Loc: loc, Data: &js_ast.SLocal{Decls: []js_ast.Decl{{ Binding: js_ast.Binding{Loc: loc, Data: &js_ast.BIdentifier{Ref: namespaceRef}}, Value: &js_ast.Expr{Loc: record.Range.Loc, Data: &js_ast.ERequire{ImportRecordIndex: importRecordIndex}}, }}}, }) case graph.WrapESM: // Ignore this file if it's not included in the bundle. This can happen for // wrapped ESM files but not for wrapped CommonJS files because we allow // tree shaking inside wrapped ESM files. if !otherFile.IsLive { break } // Replace the statement with a call to "init()" value := js_ast.Expr{Loc: loc, Data: &js_ast.ECall{Target: js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: otherRepr.AST.WrapperRef}}}} if otherRepr.Meta.IsAsyncOrHasAsyncDependency { // This currently evaluates sibling dependencies in serial instead of in // parallel, which is incorrect. This should be changed to store a promise // and await all stored promises after all imports but before any code. value.Data = &js_ast.EAwait{Value: value} } stmtList.insideWrapperPrefix = append(stmtList.insideWrapperPrefix, js_ast.Stmt{Loc: loc, Data: &js_ast.SExpr{Value: value}}) } return true } func (c *linkerContext) convertStmtsForChunk(sourceIndex uint32, stmtList *stmtList, partStmts []js_ast.Stmt) { file := &c.graph.Files[sourceIndex] shouldStripExports := c.options.Mode != config.ModePassThrough || !file.IsEntryPoint() repr := file.InputFile.Repr.(*graph.JSRepr) shouldExtractESMStmtsForWrap := repr.Meta.Wrap != graph.WrapNone for _, stmt := range partStmts { switch s := stmt.Data.(type) { case *js_ast.SImport: // "import * as ns from 'path'" // "import {foo} from 'path'" if c.shouldRemoveImportExportStmt(sourceIndex, stmtList, stmt.Loc, s.NamespaceRef, s.ImportRecordIndex) { continue } // Make sure these don't end up in the wrapper closure if shouldExtractESMStmtsForWrap { stmtList.outsideWrapperPrefix = append(stmtList.outsideWrapperPrefix, stmt) continue } case *js_ast.SExportStar: // "export * as ns from 'path'" if s.Alias != nil { if c.shouldRemoveImportExportStmt(sourceIndex, stmtList, stmt.Loc, s.NamespaceRef, s.ImportRecordIndex) { continue } if shouldStripExports { // Turn this statement into "import * as ns from 'path'" stmt.Data = &js_ast.SImport{ NamespaceRef: s.NamespaceRef, StarNameLoc: &s.Alias.Loc, ImportRecordIndex: s.ImportRecordIndex, } } // Make sure these don't end up in the wrapper closure if shouldExtractESMStmtsForWrap { stmtList.outsideWrapperPrefix = append(stmtList.outsideWrapperPrefix, stmt) continue } break } // "export * from 'path'" if !shouldStripExports { break } record := &repr.AST.ImportRecords[s.ImportRecordIndex] // Is this export star evaluated at run time? if !record.SourceIndex.IsValid() && c.options.OutputFormat.KeepES6ImportExportSyntax() { if record.CallsRunTimeReExportFn { // Turn this statement into "import * as ns from 'path'" stmt.Data = &js_ast.SImport{ NamespaceRef: s.NamespaceRef, StarNameLoc: &stmt.Loc, ImportRecordIndex: s.ImportRecordIndex, } // Prefix this module with "__reExport(exports, ns)" exportStarRef := c.graph.Files[runtime.SourceIndex].InputFile.Repr.(*graph.JSRepr).AST.ModuleScope.Members["__reExport"].Ref stmtList.insideWrapperPrefix = append(stmtList.insideWrapperPrefix, js_ast.Stmt{ Loc: stmt.Loc, Data: &js_ast.SExpr{Value: js_ast.Expr{Loc: stmt.Loc, Data: &js_ast.ECall{ Target: js_ast.Expr{Loc: stmt.Loc, Data: &js_ast.EIdentifier{Ref: exportStarRef}}, Args: []js_ast.Expr{ {Loc: stmt.Loc, Data: &js_ast.EIdentifier{Ref: repr.AST.ExportsRef}}, {Loc: stmt.Loc, Data: &js_ast.EIdentifier{Ref: s.NamespaceRef}}, }, }}}, }) // Make sure these don't end up in the wrapper closure if shouldExtractESMStmtsForWrap { stmtList.outsideWrapperPrefix = append(stmtList.outsideWrapperPrefix, stmt) continue } } } else { if record.SourceIndex.IsValid() { if otherRepr := c.graph.Files[record.SourceIndex.GetIndex()].InputFile.Repr.(*graph.JSRepr); otherRepr.Meta.Wrap == graph.WrapESM { stmtList.insideWrapperPrefix = append(stmtList.insideWrapperPrefix, js_ast.Stmt{Loc: stmt.Loc, Data: &js_ast.SExpr{Value: js_ast.Expr{Loc: stmt.Loc, Data: &js_ast.ECall{ Target: js_ast.Expr{Loc: stmt.Loc, Data: &js_ast.EIdentifier{Ref: otherRepr.AST.WrapperRef}}}}}}) } } if record.CallsRunTimeReExportFn { var target js_ast.E if record.SourceIndex.IsValid() { if otherRepr := c.graph.Files[record.SourceIndex.GetIndex()].InputFile.Repr.(*graph.JSRepr); otherRepr.AST.ExportsKind == js_ast.ExportsESMWithDynamicFallback { // Prefix this module with "__reExport(exports, otherExports)" target = &js_ast.EIdentifier{Ref: otherRepr.AST.ExportsRef} } } if target == nil { // Prefix this module with "__reExport(exports, require(path))" target = &js_ast.ERequire{ImportRecordIndex: s.ImportRecordIndex} } exportStarRef := c.graph.Files[runtime.SourceIndex].InputFile.Repr.(*graph.JSRepr).AST.ModuleScope.Members["__reExport"].Ref stmtList.insideWrapperPrefix = append(stmtList.insideWrapperPrefix, js_ast.Stmt{ Loc: stmt.Loc, Data: &js_ast.SExpr{Value: js_ast.Expr{Loc: stmt.Loc, Data: &js_ast.ECall{ Target: js_ast.Expr{Loc: stmt.Loc, Data: &js_ast.EIdentifier{Ref: exportStarRef}}, Args: []js_ast.Expr{ {Loc: stmt.Loc, Data: &js_ast.EIdentifier{Ref: repr.AST.ExportsRef}}, {Loc: record.Range.Loc, Data: target}, }, }}}, }) } // Remove the export star statement continue } case *js_ast.SExportFrom: // "export {foo} from 'path'" if c.shouldRemoveImportExportStmt(sourceIndex, stmtList, stmt.Loc, s.NamespaceRef, s.ImportRecordIndex) { continue } if shouldStripExports { // Turn this statement into "import {foo} from 'path'" for i, item := range s.Items { s.Items[i].Alias = item.OriginalName } stmt.Data = &js_ast.SImport{ NamespaceRef: s.NamespaceRef, Items: &s.Items, ImportRecordIndex: s.ImportRecordIndex, IsSingleLine: s.IsSingleLine, } } // Make sure these don't end up in the wrapper closure if shouldExtractESMStmtsForWrap { stmtList.outsideWrapperPrefix = append(stmtList.outsideWrapperPrefix, stmt) continue } case *js_ast.SExportClause: if shouldStripExports { // Remove export statements entirely continue } // Make sure these don't end up in the wrapper closure if shouldExtractESMStmtsForWrap { stmtList.outsideWrapperPrefix = append(stmtList.outsideWrapperPrefix, stmt) continue } case *js_ast.SFunction: // Strip the "export" keyword while bundling if shouldStripExports && s.IsExport { // Be careful to not modify the original statement clone := *s clone.IsExport = false stmt.Data = &clone } case *js_ast.SClass: if shouldStripExports && s.IsExport { // Be careful to not modify the original statement clone := *s clone.IsExport = false stmt.Data = &clone } case *js_ast.SLocal: if shouldStripExports && s.IsExport { // Be careful to not modify the original statement clone := *s clone.IsExport = false stmt.Data = &clone } case *js_ast.SExportDefault: // If we're bundling, convert "export default" into a normal declaration if shouldStripExports { if s.Value.Expr != nil { // "export default foo;" => "var default = foo;" stmt = js_ast.Stmt{Loc: stmt.Loc, Data: &js_ast.SLocal{Decls: []js_ast.Decl{ {Binding: js_ast.Binding{Loc: s.DefaultName.Loc, Data: &js_ast.BIdentifier{Ref: s.DefaultName.Ref}}, Value: s.Value.Expr}, }}} } else { switch s2 := s.Value.Stmt.Data.(type) { case *js_ast.SFunction: // "export default function() {}" => "function default() {}" // "export default function foo() {}" => "function foo() {}" // Be careful to not modify the original statement s2 = &js_ast.SFunction{Fn: s2.Fn} s2.Fn.Name = &s.DefaultName stmt = js_ast.Stmt{Loc: s.Value.Stmt.Loc, Data: s2} case *js_ast.SClass: // "export default class {}" => "class default {}" // "export default class Foo {}" => "class Foo {}" // Be careful to not modify the original statement s2 = &js_ast.SClass{Class: s2.Class} s2.Class.Name = &s.DefaultName stmt = js_ast.Stmt{Loc: s.Value.Stmt.Loc, Data: s2} default: panic("Internal error") } } } } stmtList.insideWrapperSuffix = append(stmtList.insideWrapperSuffix, stmt) } } // "var a = 1; var b = 2;" => "var a = 1, b = 2;" func mergeAdjacentLocalStmts(stmts []js_ast.Stmt) []js_ast.Stmt { if len(stmts) == 0 { return stmts } didMergeWithPreviousLocal := false end := 1 for _, stmt := range stmts[1:] { // Try to merge with the previous variable statement if after, ok := stmt.Data.(*js_ast.SLocal); ok { if before, ok := stmts[end-1].Data.(*js_ast.SLocal); ok { // It must be the same kind of variable statement (i.e. let/var/const) if before.Kind == after.Kind && before.IsExport == after.IsExport { if didMergeWithPreviousLocal { // Avoid O(n^2) behavior for repeated variable declarations before.Decls = append(before.Decls, after.Decls...) } else { // Be careful to not modify the original statement didMergeWithPreviousLocal = true clone := *before clone.Decls = make([]js_ast.Decl, 0, len(before.Decls)+len(after.Decls)) clone.Decls = append(clone.Decls, before.Decls...) clone.Decls = append(clone.Decls, after.Decls...) stmts[end-1].Data = &clone } continue } } } // Otherwise, append a normal statement didMergeWithPreviousLocal = false stmts[end] = stmt end++ } return stmts[:end] } type stmtList struct { // These statements come first, and can be inside the wrapper insideWrapperPrefix []js_ast.Stmt // These statements come last, and can be inside the wrapper insideWrapperSuffix []js_ast.Stmt outsideWrapperPrefix []js_ast.Stmt } type compileResultJS struct { js_printer.PrintResult sourceIndex uint32 // This is the line and column offset since the previous JavaScript string // or the start of the file if this is the first JavaScript string. generatedOffset sourcemap.LineColumnOffset } func (c *linkerContext) requireOrImportMetaForSource(sourceIndex uint32) (meta js_printer.RequireOrImportMeta) { repr := c.graph.Files[sourceIndex].InputFile.Repr.(*graph.JSRepr) meta.WrapperRef = repr.AST.WrapperRef meta.IsWrapperAsync = repr.Meta.IsAsyncOrHasAsyncDependency if repr.Meta.Wrap == graph.WrapESM { meta.ExportsRef = repr.AST.ExportsRef } else { meta.ExportsRef = js_ast.InvalidRef } return } func (c *linkerContext) generateCodeForFileInChunkJS( r renamer.Renamer, waitGroup *sync.WaitGroup, partRange partRange, entryBits helpers.BitSet, chunkAbsDir string, commonJSRef js_ast.Ref, esmRef js_ast.Ref, toModuleRef js_ast.Ref, result *compileResultJS, dataForSourceMaps []dataForSourceMap, ) { file := &c.graph.Files[partRange.sourceIndex] repr := file.InputFile.Repr.(*graph.JSRepr) nsExportPartIndex := repr.Meta.NSExportPartIndex needsWrapper := false stmtList := stmtList{} // Make sure the generated call to "__export(exports, ...)" comes first // before anything else. if nsExportPartIndex >= partRange.partIndexBegin && nsExportPartIndex < partRange.partIndexEnd && repr.AST.Parts[nsExportPartIndex].IsLive { c.convertStmtsForChunk(partRange.sourceIndex, &stmtList, repr.AST.Parts[nsExportPartIndex].Stmts) // Move everything to the prefix list if repr.Meta.Wrap == graph.WrapESM { stmtList.outsideWrapperPrefix = append(stmtList.outsideWrapperPrefix, stmtList.insideWrapperSuffix...) } else { stmtList.insideWrapperPrefix = append(stmtList.insideWrapperPrefix, stmtList.insideWrapperSuffix...) } stmtList.insideWrapperSuffix = nil } // Add all other parts in this chunk for partIndex := partRange.partIndexBegin; partIndex < partRange.partIndexEnd; partIndex++ { part := repr.AST.Parts[partIndex] if !repr.AST.Parts[partIndex].IsLive { // Skip the part if it's not in this chunk continue } if uint32(partIndex) == nsExportPartIndex { // Skip the generated call to "__export()" that was extracted above continue } // Mark if we hit the dummy part representing the wrapper if uint32(partIndex) == repr.Meta.WrapperPartIndex.GetIndex() { needsWrapper = true continue } c.convertStmtsForChunk(partRange.sourceIndex, &stmtList, part.Stmts) } // Hoist all import statements before any normal statements. ES6 imports // are different than CommonJS imports. All modules imported via ES6 import // statements are evaluated before the module doing the importing is // evaluated (well, except for cyclic import scenarios). We need to preserve // these semantics even when modules imported via ES6 import statements end // up being CommonJS modules. stmts := stmtList.insideWrapperSuffix if len(stmtList.insideWrapperPrefix) > 0 { stmts = append(stmtList.insideWrapperPrefix, stmts...) } if c.options.MangleSyntax { stmts = mergeAdjacentLocalStmts(stmts) } // Optionally wrap all statements in a closure if needsWrapper { switch repr.Meta.Wrap { case graph.WrapCJS: // Only include the arguments that are actually used args := []js_ast.Arg{} if repr.AST.UsesExportsRef || repr.AST.UsesModuleRef { args = append(args, js_ast.Arg{Binding: js_ast.Binding{Data: &js_ast.BIdentifier{Ref: repr.AST.ExportsRef}}}) if repr.AST.UsesModuleRef { args = append(args, js_ast.Arg{Binding: js_ast.Binding{Data: &js_ast.BIdentifier{Ref: repr.AST.ModuleRef}}}) } } // "__commonJS((exports, module) => { ... })" var value js_ast.Expr if c.options.UnsupportedJSFeatures.Has(compat.Arrow) { value = js_ast.Expr{Data: &js_ast.ECall{ Target: js_ast.Expr{Data: &js_ast.EIdentifier{Ref: commonJSRef}}, Args: []js_ast.Expr{{Data: &js_ast.EFunction{Fn: js_ast.Fn{Args: args, Body: js_ast.FnBody{Stmts: stmts}}}}}, }} } else { value = js_ast.Expr{Data: &js_ast.ECall{ Target: js_ast.Expr{Data: &js_ast.EIdentifier{Ref: commonJSRef}}, Args: []js_ast.Expr{{Data: &js_ast.EArrow{Args: args, Body: js_ast.FnBody{Stmts: stmts}}}}, }} } // "var require_foo = __commonJS((exports, module) => { ... });" stmts = append(stmtList.outsideWrapperPrefix, js_ast.Stmt{Data: &js_ast.SLocal{ Decls: []js_ast.Decl{{ Binding: js_ast.Binding{Data: &js_ast.BIdentifier{Ref: repr.AST.WrapperRef}}, Value: &value, }}, }}) case graph.WrapESM: // The wrapper only needs to be "async" if there is a transitive async // dependency. For correctness, we must not use "async" if the module // isn't async because then calling "require()" on that module would // swallow any exceptions thrown during module initialization. isAsync := repr.Meta.IsAsyncOrHasAsyncDependency // Hoist all top-level "var" and "function" declarations out of the closure var decls []js_ast.Decl end := 0 for _, stmt := range stmts { switch s := stmt.Data.(type) { case *js_ast.SLocal: // Convert the declarations to assignments wrapIdentifier := func(loc logger.Loc, ref js_ast.Ref) js_ast.Expr { decls = append(decls, js_ast.Decl{Binding: js_ast.Binding{Loc: loc, Data: &js_ast.BIdentifier{Ref: ref}}}) return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: ref}} } var value js_ast.Expr for _, decl := range s.Decls { binding := js_ast.ConvertBindingToExpr(decl.Binding, wrapIdentifier) if decl.Value != nil { value = js_ast.JoinWithComma(value, js_ast.Assign(binding, *decl.Value)) } } if value.Data == nil { continue } stmt = js_ast.Stmt{Loc: stmt.Loc, Data: &js_ast.SExpr{Value: value}} case *js_ast.SFunction: stmtList.outsideWrapperPrefix = append(stmtList.outsideWrapperPrefix, stmt) continue } stmts[end] = stmt end++ } stmts = stmts[:end] // "__esm(() => { ... })" var value js_ast.Expr if c.options.UnsupportedJSFeatures.Has(compat.Arrow) { value = js_ast.Expr{Data: &js_ast.ECall{ Target: js_ast.Expr{Data: &js_ast.EIdentifier{Ref: esmRef}}, Args: []js_ast.Expr{{Data: &js_ast.EFunction{Fn: js_ast.Fn{Body: js_ast.FnBody{Stmts: stmts}, IsAsync: isAsync}}}}, }} } else { value = js_ast.Expr{Data: &js_ast.ECall{ Target: js_ast.Expr{Data: &js_ast.EIdentifier{Ref: esmRef}}, Args: []js_ast.Expr{{Data: &js_ast.EArrow{Body: js_ast.FnBody{Stmts: stmts}, IsAsync: isAsync}}}, }} } // "var foo, bar;" if !c.options.MangleSyntax && len(decls) > 0 { stmtList.outsideWrapperPrefix = append(stmtList.outsideWrapperPrefix, js_ast.Stmt{Data: &js_ast.SLocal{ Decls: decls, }}) decls = nil } // "var init_foo = __esm(() => { ... });" stmts = append(stmtList.outsideWrapperPrefix, js_ast.Stmt{Data: &js_ast.SLocal{ Decls: append(decls, js_ast.Decl{ Binding: js_ast.Binding{Data: &js_ast.BIdentifier{Ref: repr.AST.WrapperRef}}, Value: &value, }), }}) } } // Only generate a source map if needed var addSourceMappings bool var inputSourceMap *sourcemap.SourceMap var lineOffsetTables []js_printer.LineOffsetTable if file.InputFile.Loader.CanHaveSourceMap() && c.options.SourceMap != config.SourceMapNone { addSourceMappings = true inputSourceMap = file.InputFile.InputSourceMap lineOffsetTables = dataForSourceMaps[partRange.sourceIndex].lineOffsetTables } // Indent the file if everything is wrapped in an IIFE indent := 0 if c.options.OutputFormat == config.FormatIIFE { indent++ } // Convert the AST to JavaScript code printOptions := js_printer.Options{ Indent: indent, OutputFormat: c.options.OutputFormat, RemoveWhitespace: c.options.RemoveWhitespace, MangleSyntax: c.options.MangleSyntax, ASCIIOnly: c.options.ASCIIOnly, ToModuleRef: toModuleRef, ExtractComments: c.options.Mode == config.ModeBundle && c.options.RemoveWhitespace, UnsupportedFeatures: c.options.UnsupportedJSFeatures, AddSourceMappings: addSourceMappings, InputSourceMap: inputSourceMap, LineOffsetTables: lineOffsetTables, RequireOrImportMetaForSource: c.requireOrImportMetaForSource, } tree := repr.AST tree.Directive = "" // This is handled elsewhere tree.Parts = []js_ast.Part{{Stmts: stmts}} *result = compileResultJS{ PrintResult: js_printer.Print(tree, c.graph.Symbols, r, printOptions), sourceIndex: partRange.sourceIndex, } waitGroup.Done() } func (c *linkerContext) generateEntryPointTailJS( r renamer.Renamer, toModuleRef js_ast.Ref, sourceIndex uint32, ) (result compileResultJS) { file := &c.graph.Files[sourceIndex] repr := file.InputFile.Repr.(*graph.JSRepr) var stmts []js_ast.Stmt switch c.options.OutputFormat { case config.FormatPreserve: if repr.Meta.Wrap != graph.WrapNone { // "require_foo();" // "init_foo();" stmts = append(stmts, js_ast.Stmt{Data: &js_ast.SExpr{Value: js_ast.Expr{Data: &js_ast.ECall{ Target: js_ast.Expr{Data: &js_ast.EIdentifier{Ref: repr.AST.WrapperRef}}, }}}}) } case config.FormatIIFE: if repr.Meta.Wrap == graph.WrapCJS { if len(c.options.GlobalName) > 0 { // "return require_foo();" stmts = append(stmts, js_ast.Stmt{Data: &js_ast.SReturn{Value: &js_ast.Expr{Data: &js_ast.ECall{ Target: js_ast.Expr{Data: &js_ast.EIdentifier{Ref: repr.AST.WrapperRef}}, }}}}) } else { // "require_foo();" stmts = append(stmts, js_ast.Stmt{Data: &js_ast.SExpr{Value: js_ast.Expr{Data: &js_ast.ECall{ Target: js_ast.Expr{Data: &js_ast.EIdentifier{Ref: repr.AST.WrapperRef}}, }}}}) } } else { if repr.Meta.Wrap == graph.WrapESM { // "init_foo();" stmts = append(stmts, js_ast.Stmt{Data: &js_ast.SExpr{Value: js_ast.Expr{Data: &js_ast.ECall{ Target: js_ast.Expr{Data: &js_ast.EIdentifier{Ref: repr.AST.WrapperRef}}, }}}}) } if repr.Meta.ForceIncludeExportsForEntryPoint && len(c.options.GlobalName) > 0 { // "return exports;" stmts = append(stmts, js_ast.Stmt{Data: &js_ast.SReturn{ Value: &js_ast.Expr{Data: &js_ast.EIdentifier{Ref: repr.AST.ExportsRef}}, }}) } } case config.FormatCommonJS: if repr.Meta.Wrap == graph.WrapCJS { // "module.exports = require_foo();" stmts = append(stmts, js_ast.AssignStmt( js_ast.Expr{Data: &js_ast.EDot{ Target: js_ast.Expr{Data: &js_ast.EIdentifier{Ref: c.unboundModuleRef}}, Name: "exports", }}, js_ast.Expr{Data: &js_ast.ECall{ Target: js_ast.Expr{Data: &js_ast.EIdentifier{Ref: repr.AST.WrapperRef}}, }}, )) } else if repr.Meta.Wrap == graph.WrapESM { // "init_foo();" stmts = append(stmts, js_ast.Stmt{Data: &js_ast.SExpr{Value: js_ast.Expr{Data: &js_ast.ECall{ Target: js_ast.Expr{Data: &js_ast.EIdentifier{Ref: repr.AST.WrapperRef}}, }}}}) } // If we are generating CommonJS for node, encode the known export names in // a form that node can understand them. This relies on the specific behavior // of this parser, which the node project uses to detect named exports in // CommonJS files: https://github.com/guybedford/cjs-module-lexer. Think of // this code as an annotation for that parser. if c.options.Platform == config.PlatformNode && len(repr.Meta.SortedAndFilteredExportAliases) > 0 { // Add a comment since otherwise people will surely wonder what this is. // This annotation means you can do this and have it work: // // import { name } from './file-from-esbuild.cjs' // // when "file-from-esbuild.cjs" looks like this: // // __export(exports, { name: () => name }); // 0 && (module.exports = {name}); // // The maintainer of "cjs-module-lexer" is receptive to adding esbuild- // friendly patterns to this library. However, this library has already // shipped in node and using existing patterns instead of defining new // patterns is maximally compatible. // // An alternative to doing this could be to use "Object.defineProperties" // instead of "__export" but support for that would need to be added to // "cjs-module-lexer" and then we would need to be ok with not supporting // older versions of node that don't have that newly-added support. if !c.options.RemoveWhitespace { stmts = append(stmts, js_ast.Stmt{Data: &js_ast.SComment{Text: `// Annotate the CommonJS export names for ESM import in node:`}}, ) } // "{a, b, if: null}" var moduleExports []js_ast.Property for _, export := range repr.Meta.SortedAndFilteredExportAliases { if export == "default" { // In node the default export is always "module.exports" regardless of // what the annotation says. So don't bother generating "default". continue } // "{if: null}" var value *js_ast.Expr if _, ok := js_lexer.Keywords[export]; ok { // Make sure keywords don't cause a syntax error. This has to map to // "null" instead of something shorter like "0" because the library // "cjs-module-lexer" only supports identifiers in this position, and // it thinks "null" is an identifier. value = &js_ast.Expr{Data: &js_ast.ENull{}} } moduleExports = append(moduleExports, js_ast.Property{ Key: js_ast.Expr{Data: &js_ast.EString{Value: js_lexer.StringToUTF16(export)}}, Value: value, }) } // "0 && (module.exports = {a, b, if: null});" expr := js_ast.Expr{Data: &js_ast.EBinary{ Op: js_ast.BinOpLogicalAnd, Left: js_ast.Expr{Data: &js_ast.ENumber{Value: 0}}, Right: js_ast.Assign( js_ast.Expr{Data: &js_ast.EDot{ Target: js_ast.Expr{Data: &js_ast.EIdentifier{Ref: repr.AST.ModuleRef}}, Name: "exports", }}, js_ast.Expr{Data: &js_ast.EObject{Properties: moduleExports}}, ), }} stmts = append(stmts, js_ast.Stmt{Data: &js_ast.SExpr{Value: expr}}) } case config.FormatESModule: if repr.Meta.Wrap == graph.WrapCJS { // "export default require_foo();" stmts = append(stmts, js_ast.Stmt{ Data: &js_ast.SExportDefault{Value: js_ast.ExprOrStmt{Expr: &js_ast.Expr{ Data: &js_ast.ECall{Target: js_ast.Expr{ Data: &js_ast.EIdentifier{Ref: repr.AST.WrapperRef}}}}}}}) } else { if repr.Meta.Wrap == graph.WrapESM { if repr.Meta.IsAsyncOrHasAsyncDependency { // "await init_foo();" stmts = append(stmts, js_ast.Stmt{ Data: &js_ast.SExpr{Value: js_ast.Expr{ Data: &js_ast.EAwait{Value: js_ast.Expr{ Data: &js_ast.ECall{Target: js_ast.Expr{ Data: &js_ast.EIdentifier{Ref: repr.AST.WrapperRef}}}}}}}}) } else { // "init_foo();" stmts = append(stmts, js_ast.Stmt{ Data: &js_ast.SExpr{ Value: js_ast.Expr{Data: &js_ast.ECall{Target: js_ast.Expr{ Data: &js_ast.EIdentifier{Ref: repr.AST.WrapperRef}}}}}}) } } if len(repr.Meta.SortedAndFilteredExportAliases) > 0 { // If the output format is ES6 modules and we're an entry point, generate an // ES6 export statement containing all exports. Except don't do that if this // entry point is a CommonJS-style module, since that would generate an ES6 // export statement that's not top-level. Instead, we will export the CommonJS // exports as a default export later on. var items []js_ast.ClauseItem for i, alias := range repr.Meta.SortedAndFilteredExportAliases { export := repr.Meta.ResolvedExports[alias] // If this is an export of an import, reference the symbol that the import // was eventually resolved to. We need to do this because imports have // already been resolved by this point, so we can't generate a new import // and have that be resolved later. if importData, ok := c.graph.Files[export.SourceIndex].InputFile.Repr.(*graph.JSRepr).Meta.ImportsToBind[export.Ref]; ok { export.Ref = importData.Ref export.SourceIndex = importData.SourceIndex } // Exports of imports need EImportIdentifier in case they need to be re- // written to a property access later on if c.graph.Symbols.Get(export.Ref).NamespaceAlias != nil { // Create both a local variable and an export clause for that variable. // The local variable is initialized with the initial value of the // export. This isn't fully correct because it's a "dead" binding and // doesn't update with the "live" value as it changes. But ES6 modules // don't have any syntax for bare named getter functions so this is the // best we can do. // // These input files: // // // entry_point.js // export {foo} from './cjs-format.js' // // // cjs-format.js // Object.defineProperty(exports, 'foo', { // enumerable: true, // get: () => Math.random(), // }) // // Become this output file: // // // cjs-format.js // var require_cjs_format = __commonJS((exports) => { // Object.defineProperty(exports, "foo", { // enumerable: true, // get: () => Math.random() // }); // }); // // // entry_point.js // var cjs_format = __toModule(require_cjs_format()); // var export_foo = cjs_format.foo; // export { // export_foo as foo // }; // tempRef := repr.Meta.CJSExportCopies[i] stmts = append(stmts, js_ast.Stmt{Data: &js_ast.SLocal{ Decls: []js_ast.Decl{{ Binding: js_ast.Binding{Data: &js_ast.BIdentifier{Ref: tempRef}}, Value: &js_ast.Expr{Data: &js_ast.EImportIdentifier{Ref: export.Ref}}, }}, }}) items = append(items, js_ast.ClauseItem{ Name: js_ast.LocRef{Ref: tempRef}, Alias: alias, }) } else { // Local identifiers can be exported using an export clause. This is done // this way instead of leaving the "export" keyword on the local declaration // itself both because it lets the local identifier be minified and because // it works transparently for re-exports across files. // // These input files: // // // entry_point.js // export * from './esm-format.js' // // // esm-format.js // export let foo = 123 // // Become this output file: // // // esm-format.js // let foo = 123; // // // entry_point.js // export { // foo // }; // items = append(items, js_ast.ClauseItem{ Name: js_ast.LocRef{Ref: export.Ref}, Alias: alias, }) } } stmts = append(stmts, js_ast.Stmt{Data: &js_ast.SExportClause{Items: items}}) } } } if len(stmts) == 0 { return } tree := repr.AST tree.Parts = []js_ast.Part{{Stmts: stmts}} // Indent the file if everything is wrapped in an IIFE indent := 0 if c.options.OutputFormat == config.FormatIIFE { indent++ } // Convert the AST to JavaScript code printOptions := js_printer.Options{ Indent: indent, OutputFormat: c.options.OutputFormat, RemoveWhitespace: c.options.RemoveWhitespace, MangleSyntax: c.options.MangleSyntax, ASCIIOnly: c.options.ASCIIOnly, ToModuleRef: toModuleRef, ExtractComments: c.options.Mode == config.ModeBundle && c.options.RemoveWhitespace, UnsupportedFeatures: c.options.UnsupportedJSFeatures, RequireOrImportMetaForSource: c.requireOrImportMetaForSource, } result.PrintResult = js_printer.Print(tree, c.graph.Symbols, r, printOptions) return } func (c *linkerContext) renameSymbolsInChunk(chunk *chunkInfo, filesInOrder []uint32) renamer.Renamer { // Determine the reserved names (e.g. can't generate the name "if") moduleScopes := make([]*js_ast.Scope, len(filesInOrder)) for i, sourceIndex := range filesInOrder { moduleScopes[i] = c.graph.Files[sourceIndex].InputFile.Repr.(*graph.JSRepr).AST.ModuleScope } reservedNames := renamer.ComputeReservedNames(moduleScopes, c.graph.Symbols) // These are used to implement bundling, and need to be free for use if c.options.Mode != config.ModePassThrough { reservedNames["require"] = 1 reservedNames["Promise"] = 1 } // Minification uses frequency analysis to give shorter names to more frequent symbols if c.options.MinifyIdentifiers { // Determine the first top-level slot (i.e. not in a nested scope) var firstTopLevelSlots js_ast.SlotCounts for _, sourceIndex := range filesInOrder { firstTopLevelSlots.UnionMax(c.graph.Files[sourceIndex].InputFile.Repr.(*graph.JSRepr).AST.NestedScopeSlotCounts) } r := renamer.NewMinifyRenamer(c.graph.Symbols, firstTopLevelSlots, reservedNames) // Accumulate symbol usage counts into their slots freq := js_ast.CharFreq{} for _, sourceIndex := range filesInOrder { repr := c.graph.Files[sourceIndex].InputFile.Repr.(*graph.JSRepr) if repr.AST.CharFreq != nil { freq.Include(repr.AST.CharFreq) } if repr.AST.UsesExportsRef { r.AccumulateSymbolCount(repr.AST.ExportsRef, 1) } if repr.AST.UsesModuleRef { r.AccumulateSymbolCount(repr.AST.ModuleRef, 1) } for partIndex, part := range repr.AST.Parts { if !repr.AST.Parts[partIndex].IsLive { // Skip the part if it's not in this chunk continue } // Accumulate symbol use counts r.AccumulateSymbolUseCounts(part.SymbolUses, c.graph.StableSourceIndices) // Make sure to also count the declaration in addition to the uses for _, declared := range part.DeclaredSymbols { r.AccumulateSymbolCount(declared.Ref, 1) } } } // Add all of the character frequency histograms for all files in this // chunk together, then use it to compute the character sequence used to // generate minified names. This results in slightly better gzip compression // over assigning minified names in order (i.e. "a b c ..."). Even though // it's a very small win, we still do it because it's simple to do and very // cheap to compute. minifier := freq.Compile() r.AssignNamesByFrequency(&minifier) return r } // When we're not minifying, just append numbers to symbol names to avoid collisions r := renamer.NewNumberRenamer(c.graph.Symbols, reservedNames) nestedScopes := make(map[uint32][]*js_ast.Scope) // Make sure imports get a chance to be renamed var sorted renamer.StableRefArray for _, imports := range chunk.chunkRepr.(*chunkReprJS).importsFromOtherChunks { for _, item := range imports { sorted = append(sorted, renamer.StableRef{ StableSourceIndex: c.graph.StableSourceIndices[item.ref.SourceIndex], Ref: item.ref, }) } } sort.Sort(sorted) for _, stable := range sorted { r.AddTopLevelSymbol(stable.Ref) } for _, sourceIndex := range filesInOrder { repr := c.graph.Files[sourceIndex].InputFile.Repr.(*graph.JSRepr) var scopes []*js_ast.Scope // Modules wrapped in a CommonJS closure look like this: // // // foo.js // var require_foo = __commonJS((exports, module) => { // exports.foo = 123; // }); // // The symbol "require_foo" is stored in "file.ast.WrapperRef". We want // to be able to minify everything inside the closure without worrying // about collisions with other CommonJS modules. Set up the scopes such // that it appears as if the file was structured this way all along. It's // not completely accurate (e.g. we don't set the parent of the module // scope to this new top-level scope) but it's good enough for the // renaming code. if repr.Meta.Wrap == graph.WrapCJS { r.AddTopLevelSymbol(repr.AST.WrapperRef) // External import statements will be hoisted outside of the CommonJS // wrapper if the output format supports import statements. We need to // add those symbols to the top-level scope to avoid causing name // collisions. This code special-cases only those symbols. if c.options.OutputFormat.KeepES6ImportExportSyntax() { for _, part := range repr.AST.Parts { for _, stmt := range part.Stmts { switch s := stmt.Data.(type) { case *js_ast.SImport: if !repr.AST.ImportRecords[s.ImportRecordIndex].SourceIndex.IsValid() { r.AddTopLevelSymbol(s.NamespaceRef) if s.DefaultName != nil { r.AddTopLevelSymbol(s.DefaultName.Ref) } if s.Items != nil { for _, item := range *s.Items { r.AddTopLevelSymbol(item.Name.Ref) } } } case *js_ast.SExportStar: if !repr.AST.ImportRecords[s.ImportRecordIndex].SourceIndex.IsValid() { r.AddTopLevelSymbol(s.NamespaceRef) } case *js_ast.SExportFrom: if !repr.AST.ImportRecords[s.ImportRecordIndex].SourceIndex.IsValid() { r.AddTopLevelSymbol(s.NamespaceRef) for _, item := range s.Items { r.AddTopLevelSymbol(item.Name.Ref) } } } } } } nestedScopes[sourceIndex] = []*js_ast.Scope{repr.AST.ModuleScope} continue } // Modules wrapped in an ESM closure look like this: // // // foo.js // var foo, foo_exports = {}; // __exports(foo_exports, { // foo: () => foo // }); // let init_foo = __esm(() => { // foo = 123; // }); // // The symbol "init_foo" is stored in "file.ast.WrapperRef". We need to // minify everything inside the closure without introducing a new scope // since all top-level variables will be hoisted outside of the closure. if repr.Meta.Wrap == graph.WrapESM { r.AddTopLevelSymbol(repr.AST.WrapperRef) } // Rename each top-level symbol declaration in this chunk for partIndex, part := range repr.AST.Parts { if repr.AST.Parts[partIndex].IsLive { for _, declared := range part.DeclaredSymbols { if declared.IsTopLevel { r.AddTopLevelSymbol(declared.Ref) } } scopes = append(scopes, part.Scopes...) } } nestedScopes[sourceIndex] = scopes } // Recursively rename symbols in child scopes now that all top-level // symbols have been renamed. This is done in parallel because the symbols // inside nested scopes are independent and can't conflict. r.AssignNamesByScope(nestedScopes) return r } func (c *linkerContext) generateChunkJS(chunks []chunkInfo, chunkIndex int, chunkWaitGroup *sync.WaitGroup) { chunk := &chunks[chunkIndex] chunkRepr := chunk.chunkRepr.(*chunkReprJS) compileResults := make([]compileResultJS, 0, len(chunk.partsInChunkInOrder)) runtimeMembers := c.graph.Files[runtime.SourceIndex].InputFile.Repr.(*graph.JSRepr).AST.ModuleScope.Members commonJSRef := js_ast.FollowSymbols(c.graph.Symbols, runtimeMembers["__commonJS"].Ref) esmRef := js_ast.FollowSymbols(c.graph.Symbols, runtimeMembers["__esm"].Ref) toModuleRef := js_ast.FollowSymbols(c.graph.Symbols, runtimeMembers["__toModule"].Ref) r := c.renameSymbolsInChunk(chunk, chunk.filesInChunkInOrder) dataForSourceMaps := c.dataForSourceMaps() // Note: This contains placeholders instead of what the placeholders are // substituted with. That should be fine though because this should only // ever be used for figuring out how many "../" to add to a relative path // from a chunk whose final path hasn't been calculated yet to a chunk // whose final path has already been calculated. That and placeholders are // never substituted with something containing a "/" so substitution should // never change the "../" count. chunkAbsDir := c.fs.Dir(c.fs.Join(c.options.AbsOutputDir, config.TemplateToString(chunk.finalTemplate))) // Generate JavaScript for each file in parallel waitGroup := sync.WaitGroup{} for _, partRange := range chunk.partsInChunkInOrder { // Skip the runtime in test output if partRange.sourceIndex == runtime.SourceIndex && c.options.OmitRuntimeForTests { continue } // Create a goroutine for this file compileResults = append(compileResults, compileResultJS{}) compileResult := &compileResults[len(compileResults)-1] waitGroup.Add(1) go c.generateCodeForFileInChunkJS( r, &waitGroup, partRange, chunk.entryBits, chunkAbsDir, commonJSRef, esmRef, toModuleRef, compileResult, dataForSourceMaps, ) } // Also generate the cross-chunk binding code var crossChunkPrefix []byte var crossChunkSuffix []byte { // Indent the file if everything is wrapped in an IIFE indent := 0 if c.options.OutputFormat == config.FormatIIFE { indent++ } printOptions := js_printer.Options{ Indent: indent, OutputFormat: c.options.OutputFormat, RemoveWhitespace: c.options.RemoveWhitespace, MangleSyntax: c.options.MangleSyntax, } crossChunkImportRecords := make([]ast.ImportRecord, len(chunk.crossChunkImports)) for i, chunkImport := range chunk.crossChunkImports { crossChunkImportRecords[i] = ast.ImportRecord{ Kind: chunkImport.importKind, Path: logger.Path{Text: chunks[chunkImport.chunkIndex].uniqueKey}, } } crossChunkPrefix = js_printer.Print(js_ast.AST{ ImportRecords: crossChunkImportRecords, Parts: []js_ast.Part{{Stmts: chunkRepr.crossChunkPrefixStmts}}, }, c.graph.Symbols, r, printOptions).JS crossChunkSuffix = js_printer.Print(js_ast.AST{ Parts: []js_ast.Part{{Stmts: chunkRepr.crossChunkSuffixStmts}}, }, c.graph.Symbols, r, printOptions).JS } // Generate the exports for the entry point, if there are any var entryPointTail compileResultJS if chunk.isEntryPoint { entryPointTail = c.generateEntryPointTailJS( r, toModuleRef, chunk.sourceIndex, ) } waitGroup.Wait() j := helpers.Joiner{} prevOffset := sourcemap.LineColumnOffset{} // Optionally strip whitespace indent := "" space := " " newline := "\n" if c.options.RemoveWhitespace { space = "" newline = "" } newlineBeforeComment := false isExecutable := false if chunk.isEntryPoint { repr := c.graph.Files[chunk.sourceIndex].InputFile.Repr.(*graph.JSRepr) // Start with the hashbang if there is one if repr.AST.Hashbang != "" { hashbang := repr.AST.Hashbang + "\n" prevOffset.AdvanceString(hashbang) j.AddString(hashbang) newlineBeforeComment = true isExecutable = true } // Add the top-level directive if present if repr.AST.Directive != "" { quoted := string(js_printer.QuoteForJSON(repr.AST.Directive, c.options.ASCIIOnly)) + ";" + newline prevOffset.AdvanceString(quoted) j.AddString(quoted) newlineBeforeComment = true } } if len(c.options.JSBanner) > 0 { prevOffset.AdvanceString(c.options.JSBanner) prevOffset.AdvanceString("\n") j.AddString(c.options.JSBanner) j.AddString("\n") } // Optionally wrap with an IIFE if c.options.OutputFormat == config.FormatIIFE { var text string indent = " " if len(c.options.GlobalName) > 0 { text = c.generateGlobalNamePrefix() } if c.options.UnsupportedJSFeatures.Has(compat.Arrow) { text += "(function()" + space + "{" + newline } else { text += "(()" + space + "=>" + space + "{" + newline } prevOffset.AdvanceString(text) j.AddString(text) newlineBeforeComment = false } // Put the cross-chunk prefix inside the IIFE if len(crossChunkPrefix) > 0 { newlineBeforeComment = true prevOffset.AdvanceBytes(crossChunkPrefix) j.AddBytes(crossChunkPrefix) } // Start the metadata jMeta := helpers.Joiner{} if c.options.NeedsMetafile { // Print imports isFirstMeta := true jMeta.AddString("{\n \"imports\": [") for _, chunkImport := range chunk.crossChunkImports { if isFirstMeta { isFirstMeta = false } else { jMeta.AddString(",") } jMeta.AddString(fmt.Sprintf("\n {\n \"path\": %s,\n \"kind\": %s\n }", js_printer.QuoteForJSON(c.res.PrettyPath(logger.Path{Text: chunks[chunkImport.chunkIndex].uniqueKey, Namespace: "file"}), c.options.ASCIIOnly), js_printer.QuoteForJSON(chunkImport.importKind.StringForMetafile(), c.options.ASCIIOnly))) } if !isFirstMeta { jMeta.AddString("\n ") } // Print exports jMeta.AddString("],\n \"exports\": [") var aliases []string if c.options.OutputFormat.KeepES6ImportExportSyntax() { if chunk.isEntryPoint { if fileRepr := c.graph.Files[chunk.sourceIndex].InputFile.Repr.(*graph.JSRepr); fileRepr.Meta.Wrap == graph.WrapCJS { aliases = []string{"default"} } else { resolvedExports := fileRepr.Meta.ResolvedExports aliases = make([]string, 0, len(resolvedExports)) for alias := range resolvedExports { aliases = append(aliases, alias) } } } else { aliases = make([]string, 0, len(chunkRepr.exportsToOtherChunks)) for _, alias := range chunkRepr.exportsToOtherChunks { aliases = append(aliases, alias) } } } isFirstMeta = true sort.Strings(aliases) // Sort for determinism for _, alias := range aliases { if isFirstMeta { isFirstMeta = false } else { jMeta.AddString(",") } jMeta.AddString(fmt.Sprintf("\n %s", js_printer.QuoteForJSON(alias, c.options.ASCIIOnly))) } if !isFirstMeta { jMeta.AddString("\n ") } if chunk.isEntryPoint { entryPoint := c.graph.Files[chunk.sourceIndex].InputFile.Source.PrettyPath jMeta.AddString(fmt.Sprintf("],\n \"entryPoint\": %s,\n \"inputs\": {", js_printer.QuoteForJSON(entryPoint, c.options.ASCIIOnly))) } else { jMeta.AddString("],\n \"inputs\": {") } } // Concatenate the generated JavaScript chunks together var compileResultsForSourceMap []compileResultJS var commentList []string var metaOrder []uint32 var metaByteCount map[string]int commentSet := make(map[string]bool) prevComment := uint32(0) if c.options.NeedsMetafile { metaOrder = make([]uint32, 0, len(compileResults)) metaByteCount = make(map[string]int, len(compileResults)) } for _, compileResult := range compileResults { isRuntime := compileResult.sourceIndex == runtime.SourceIndex for text := range compileResult.ExtractedComments { if !commentSet[text] { commentSet[text] = true commentList = append(commentList, text) } } // Add a comment with the file path before the file contents if c.options.Mode == config.ModeBundle && !c.options.RemoveWhitespace && prevComment != compileResult.sourceIndex && len(compileResult.JS) > 0 { if newlineBeforeComment { prevOffset.AdvanceString("\n") j.AddString("\n") } path := c.graph.Files[compileResult.sourceIndex].InputFile.Source.PrettyPath // Make sure newlines in the path can't cause a syntax error. This does // not minimize allocations because it's expected that this case never // comes up in practice. path = strings.ReplaceAll(path, "\r", "\\r") path = strings.ReplaceAll(path, "\n", "\\n") path = strings.ReplaceAll(path, "\u2028", "\\u2028") path = strings.ReplaceAll(path, "\u2029", "\\u2029") text := fmt.Sprintf("%s// %s\n", indent, path) prevOffset.AdvanceString(text) j.AddString(text) prevComment = compileResult.sourceIndex } // Don't include the runtime in source maps if isRuntime { prevOffset.AdvanceString(string(compileResult.JS)) j.AddBytes(compileResult.JS) } else { // Save the offset to the start of the stored JavaScript compileResult.generatedOffset = prevOffset j.AddBytes(compileResult.JS) // Ignore empty source map chunks if compileResult.SourceMapChunk.ShouldIgnore { prevOffset.AdvanceBytes(compileResult.JS) } else { prevOffset = sourcemap.LineColumnOffset{} // Include this file in the source map if c.options.SourceMap != config.SourceMapNone { compileResultsForSourceMap = append(compileResultsForSourceMap, compileResult) } } // Include this file in the metadata if c.options.NeedsMetafile { // Accumulate file sizes since a given file may be split into multiple parts path := c.graph.Files[compileResult.sourceIndex].InputFile.Source.PrettyPath if count, ok := metaByteCount[path]; ok { metaByteCount[path] = count + len(compileResult.JS) } else { metaOrder = append(metaOrder, compileResult.sourceIndex) metaByteCount[path] = len(compileResult.JS) } } } // Put a newline before the next file path comment if len(compileResult.JS) > 0 { newlineBeforeComment = true } } // Stick the entry point tail at the end of the file. Deliberately don't // include any source mapping information for this because it's automatically // generated and doesn't correspond to a location in the input file. j.AddBytes(entryPointTail.JS) // Put the cross-chunk suffix inside the IIFE if len(crossChunkSuffix) > 0 { if newlineBeforeComment { j.AddString(newline) } j.AddBytes(crossChunkSuffix) } // Optionally wrap with an IIFE if c.options.OutputFormat == config.FormatIIFE { j.AddString("})();" + newline) } // Make sure the file ends with a newline j.EnsureNewlineAtEnd() // Add all unique license comments to the end of the file. These are // deduplicated because some projects have thousands of files with the same // comment. The comment must be preserved in the output for legal reasons but // at the same time we want to generate a small bundle when minifying. sort.Strings(commentList) for _, text := range commentList { j.AddString(text) j.AddString("\n") } if len(c.options.JSFooter) > 0 { j.AddString(c.options.JSFooter) j.AddString("\n") } if c.options.SourceMap != config.SourceMapNone { chunk.outputSourceMap = c.generateSourceMapForChunk(compileResultsForSourceMap, chunkAbsDir, dataForSourceMaps) } // The JavaScript contents are done now that the source map comment is in jsContents := j.Done() // End the metadata lazily. The final output size is not known until the // final import paths are substituted into the output pieces generated below. if c.options.NeedsMetafile { chunk.jsonMetadataChunkCallback = func(finalOutputSize int) []byte { isFirstMeta := true for _, sourceIndex := range metaOrder { if isFirstMeta { isFirstMeta = false } else { jMeta.AddString(",") } path := c.graph.Files[sourceIndex].InputFile.Source.PrettyPath extra := c.generateExtraDataForFileJS(sourceIndex) jMeta.AddString(fmt.Sprintf("\n %s: {\n \"bytesInOutput\": %d\n %s}", js_printer.QuoteForJSON(path, c.options.ASCIIOnly), metaByteCount[path], extra)) } if !isFirstMeta { jMeta.AddString("\n ") } jMeta.AddString(fmt.Sprintf("},\n \"bytes\": %d\n }", finalOutputSize)) return jMeta.Done() } } chunk.outputPieces = c.breakOutputIntoPieces(jsContents, uint32(len(chunks))) c.generateIsolatedHashInParallel(chunk) chunk.isExecutable = isExecutable chunkWaitGroup.Done() } func (c *linkerContext) generateGlobalNamePrefix() string { var text string prefix := c.options.GlobalName[0] space := " " join := ";\n" if c.options.RemoveWhitespace { space = "" join = ";" } if js_printer.CanQuoteIdentifier(prefix, c.options.UnsupportedJSFeatures, c.options.ASCIIOnly) { if c.options.ASCIIOnly { prefix = string(js_printer.QuoteIdentifier(nil, prefix, c.options.UnsupportedJSFeatures)) } text = fmt.Sprintf("var %s%s=%s", prefix, space, space) } else { prefix = fmt.Sprintf("this[%s]", js_printer.QuoteForJSON(prefix, c.options.ASCIIOnly)) text = fmt.Sprintf("%s%s=%s", prefix, space, space) } for _, name := range c.options.GlobalName[1:] { oldPrefix := prefix if js_printer.CanQuoteIdentifier(name, c.options.UnsupportedJSFeatures, c.options.ASCIIOnly) { if c.options.ASCIIOnly { name = string(js_printer.QuoteIdentifier(nil, name, c.options.UnsupportedJSFeatures)) } prefix = fmt.Sprintf("%s.%s", prefix, name) } else { prefix = fmt.Sprintf("%s[%s]", prefix, js_printer.QuoteForJSON(name, c.options.ASCIIOnly)) } text += fmt.Sprintf("%s%s||%s{}%s%s%s=%s", oldPrefix, space, space, join, prefix, space, space) } return text } type compileResultCSS struct { printedCSS string sourceIndex uint32 hasCharset bool externalImports []externalImportCSS } type externalImportCSS struct { record ast.ImportRecord conditions []css_ast.Token } func (c *linkerContext) generateChunkCSS(chunks []chunkInfo, chunkIndex int, chunkWaitGroup *sync.WaitGroup) { chunk := &chunks[chunkIndex] compileResults := make([]compileResultCSS, 0, len(chunk.filesInChunkInOrder)) // Generate CSS for each file in parallel waitGroup := sync.WaitGroup{} for _, sourceIndex := range chunk.filesInChunkInOrder { // Create a goroutine for this file compileResults = append(compileResults, compileResultCSS{}) compileResult := &compileResults[len(compileResults)-1] waitGroup.Add(1) go func(sourceIndex uint32, compileResult *compileResultCSS) { file := &c.graph.Files[sourceIndex] ast := file.InputFile.Repr.(*graph.CSSRepr).AST // Filter out "@import" rules rules := make([]css_ast.R, 0, len(ast.Rules)) for _, rule := range ast.Rules { switch r := rule.(type) { case *css_ast.RAtCharset: compileResult.hasCharset = true continue case *css_ast.RAtImport: if record := ast.ImportRecords[r.ImportRecordIndex]; !record.SourceIndex.IsValid() { compileResult.externalImports = append(compileResult.externalImports, externalImportCSS{ record: record, conditions: r.ImportConditions, }) } continue } rules = append(rules, rule) } ast.Rules = rules compileResult.printedCSS = css_printer.Print(ast, css_printer.Options{ RemoveWhitespace: c.options.RemoveWhitespace, ASCIIOnly: c.options.ASCIIOnly, }) compileResult.sourceIndex = sourceIndex waitGroup.Done() }(sourceIndex, compileResult) } waitGroup.Wait() j := helpers.Joiner{} newlineBeforeComment := false if len(c.options.CSSBanner) > 0 { j.AddString(c.options.CSSBanner) j.AddString("\n") } // Generate any prefix rules now { ast := css_ast.AST{} // "@charset" is the only thing that comes before "@import" for _, compileResult := range compileResults { if compileResult.hasCharset { ast.Rules = append(ast.Rules, &css_ast.RAtCharset{Encoding: "UTF-8"}) break } } // Insert all external "@import" rules at the front. In CSS, all "@import" // rules must come first or the browser will just ignore them. for _, compileResult := range compileResults { for _, external := range compileResult.externalImports { ast.Rules = append(ast.Rules, &css_ast.RAtImport{ ImportRecordIndex: uint32(len(ast.ImportRecords)), ImportConditions: external.conditions, }) ast.ImportRecords = append(ast.ImportRecords, external.record) } } if len(ast.Rules) > 0 { css := css_printer.Print(ast, css_printer.Options{ RemoveWhitespace: c.options.RemoveWhitespace, }) if len(css) > 0 { j.AddString(css) newlineBeforeComment = true } } } // Start the metadata jMeta := helpers.Joiner{} if c.options.NeedsMetafile { isFirstMeta := true jMeta.AddString("{\n \"imports\": [") for _, chunkImport := range chunk.crossChunkImports { if isFirstMeta { isFirstMeta = false } else { jMeta.AddString(",") } jMeta.AddString(fmt.Sprintf("\n {\n \"path\": %s,\n \"kind\": %s\n }", js_printer.QuoteForJSON(c.res.PrettyPath(logger.Path{Text: chunks[chunkImport.chunkIndex].uniqueKey, Namespace: "file"}), c.options.ASCIIOnly), js_printer.QuoteForJSON(chunkImport.importKind.StringForMetafile(), c.options.ASCIIOnly))) } if !isFirstMeta { jMeta.AddString("\n ") } if chunk.isEntryPoint { file := &c.graph.Files[chunk.sourceIndex] // Do not generate "entryPoint" for CSS files that are the result of // importing CSS into JavaScript. We want this to be a 1:1 relationship // and there is already an output file for the JavaScript entry point. if _, ok := file.InputFile.Repr.(*graph.CSSRepr); ok { jMeta.AddString(fmt.Sprintf("],\n \"entryPoint\": %s,\n \"inputs\": {", js_printer.QuoteForJSON(file.InputFile.Source.PrettyPath, c.options.ASCIIOnly))) } else { jMeta.AddString("],\n \"inputs\": {") } } else { jMeta.AddString("],\n \"inputs\": {") } } isFirstMeta := true // Concatenate the generated CSS chunks together for _, compileResult := range compileResults { if c.options.Mode == config.ModeBundle && !c.options.RemoveWhitespace { if newlineBeforeComment { j.AddString("\n") } j.AddString(fmt.Sprintf("/* %s */\n", c.graph.Files[compileResult.sourceIndex].InputFile.Source.PrettyPath)) } if len(compileResult.printedCSS) > 0 { newlineBeforeComment = true } j.AddString(compileResult.printedCSS) // Include this file in the metadata if c.options.NeedsMetafile { if isFirstMeta { isFirstMeta = false } else { jMeta.AddString(",") } jMeta.AddString(fmt.Sprintf("\n %s: {\n \"bytesInOutput\": %d\n }", js_printer.QuoteForJSON(c.graph.Files[compileResult.sourceIndex].InputFile.Source.PrettyPath, c.options.ASCIIOnly), len(compileResult.printedCSS))) } } // Make sure the file ends with a newline j.EnsureNewlineAtEnd() if len(c.options.CSSFooter) > 0 { j.AddString(c.options.CSSFooter) j.AddString("\n") } // The CSS contents are done now that the source map comment is in cssContents := j.Done() // End the metadata lazily. The final output size is not known until the // final import paths are substituted into the output pieces generated below. if c.options.NeedsMetafile { chunk.jsonMetadataChunkCallback = func(finalOutputSize int) []byte { if !isFirstMeta { jMeta.AddString("\n ") } jMeta.AddString(fmt.Sprintf("},\n \"bytes\": %d\n }", finalOutputSize)) return jMeta.Done() } } chunk.outputPieces = c.breakOutputIntoPieces(cssContents, uint32(len(chunks))) c.generateIsolatedHashInParallel(chunk) chunkWaitGroup.Done() } func appendIsolatedHashesForImportedChunks( hash hash.Hash, chunks []chunkInfo, chunkIndex uint32, visited []uint32, visitedKey uint32, ) { // Only visit each chunk at most once. This is important because there may be // cycles in the chunk import graph. If there's a cycle, we want to include // the hash of every chunk involved in the cycle (along with all of their // dependencies). This depth-first traversal will naturally do that. if visited[chunkIndex] == visitedKey { return } visited[chunkIndex] = visitedKey chunk := &chunks[chunkIndex] // Visit the other chunks that this chunk imports before visiting this chunk for _, chunkImport := range chunk.crossChunkImports { appendIsolatedHashesForImportedChunks(hash, chunks, chunkImport.chunkIndex, visited, visitedKey) } // Mix in the hash for this chunk hash.Write(chunk.waitForIsolatedHash()) } func (c *linkerContext) breakOutputIntoPieces(output []byte, chunkCount uint32) []outputPiece { var pieces []outputPiece prefix := c.uniqueKeyPrefixBytes for { // Scan for the next chunk path boundary := bytes.Index(output, prefix) // Try to parse the chunk index var chunkIndex uint32 if boundary != -1 { if start := boundary + len(prefix); start+8 > len(output) { boundary = -1 } else { for j := 0; j < 8; j++ { c := output[start+j] if c < '0' || c > '9' { boundary = -1 break } chunkIndex = chunkIndex*10 + uint32(c) - '0' } } if chunkIndex >= chunkCount { boundary = -1 } } // If we're at the end, generate one final piece if boundary == -1 { pieces = append(pieces, outputPiece{ data: output, }) break } // Otherwise, generate an interior piece and continue pieces = append(pieces, outputPiece{ data: output[:boundary], chunkIndex: ast.MakeIndex32(chunkIndex), }) output = output[boundary+len(prefix)+8:] } return pieces } func (c *linkerContext) generateIsolatedHashInParallel(chunk *chunkInfo) { // Compute the hash in parallel. This is a speedup when it turns out the hash // isn't needed (well, as long as there are threads to spare). channel := make(chan []byte, 1) chunk.waitForIsolatedHash = func() []byte { data := <-channel channel <- data return data } go c.generateIsolatedHash(chunk, channel) } func (c *linkerContext) generateIsolatedHash(chunk *chunkInfo, channel chan []byte) { hash := xxhash.New() // Mix the file names and part ranges of all of the files in this chunk into // the hash. Objects that appear identical but that live in separate files or // that live in separate parts in the same file must not be merged. This only // needs to be done for JavaScript files, not CSS files. for _, partRange := range chunk.partsInChunkInOrder { var filePath string file := &c.graph.Files[partRange.sourceIndex] if file.InputFile.Source.KeyPath.Namespace == "file" { // Use the pretty path as the file name since it should be platform- // independent (relative paths and the "/" path separator) filePath = file.InputFile.Source.PrettyPath } else { // If this isn't in the "file" namespace, just use the full path text // verbatim. This could be a source of cross-platform differences if // plugins are storing platform-specific information in here, but then // that problem isn't caused by esbuild itself. filePath = file.InputFile.Source.KeyPath.Text } // Include the path namespace in the hash hashWriteLengthPrefixed(hash, []byte(file.InputFile.Source.KeyPath.Namespace)) // Then include the file path hashWriteLengthPrefixed(hash, []byte(filePath)) // Also write the part range. These numbers are deterministic and allocated // per-file so this should be a well-behaved base for a hash. hashWriteUint32(hash, partRange.partIndexBegin) hashWriteUint32(hash, partRange.partIndexEnd) } // Hash the output path template as part of the content hash because we want // any import to be considered different if the import's output path has changed. for _, part := range chunk.finalTemplate { hashWriteLengthPrefixed(hash, []byte(part.Data)) } // Include the generated output content in the hash. This excludes the // randomly-generated import paths (the unique keys) and only includes the // data in the spans between them. for _, piece := range chunk.outputPieces { hashWriteLengthPrefixed(hash, piece.data) } // Also include the source map data in the hash. The source map is named the // same name as the chunk name for ease of discovery. So we want the hash to // change if the source map data changes even if the chunk data doesn't change. // Otherwise the output path for the source map wouldn't change and the source // map wouldn't end up being updated. // // Note that this means the contents of all input files are included in the // hash because of "sourcesContent", so changing a comment in an input file // can now change the hash of the output file. This only happens when you // have source maps enabled (and "sourcesContent", which is on by default). // // The generated positions in the mappings here are in the output content // *before* the final paths have been substituted. This may seem weird. // However, I think this shouldn't cause issues because a) the unique key // values are all always the same length so the offsets are deterministic // and b) the final paths will be folded into the final hash later. hashWriteLengthPrefixed(hash, chunk.outputSourceMap.Prefix) hashWriteLengthPrefixed(hash, chunk.outputSourceMap.Mappings) hashWriteLengthPrefixed(hash, chunk.outputSourceMap.Suffix) // Store the hash so far. All other chunks that import this chunk will mix // this hash into their final hash to ensure that the import path changes // if this chunk (or any dependencies of this chunk) is changed. channel <- hash.Sum(nil) } func hashWriteUint32(hash hash.Hash, value uint32) { var lengthBytes [4]byte binary.LittleEndian.PutUint32(lengthBytes[:], value) hash.Write(lengthBytes[:]) } // Hash the data in length-prefixed form because boundary locations are // important. We don't want "a" + "bc" to hash the same as "ab" + "c". func hashWriteLengthPrefixed(hash hash.Hash, bytes []byte) { hashWriteUint32(hash, uint32(len(bytes))) hash.Write(bytes) } func preventBindingsFromBeingRenamed(binding js_ast.Binding, symbols js_ast.SymbolMap) { switch b := binding.Data.(type) { case *js_ast.BMissing: case *js_ast.BIdentifier: symbols.Get(b.Ref).MustNotBeRenamed = true case *js_ast.BArray: for _, i := range b.Items { preventBindingsFromBeingRenamed(i.Binding, symbols) } case *js_ast.BObject: for _, p := range b.Properties { preventBindingsFromBeingRenamed(p.Value, symbols) } default: panic(fmt.Sprintf("Unexpected binding of type %T", binding.Data)) } } // Marking a symbol as unbound prevents it from being renamed or minified. // This is only used when a module is compiled independently. We use a very // different way of handling exports and renaming/minifying when bundling. func (c *linkerContext) preventExportsFromBeingRenamed(sourceIndex uint32) { repr, ok := c.graph.Files[sourceIndex].InputFile.Repr.(*graph.JSRepr) if !ok { return } hasImportOrExport := false for _, part := range repr.AST.Parts { for _, stmt := range part.Stmts { switch s := stmt.Data.(type) { case *js_ast.SImport: // Ignore imports from the internal runtime code. These are generated // automatically and aren't part of the original source code. We // shouldn't consider the file a module if the only ES6 import or // export is the automatically generated one. record := &repr.AST.ImportRecords[s.ImportRecordIndex] if record.SourceIndex.IsValid() && record.SourceIndex.GetIndex() == runtime.SourceIndex { continue } hasImportOrExport = true case *js_ast.SLocal: if s.IsExport { for _, decl := range s.Decls { preventBindingsFromBeingRenamed(decl.Binding, c.graph.Symbols) } hasImportOrExport = true } case *js_ast.SFunction: if s.IsExport { c.graph.Symbols.Get(s.Fn.Name.Ref).Kind = js_ast.SymbolUnbound hasImportOrExport = true } case *js_ast.SClass: if s.IsExport { c.graph.Symbols.Get(s.Class.Name.Ref).Kind = js_ast.SymbolUnbound hasImportOrExport = true } case *js_ast.SExportClause, *js_ast.SExportDefault, *js_ast.SExportStar: hasImportOrExport = true case *js_ast.SExportFrom: hasImportOrExport = true } } } // Heuristic: If this module has top-level import or export statements, we // consider this an ES6 module and only preserve the names of the exported // symbols. Everything else is minified since the names are private. // // Otherwise, we consider this potentially a script-type file instead of an // ES6 module. In that case, preserve the names of all top-level symbols // since they are all potentially exported (e.g. if this is used in a //