evanw/esbuild
github.com/evanw/esbuild ↗ · 334 files · 8.5 MB
Website | Getting started | Documentation | Plugins | FAQ Why? Our current build tools for the web are 10-100x slower than they could be: The main goal of the esbuild bundler project is to bring about a new era of build tool performance, and create an easy-to-
Overview
esbuild 0.28.2 is a Go bundler and minifier for JavaScript, TypeScript, JSX, JSON, and CSS. It is used from a native CLI (cmd/esbuild), a Go library (pkg/api and pkg/cli), and an npm package whose JavaScript spawns a long-lived native binary or a WebAssembly build. The pipeline is two phases, scan then compile, with three full AST passes, parallel parsing, and incremental rebuilds that reuse immutable cached ASTs instead of a content-addressed JS cache. Releases are a matrix of optional @esbuild/* native packages plus esbuild-wasm, published by GitHub Actions when version.txt changes on main.
System map
The module is github.com/evanw/esbuild. The process entry is cmd/esbuild, which either runs the CLI or a stdin/stdout service. Bundling lives under internal/ and is wrapped by pkg/api. There is no database: inputs are files or stdin, outputs are files, stdout, or in-memory bytes. The JS API in lib/ is compiled into the published npm packages and is not a separate server. Official builds pin Go 1.26.5, keep the language at Go 1.13, and disable cgo.
- go.mod declares module github.com/evanw/esbuild, sets go 1.13 on purpose so old OS toolchains can still build it, and requires only golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8, which the comment says must not be upgraded.
- The native entrypoint is main() in cmd/esbuild/main.go. It handles --help, --version (printed from esbuildVersion), hidden --trace/--heap/--cpuprofile/--timing, and --service=<hostVersion>, which must equal the binary version or the process exits 1.
- version.txt and cmd/esbuild/version.go both say 0.28.2, matching npm/esbuild/package.json and npm/esbuild-wasm/package.json.
- pkg/api exposes Build, Transform, and Context (Rebuild, Watch, Serve, Cancel, Dispose). pkg/cli.Run and RunWithPlugins invoke the same CLI parser without spawning a child process; ParseBuildOptions, ParseTransformOptions, and ParseServeOptions reuse that parser.
- The bundler pipeline is implemented in internal/bundler.ScanBundle and (*Bundle).Compile. Compile calls a Linker, which production code passes as linker.Link. Docs place both phases in internal/bundler/bundler.go.
- Parsing, printing, resolution, and helpers are split across internal/js_parser, internal/js_printer, internal/css_parser, internal/css_printer, internal/resolver, internal/runtime, internal/cache, internal/compat, and internal/config. JS parse entry is js_parser.Parse; CSS parse entry is css_parser.Parse; print entries are js_printer.Print and css_printer.Print.
- File IO goes through internal/fs.FS so tests can mock it and so Yarn PnP zip installs can be overlaid. fs.go documents the abstraction; fs_zip.go wraps an inner FS and treats zip files as directories, including Yarn __virtual__ and $$virtual path segments.
- The Makefile default target builds with CGO_ENABLED=0, -ldflags=-s -w -buildid=, -buildvcs=false, and -trimpath. go.version is 1.26.5. The go-compiler target downloads that Go source and comments out ctxt.buildinfo() so go version ./esbuild does not embed the toolchain version.
- make no-filepath fails the build if any Go file under cmd, internal, or pkg imports path/filepath, citing golang.org/issue/43768. CI runs that check on Ubuntu.
- WebAssembly builds use main_wasm.go under //go:build js && wasm. That file rejects --trace, --heap, and --cpuprofile and sets isServeUnsupported to true, so the dev server is not available in WASM.
- npm/esbuild postinstall is node install.js. optionalDependencies lists per-OS @esbuild/* packages at 0.28.2, and esbuild.binaryHashes stores sha256 digests of those binaries. esbuild-wasm has no optionalDependencies and points browser at lib/browser.js.
- GitHub Actions workflows are ci.yml (push, PR, dispatch), publish.yml (push to main when version.txt changes), e2e.yml (cron every 6 hours plus dispatch), and validate.yml (tags v* and dispatch running make validate-builds).
Feature catalog
User-facing features are the CLI flag set, the Go and JavaScript build/transform/context APIs, plugins, a local HTTP dev server, polling watch mode, and packaging for many OS/CPU targets plus WASM. Loaders cover JS, JSX, TS, TSX, CSS (including CSS modules), JSON, and several asset loaders. React appears only as a test dependency in scripts/package.json, not as a product UI.
- CLI help in main.go documents --bundle, --define:K=V, --external:M, --format=iife|cjs|esm, --loader, --minify, --outdir/--outfile, --packages, --platform=browser|node|neutral, --serve, --sourcemap, --splitting, --target, and --watch / --watch=forever.
- The same help lists loaders base64, binary, copy, css, dataurl, empty, file, global-css, js, json, jsx, local-css, text, ts, and tsx. Default extension mapping in bundler.go assigns .js/.mjs/.cjs to JS, .jsx to JSX, and .json to JSON.
- Advanced CLI flags include --analyze, --banner/--footer, --certfile/--keyfile, --drop, --jsx and --jsx-import-source (default react), --mangle-props, --metafile, --minify-whitespace/identifiers/syntax, --pure, --supported, --tree-shaking, --tsconfig, and --tsconfig-raw.
- pkg/api.BuildOptions-style operations are Build (one-shot), Transform (string in, code/map/legal comments out), and Context for repeated Rebuild, Watch, and Serve. Plugin hooks are Setup plus OnStart, OnEnd, OnResolve, OnLoad, and OnDispose.
- lib/npm/node.ts exports build, context, transform, formatMessages, analyzeMetafile, and the sync twins buildSync, transformSync, and formatMessagesSync. Sync calls prefer a worker_threads service unless ESBUILD_WORKER_THREADS is 0 or the thread is already the internal worker.
- The service protocol commands handled in service.go are build, transform, resolve, rebuild, watch, serve, cancel, dispose, error, format-msgs, and analyze-metafile. Resolve kinds encoded later include entry-point, import-statement, require-call, dynamic-import, require-resolve, import-rule, composes-from, and url-token.
- Serve mode is a local HTTP server. It overlays in-memory build outputs on servedir, serves index.html or a generated directory listing, and special-cases GET /esbuild with Accept text/event-stream. HTTPS requires both keyfile and certfile. CORS origins support a single * wildcard.
- Watch mode is a polling scanner, not fsnotify and not cgo. It sleeps 100ms, checks up to 16 recent paths every tick plus at least 64 other paths, and aims to scan a large project about every 2 seconds.
- When bundling, an unspecified format becomes IIFE for platform browser, CommonJS for node, and ESM for neutral. --splitting is rejected unless the output format is ESM. external and alias are rejected unless bundle is set.
- Yarn PnP resolution is implemented in internal/resolver/yarnpnp.go against the Yarn PnP spec (package registry, fallback pool, ignore pattern). Zip contents are read through internal/fs/fs_zip.go.
- The JS API locates a platform binary via knownWindowsPackages, knownUnixlikePackages, and knownWebAssemblyFallbackPackages (android arm, android x64, and openharmony arm64 are WASM fallbacks). ESBUILD_BINARY_PATH overrides the path, except the hard-coded ignore of /usr/bin/esbuild.
- scripts/package.json depends on react 17.0.1, typescript 4.7.4, source-map, and unicode data. Those are test/tooling dependencies. The product itself has no React UI; JSX support defaults the factory to React.createElement.
Data flow
CLI arguments and JS option objects become config.Options and entry points. ScanBundle walks the graph in parallel: resolve, onLoad, parse, then enqueue imports. Cached ASTs must stay immutable. Compile clones the linker graph, tree-shakes, splits chunks, prints, and optionally writes files. Transform skips the real filesystem and feeds stdin bytes through the same two phases. The dev server serves the latest in-memory output instead of those files.
- CLI runImpl reads NODE_PATH (colon-separated, semicolon on Windows) into BuildOptions.NodePaths, and if there are no entry points it reads all of stdin into Stdin.Contents with ResolveDir set from os.Getwd.
- A JS build request is a length-prefixed packet whose BuildRequest carries entries, flags, write, stdin bytes, absWorkingDir, nodePaths, plugins, and mangleCache. The Go side parses flags with cli.ParseBuildOptions and calls api.Build.
- contextImpl builds a real FS from AbsWorkingDir, runs loadPlugins once, validates options, and stores a rebuildArgs including caches and onEnd callbacks. Later rebuilds do not re-run plugin setup.
- rebuildImpl calls bundler.ScanBundle(config.BuildCall, ...) then, if the log has no errors, bundle.Compile(..., linker.Link). Each output file is hashed with xxhash into an 8-byte base64 hash stored on OutputFile and in latestHashes.
- ScanBundle always parses the runtime as source index 0 via globalRuntimeCache.parseRuntime before entry points. The runtime text lives in internal/runtime/runtime.go and is tree-shaken if unused. SourceIndex is the constant 0.
- maybeParseFile dedupes by canonical path, copies tsconfig JSX and module-type data (.mjs/.mts vs .cjs/.cts vs package.json type), then starts parseFile on a goroutine. scanAllDependencies reads resultChannel until remaining hits zero and resolves each import record.
- parseFile runs onLoad plugins unless the input is stdin, maps LoaderDefault through ExtensionToLoader, then dispatches: JS/JSX/TS/TSX to caches.JSCache.Parse, CSS/global-css/local-css to CSSCache.Parse, and JSON to JSONCache.Parse plus LazyExportAST. Import attributes type json, bytes, and text retarget the loader.
- JSCache.Parse reuses an AST only when source.KeyPath hits and both source contents and js_parser.Options.Equal match. The cache comment forbids mutating cached ASTs and forbids baking in data from other files.
- js_parser.Parse lexes on the fly, parses statements with parseStmtsUpTo, then a second pass splits top-level statements into parts via appendPart, which calls visitStmtsAndPrependTempRefs for binding, lowering, and mangling.
- Compile either links all entry points together (code splitting, or a single entry) or forks one linker goroutine per entry point. linker.Link clones the graph, calls scanImportsAndExports, treeShakingAndCodeSplitting, computeChunks, mangles props under ExclusiveMangleCacheUpdate, then generateChunksInParallel. Metafile JSON is generated only when NeedsMetafile is set.
- When write is true and not stdout, rebuildImpl deletes paths present in oldHashes but not newHashes, skips writes whose on-disk bytes already match, and otherwise MkdirAll 0755 plus WriteFile 0666 or 0777 for executables. OnEnd callbacks run after writes. Errors suppress writing (issue 3643 is cited in code).
- Transform builds options with Stdin.Contents set to the input string, uses fs.MockFS, and calls ScanBundle(config.TransformCall) then Compile. Results are split into code, a .map file, and a .LEGAL.txt file by path suffix.
Behavior traces
Three flows matter: process startup (CLI versus service), a full bundle (scan, link, write), and the JS client attaching to that service. Watch and serve sit on the same Context and reuse rebuild. Transform is the bundle path with a mock filesystem and no entry-point files.
- Startup with no args and a TTY stdin prints helpText and exits 0. --version prints esbuildVersion and exits 0. --service=VERSION checks the host version, then runService(sendPings) and returns without calling cli.Run.
- runService sets logger.API to JSAPI, writes the version as the first length-prefixed stdout packet, and if --ping was passed sends command ping every second. Packets are written on one goroutine so they cannot interleave. EOF on stdin ends the loop.
- A normal CLI invocation strips the special flags, optionally disables GC when there is at most one entry point and the process is not watch/serve, then sets exitCode from cli.Run. Non-TTY stdin starts a goroutine that os.Exit when stdin closes, which is how watch and serve stop.
- cli.runImpl intercepts any --serve, --servedir, or --serve-fallback argument and calls serveImpl, which parseServeOptionsImpl, api.Context, ctx.Serve, optional ctx.Watch, then blocks forever on a nil channel. Returning from serveImpl yields exit code 1.
- One-shot Build calls contextImpl, then ctx.Rebuild, optionally printSummary, then ctx.Dispose. Rebuild coalesces concurrent callers onto the active buildInProgress and otherwise calls rebuildImpl.
- Scan order inside ScanBundle is: parallel onStart callbacks, generateUniqueKeyPrefix, resolver.NewResolver, parse runtime, wait for onStart, honor CancelFlag, preprocessInjectedFiles, addEntryPoints, scanAllDependencies, processScannedFiles. Cancel after onStart still returns a Bundle so onEnd can run.
- linker.Link order is: wrappedLog, graph.CloneLinkerGraph, bind __commonJS or __commonJSMin from the runtime AST, scanImportsAndExports, treeShakingAndCodeSplitting, computeChunks, computeCrossChunkDependencies, mangleProps and mangleLocalCSS, ast.FollowAllSymbols, generateChunksInParallel.
- JS build() and transform() call ensureServiceIsRunning(). The child argv is the native binPath, or node plus the wasm bin shim when WASM or the platform fallback is WASM. esbuildCommandAndArgs throws if the API file is not lib/main.js, because bundling the package breaks the relative binary path.
- handleBuildRequest parses flags with cli.ParseBuildOptions. For a context build it keeps an activeBuild keyed by an integer and wires plugin onEnd back across the protocol. A non-context build calls api.Build and encodes errors, warnings, output files, metafile, and mangle cache.
- ServeHTTP checks Host against localhost or the server's host list (DNS rebinding), rejects backslash paths, rebuilds via h.rebuild(), returns 503 text if the build has errors, then matches the cleaned URL to in-memory output or servedir.
- A service rebuild command looks up activeBuild by key, increments withinRebuildCount, calls ctx.Rebuild(), and returns only errors and warnings, not output files. dispose waits on disposeWaitGroup so in-flight rebuild, watch, serve, and resolve calls finish.
- Plugin onResolve filters are compiled with config.CompileFilterForPlugin before the callback is stored. During the build, Go plugins call bundler.RunOnResolvePlugins; JS plugins are proxied as service resolve requests.
Testing signals
Go tests use the standard testing package and cover cmd, internal, and pkg. Bundler expectations are snapshot files updated with UPDATE_SNAPSHOTS=1. A second layer of Node scripts drives the real binary, the JS API, plugins, source maps, and WASM. CI splits fast race tests, slow compatibility suites, an old Go 1.13 job, tag validation, and a 6-hour install matrix. There is no separate browser product to test; React in scripts is a fixture dependency.
- make test runs test-common in parallel: test-go, vet-go, no-filepath, verify-source-map, end-to-end-tests, js-api-tests, plugin-tests, register-test, node-unref-tests, and decorator-tests. test-go is go test ./cmd/... ./internal/... ./pkg/... and does not pass -race unless ESBUILD_RACE is set.
- CI job esbuild on ubuntu, macos, and windows runs go test -race, go vet, npm ci in scripts, register-test, verify-source-map, end-to-end-tests.js, js-api-tests.js, node-unref-tests.js, and plugin-tests.js. Ubuntu also runs no-filepath, ts-type-tests, lib-typecheck, decorator-tests, both WASM targets, sucrase, esprima, preact splitting, unicode table generation, and Yarn PnP.
- Bundler tests in internal/bundler_tests compare against snapshots/*.txt. The header says to regenerate with UPDATE_SNAPSHOTS=1 make test and inspect the diff. expectBundled runs each case on fs.MockUnix and again with Windows path conversion.
- Snapshot files exist for css, dce, default, glob, importphase, importstar, loader, lower, packagejson, splitting, ts, tsconfig, and yarnpnp. Separate Go tests exist for the lexer, parser, printer, css parser, compat, resolver yarnpnp, runtime, logger, and pkg/api.
- The slow CI job uses Node 16 because a comment says WASM tests break on Node 18, and runs make test-rollup, CI=1 make uglify, make test-tsc, make test-old-ts, plus GOOS=plan9 builds for 386, amd64, and arm.
- A dedicated job installs Go 1.13, the minimum declared in go.mod, to prove the tree still builds on that compiler. check-go-version in the Makefile requires the go.version toolchain for release tasks.
- scripts/register-test.js writes a temporary require.extensions['.ts'] hook that calls transformSync with loader ts, then runs node -r register on an entry.ts. It is not a published register package; it is a regression test for that pattern.
- decorator-tests compiles scripts/decorator-tests.ts with the built esbuild at --target=es2022 and fails if git diff shows the checked-in JS changed. The Makefile says the source of those tests is github.com/evanw/decorator-tests.
- make test-all adds ts-type-tests, test-wasm-node, test-wasm-browser, lib-typecheck, and test-yarnpnp on top of test-common. test-wasm-node runs go test ./internal/... with GOOS=js GOARCH=wasm and then node scripts/wasm-tests.js.
- e2e.yml every 6 hours runs make test-e2e-npm, test-e2e-pnpm, test-e2e-yarn, and test-e2e-yarn-berry against the published esbuild package, including --ignore-scripts and --no-optional install paths.
- compat-table/package.json vendors compat-table at commit 8f4179ff, plus @mdn/browser-compat-data and caniuse-lite, which feed the engine support tables under internal/compat. Those generated tables are not executed as unit tests by themselves.
- lib-typecheck runs tsc -noEmit on lib/tsconfig.json and lib/tsconfig-nolib.json. The published .d.ts surface is lib/shared/types.ts, copied into the npm package as lib/main.d.ts.
Risk map
esbuild treats source text as trusted and says the dev server is not a security boundary. The sharp edges are the install-time binary download, the unauthenticated local IPC service, filesystem writes at absolute paths, and the size of the parser and linker. Go 1.13 compatibility and the path/filepath ban make casual modern-Go edits break the build. The architecture doc disagrees with the CLI on whether tree shaking can be turned off.
- node-install.ts downloads missing platform binaries by running npm install of @esbuild/<pkg>@<version> via child_process.execSync, and as a last resort https.get of https://registry.npmjs.org/<pkg>/-/<tarball>.tgz. extractFileFromTarGzip pulls package/<subpath> out of the gzip.
- binaryIntegrityCheck sha256s the bytes and compares them to packageJSON['esbuild.binaryHashes'][pkg/subpath]. A missing or mismatched hash throws. The check is skipped when ESBUILD_BINARY_PATH is set and the file exists.
- validateBinaryVersion runs the chosen binary with --version via execFileSync and throws if stdout does not equal packageJSON.version. On macOS it special-cases _SecTrustEvaluateWithError and suggests esbuild-wasm or an older Go.
- The stdin/stdout service has no authentication. Any local process that can speak the length-prefixed protocol can request build, transform, serve, or resolve. --service refuses to start only when the host version string mismatches.
- The Makefile comment states esbuild is not a security boundary: it transforms trusted source text and the dev server is a read-only view of source not intended for production. Serve still checks Host to block DNS rebinding and rejects Windows backslashes in URLs.
- rebuildImpl writes output with ioutil.WriteFile at absolute paths using mode 0666, or 0777 when IsExecutable, and os.Remove on outputs that disappeared between rebuilds. AllowOverwrite is forced when Write is false so serve can use input paths as output paths.
- OnLoad and OnResolve plugins supply file contents and paths. JS plugins cross the service boundary; a plugin callback can therefore cause the Go process to parse and later write whatever bytes the plugin returns.
- docs/architecture.md says tree shaking is always active and cannot be disabled. The CLI and TransformOptions both expose a tree-shaking switch, and validateTreeShaking is called from transformImpl. The architecture page is stale on that point. (inferred)
- publish.yml has id-token: write and contents: write, builds every platform, refuses a dirty git status, installs npm 11.5.1, runs make publish-all, pushes tag v$ESBUILD_VERSION, and creates a GitHub Release from the CHANGELOG section. A version.txt push to main is the release trigger.
- Parser panics that are not js_lexer.LexerPanic are re-panicked in js_parser.Parse. parseFile has its own recover that turns any panic into a log error with a stack, so a parser crash becomes a failed file rather than a dead process during scan.
- staticcheck.conf sets checks = [] and the comment says the author disabled the VS Code Go linter on purpose. Agents should not assume staticcheck is an enforced gate.
- ioutil is still used in api_impl.go and the install path uses sync npm install. That is consistent with the Go 1.13 floor, but edits that use newer stdlib-only APIs or generics will fail the Go 1.13 CI job even if local Go 1.26 accepts them.
Extension points
The supported plug-in surface is the plugin object with onStart, onResolve, onLoad, onEnd, and onDispose, available from Go, the CLI via RunWithPlugins, and the JS service. Loaders, defines, inject files, and the runtime helper string are the other intended extension points. New OS targets are new directories under npm/@esbuild plus installer tables, not a plugin.
- api.Plugin is Name plus Setup(PluginBuild). PluginBuild registers OnStart, OnEnd, OnResolve(filter, namespace), OnLoad(filter, namespace), OnDispose, and a Resolve helper. SideEffects is a bool-like enum on results.
- cli.RunWithPlugins appends caller plugins after parse, and --analyze is itself added as a plugin that forces Metafile true. Native Go embedding is the supported way to add plugins without the JS service.
- JS plugins are declared as BuildPlugin records (name, onStart, onEnd, onResolve/onLoad id+filter+namespace) and sent in the build packet. Callbacks round-trip as resolve and on-end requests.
- config.CompileFilterForPlugin compiles each OnResolve and OnLoad filter regex and tags errors with the plugin name and hook kind. That is the validation gate for new plugin filters.
- parseFile's switch on config.Loader is the dispatch table for new content types. Adding a loader means a Loader constant, an extension map entry, a CLI name, and a branch that produces either a JSRepr or CSSRepr.
- Architecture docs say new helper functions belong in the string inside internal/runtime/runtime.go so they are parsed as the runtime file and tree-shaken. Names must not collide with tslib helpers such as __decorate and __extends; esbuild uses its own __create, __defProp, and similar names.
- Compile-time substitution is --define:K=V and the Define map. Dead-code elimination of require() inside folded branches is documented with the React process.env.NODE_ENV pattern. --inject:F and --pure:N are the other per-build hooks that do not require a plugin.
- Resolver namespaces are part of logger.Path. The file namespace is special-cased for canonical Windows paths, relative rewriting, and overwrite checks. dataurl is a namespace handled by resolver.ParseDataURL. Plugins can return other namespaces from OnResolve.
- Engine support is data, not code: internal/compat feature bits plus tables generated from compat-table (MDN, caniuse, compat-table). --target and --supported:F=true|false are the user-facing overrides.
- A new platform package is a directory under npm/@esbuild, plus entries in node-platform.ts maps and esbuild.binaryHashes. RUNBOOK.md says to publish an empty 0.0.1 package, set the esbuild npm user as sole maintainer, and add this repo's publish.yml as the trusted publisher.
- ESBUILD_BINARY_PATH is a documented external extension point. Comments in node-platform.ts and node-install.ts say not to remove it because outside code and the test suite depend on it.
- Banner and footer are per output type (css or js). Injected files are turned into generated import statements in the parser before the visit pass. Both are extension points that do not add a new loader.
Contributor handoff
Start with the architecture doc, the Makefile, and the three public edges (CLI, pkg/api, lib service protocol). Keep Go 1.13, the single dependency pin, and the path/filepath ban. Do not hand-edit published npm trees. Version changes are a release. Parser and linker edits need snapshot and JS API tests, and cached ASTs must stay immutable.
- Read order for a pipeline change is docs/architecture.md, docs/development.md, internal/bundler/bundler.go (ScanBundle, parseFile, Compile), internal/linker/linker.go (Link), internal/js_parser/js_parser.go (Parse and appendPart), and pkg/api/api_impl.go (contextImpl, rebuildImpl, transformImpl).
- Safe local checks are make or go build ./cmd/esbuild, then make test-go or go test ./internal/... ./pkg/.... Snapshot updates are UPDATE_SNAPSHOTS=1 and must be reviewed. JS API changes also need node scripts/js-api-tests.js and node scripts/plugin-tests.js after scripts/esbuild.js has built the package under test.
- Do not change the go version line or the golang.org/x/sys pin in go.mod. Do not import path/filepath. Do not enable cgo. Release binaries are built with the go.version compiler via the Makefile GO_COMPILER wrapper, not whatever go is first on PATH.
- A release is version.txt (number only), a matching ## header in CHANGELOG.md, then make platform-all committed so every package.json matches, then a push to main. publish.yml fails if git status is dirty after its own platform-all, and it refuses an empty changelog section.
- The JS service and the Go service are one protocol. Changing a command, flag name, or packet field requires edits in lib/shared/stdio_protocol.ts, lib/shared/common.ts, and cmd/esbuild/service.go together. The first stdout bytes of the service are the version string, and the host passes that same version as --service=.
- Cache and linker invariants are unchecked by the type system. cache.go says ASTs are shared across builds and must be immutable, and options that affect parsing must be in the cache key. Architecture docs say cross-build data must stay immutable for watch mode.
- Symbol identity is a ref into a per-file symbol array, merged across files by Link chains. FollowSymbols or FollowAllSymbols must be used before printing. Renaming or cloning symbols without following links will emit the wrong identifier.
- Code splitting only works for the esm format. Moving an assignment to a module-local binding into a different chunk than its declaration is a known correctness hazard documented in architecture.md.
- Watch mode stops when stdin closes unless --watch=forever. The dev server also stops when stdin closes. WASM cannot serve. Do not replace the poller with a cgo file watcher; watcher.go explains why cgo is rejected.
- Large files an agent will fight: internal/js_parser/js_parser.go, internal/linker/linker.go, internal/bundler/bundler.go, and the css_parser declaration files. Prefer a new test in internal/bundler_tests or scripts/js-api-tests.js over a drive-by refactor. (inferred)
- npm/esbuild/lib and the platform bin/ directories are build outputs of scripts/esbuild.js. make publish-all deletes and restores npm/ from git before publishing. Editing generated install.js in the published tree without changing lib/npm/node-install.ts will be overwritten.
- There is no product web UI in this repo. scripts/try.html is a manual WASM playground served by esbuild --servedir=.. Do not treat the React dependency in scripts/package.json as application code.
Languages
| Language | Files | Share of source |
|---|---|---|
| Go | 157 | 79% |
| Markdown | 48 | docs/config |
| JSON | 46 | docs/config |
| JavaScript | 22 | 11% |
| TypeScript | 18 | 9% |
| HTML | 5 | docs/config |
| YAML | 4 | docs/config |
| CSS | 1 | docs/config |
Start reading here
Probable source entrypoint: cmd/esbuild — inferred from the directory tree and manifest, not the upstream docs.
Source roots
lib/ (15 files) pkg/ (13 files)
Source layout — lib/
| Path | Files |
|---|---|
lib/npm/ | 5 |
lib/shared/ | 5 |
lib/README.md | 1 |
lib/package.json | 1 |
lib/tsconfig-deno.json | 1 |
lib/tsconfig-nolib.json | 1 |
lib/tsconfig.json | 1 |
Repository structure
| Top-level path | Files |
|---|---|
internal/ | 154 |
npm/ | 60 |
scripts/ | 30 |
lib/ | 15 |
pkg/ | 13 |
require/ | 13 |
compat-table/ | 11 |
cmd/ | 7 |
Docs found: LICENSE.mdREADME.mddocs/architecture.mddocs/development.md
Package
go.mod — github.com/evanw/esbuild, 0 declared dependencies.
CI & config entrypoints
README.md— Repository readme.github/workflows/publish.yml— GitHub Actions workflow.github/workflows/validate.yml— GitHub Actions workflow.github/workflows/e2e.yml— GitHub Actions workflow.github/workflows/ci.yml— GitHub Actions workflownpm/esbuild/package.json— npm scripts: postinstall
Structural health signals
Average score 6.7/10 across 96 analyzed source files; 34 hotspots surfaced.
scripts/verify-source-map.js— 1319 lines, score 4 (Large file, Deep nesting)scripts/ts-type-tests.js— 464 lines, score 4.5 (Large file, Deep nesting)scripts/end-to-end-tests.js— 2264 lines, score 4.7 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
f6058f8364fe2026-08-09 — close #3701, fix #3896: temp `ref` child for killb5d4d79d6b4f2026-08-09 — add a `clang` format style214a9951fe482026-08-09 — pin each github actions dep to a git commit hash1e4d41bfb1072026-08-09 — add `es2026` as a targetc2cd4b9a90c52026-08-09 — note about workflow inactivity