webpack/webpack
github.com/webpack/webpack ↗ · 3999 files · 15.0 MB · scan capped
[![npm][npm]][npm-url] [![node][node]][node-url] [![builds1][builds1]][builds1-url] [![dependency-review][dependency-review]][dependency-review-url] [![coverage][cover]][cover-url] [![PR's welcome][prs]][prs-url] webpack Webpack is a module bundler.
Overview
webpack is the JavaScript module bundler (v5.111.1, MIT, `main: lib/index.js`, CLI at `bin/webpack.js`): a `Compiler` builds a module graph from entry dependencies, a `Compilation` seals that graph into chunks, and the result is emitted as static assets for web, Node, worker, Deno, Bun, Electron and universal targets. Everything is `tapable` hooks over CommonJS + JSDoc source — the public config API is defined by JSON Schema in `schemas/` and every type, validator and declaration is code-generated from it by `tooling/generate-types.js`. What is distinctive about this tree is how much webpack now owns itself: its own acorn-derived ECMAScript parser (`lib/javascript/syntax-parser.js`, 17.8k lines), its own CSS and HTML parser/printer/minifier pairs, generated lookup-table modules (`lib/{css,html,javascript}/data.js`), and a comparison harness in `tooling/` that benchmarks those against the ecosystem and gates printer invariants in CI. It is aimed at application developers via config, and at the plugin/loader ecosystem via ~100 exported classes and several hundred hooks.
System map
The public surface is `lib/index.js` — a lazily-evaluated getter object exporting the `webpack()` factory plus ~100 plugin/class names and namespaced sub-objects (`webpack.cli`, `webpack.javascript.syntax`, `webpack.container`, `webpack.sharing`). `lib/webpack.js` normalizes, defaults and validates options, constructs a `Compiler`, and hands off to `WebpackOptionsApply`, which is the single place that translates resolved options into the ~60 plugins a build implies. Runtime boundaries are the filesystem (`lib/fs/fs.js` declares `InputFileSystem`/`OutputFileSystem`, `NodeEnvironmentPlugin` supplies the Node implementations), the loader sandbox (`lib/loaders/LoaderRunner.js`), and optional network I/O (`lib/schemes/HttpUriPlugin.js`, `lib/hmr/lazyCompilationBackend.js`). Persistence is the filesystem cache pack under `lib/cache/` plus the custom binary serializer in `lib/serialization/`. Build/test is all yarn scripts in `package.json`, and CI is 13 GitHub Actions workflows with `test.yml` as the hub.
- `lib/index.js` is the public export surface: every entry is a memoized getter so `require("webpack")` does not eagerly load the ~770 files under `lib/` — `webpack.cli` and `webpack.util.terminalColors` are split specifically so reading a terminal color does not pull in the options schema.
- The `webpack(options, callback)` factory in `lib/webpack.js` runs a precompiled schema check (`schemas/WebpackOptions.check`) first and only falls back to the full `validateSchema` (emitting a `DEP_WEBPACK_PRE_COMPILED_SCHEMA_INVALID` deprecation) when the fast check disagrees; an array config routes to `createMultiCompiler`.
- `createCompiler` fixes the startup order: `getNormalizedWebpackOptions` → `applyWebpackOptionsBaseDefaults` → `new Compiler` → `NodeEnvironmentPlugin` → user plugins → `applyWebpackOptionsDefaults` → `environment`/`afterEnvironment` hooks → `WebpackOptionsApply.process` → `initialize` hook. User plugins are therefore applied *before* the mode/target-dependent defaults are filled in.
- `lib/config/WebpackOptionsApply.js` (1,274 lines) is the option→plugin translation layer: it statically requires the always-on plugins (`JavascriptModulesPlugin`, `APIPlugin`, `ConstPlugin`, `RuntimePlugin`, `DefaultStatsFactoryPlugin`, …) and lazily requires target-specific ones (`NodeTargetPlugin`, `DenoTargetPlugin`, `BunTargetPlugin`, `ElectronTargetPlugin`, `EnableChunkLoadingPlugin`) inside the branches that need them.
- `lib/config/target.js` holds the target preset table: `TARGETS` is a list of `[name, description, regExp, handler]` tuples covering `browserslist[:…]`, `web`, `webworker`, `node[X.Y]`, `async-node`, `electron[X.Y]-main/preload/renderer`, `nwjs`/`node-webkit`, `deno`, `bun`, and `getTargetProperties` throws a message enumerating all of them for an unknown target.
- The storage layer is a two-tier cache: `MemoryCachePlugin`/`MemoryWithGcCachePlugin` for in-process, and `IdleFileCachePlugin` wrapping `PackFileCacheStrategy` (1,893 lines, with `Pack`, `PackContent` and `PackContainer` classes) for `cache.type: "filesystem"`, storing on an idle timer after the compiler goes idle.
- Cache persistence rides on a hand-written binary serializer in `lib/serialization/` (`BinaryMiddleware`, `ObjectMiddleware`, `FileMiddleware`, plus per-type serializers for Map/Set/RegExp/Date/Error); classes opt in through `makeSerializable(Class, "webpack/lib/…")`, used across 144 files in `lib/`.
- Deserialization resolves a class by request prefix: `ObjectMiddleware.registerLoader(/^webpack\/lib\//, …)` looks the suffix up in the generated `lib/util/internalSerializables.js` map and warns `<req> not found in internalSerializables` when a class was moved without regenerating — which is why `yarn fix:serializables` exists.
- External integrations are deliberately few: runtime dependencies are `tapable` (hooks), `enhanced-resolve` (resolution), `watchpack` (watching), `webpack-sources` (source objects), `terser` + `minimizer-webpack-plugin` (minification worker pool), `@webassemblyjs/*` (wasm), `browserslist`, `es-module-lexer`, `mime-db`, `schema-utils` and `graceful-fs`.
- All commands are yarn scripts: `yarn fix` (`fix:code` + `fix:special` + `fmt`), `yarn lint` (a nine-stage `&&` chain ending in `fmt:check` and `cspell`), `yarn test:base` (the only sanctioned jest wrapper — it carries `--experimental-vm-modules --expose-gc --max-old-space-size=4096`), and `yarn fix:special`, which runs all seven generators.
- Code generation is a first-class build step: `tooling/generate-types.js` reads `schemas/**/*.json` through Ajv `standaloneCode` and `json-schema-to-typescript` to emit precompiled validators, `declarations/*.d.ts` and `types.d.ts`, while `generate-{css,html,js}-data.js` derive the parser/minifier lookup tables from `mdn-data`, `@webref/*` and the pinned `acorn`.
- CI is 13 workflows; `test.yml` gates everything expensive behind three cheap jobs (`integration: needs: [lint, basic, unit]`, same for `test262`, `parser`, `syntax-equivalence`, the Deno/Bun runtime matrix and the benchmark shards), and the integration matrix spans Node 10.x through current with sharded `cover:integration:a|b` uploads to Codecov.
Feature catalog
There are no routes or screens; the user-facing surface is the config schema, the CLI shim, and the exported plugin classes. `schemas/WebpackOptions.json` defines 36 top-level options and 12 `experiments` flags, each of which maps to plugins wired in `WebpackOptionsApply`/`defaults.js`. Language support is organized as parser+generator+printer triples per module type (javascript, css, html, json, asset, wasm, typescript), most of which webpack implements itself rather than delegating. Beyond bundling proper, the shipped features include Module Federation, HMR and lazy compilation, persistent caching, source maps, code splitting, library output formats, and a stats/reporting layer.
- The config API exposes 36 top-level options — `amd, bail, cache, context, dependencies, devServer, devtool, dotenv, entry, experiments, extends, externals, externalsPresets, externalsType, ignoreWarnings, infrastructureLogging, loader, mode, module, name, node, optimization, output, parallelism, performance, plugins, profile, recordsInputPath, recordsOutputPath, recordsPath, resolve, resolveLoader, snapshot, stats, target, validate, watch, watchOptions` — all validated against one schema.
- Twelve experiment flags gate unreleased surfaces: `asyncWebAssembly, backCompat, buildHttp, cacheUnaffected, css, deferImport, futureDefaults, html, lazyCompilation, sourceImport, syncWebAssembly, typescript`.
- Native CSS and HTML are full pipelines, not loaders: `lib/css/` ships `syntax-parser.js` (14,973 lines), `syntax-printer.js` (4,730) and a generated `data.js` (8,003) of MDN-derived tables; `lib/html/` ships `syntax-parser.js` (13,197, including §13.2 tree construction) and `HtmlGenerator.js`.
- JavaScript is parsed by webpack's own acorn-derived parser (`lib/javascript/syntax-parser.js`, 17,844 lines) feeding `JavascriptParser.js` (7,604 lines) which raises the hook surface every dependency plugin taps (`evaluateIdentifier`, `call`, `expression`, …).
- Minification defaults to one `minimizer-webpack-plugin` instance sharing a single worker-thread pool across up to three `minify` functions dispatched by extension — `jsMinify` (`[cm]?js`), `cssMinify` (`css`) and `htmlMinify` (`html`) — and steps aside entirely when `getClaimedAssetTypes` finds a user-configured minimizer for that type.
- `jsMinify` reaches terser through `webpack.javascript.syntax.printer.load()` rather than `require("terser")`, so phases webpack has reimplemented replace terser's own; it is re-required as `require("webpack")` inside the worker because the plugin ships the function to the pool as source.
- Module Federation ships as 18 files in `lib/container/` (`ModuleFederationPlugin`, `ContainerPlugin`, `ContainerReferencePlugin`, `RemoteModule`, `RemoteRuntimeModule`, `HoistContainerReferencesPlugin`) plus 15 in `lib/sharing/` (`SharePlugin`, `ConsumeSharedPlugin`, `ProvideSharedPlugin`, `ShareRuntimeModule`, and a bundled `semver.js`).
- HMR is `HotModuleReplacementPlugin` (1,000+ lines) taps on `compilation.hooks.record`, `fullHash`, `processAssets` and `additionalTreeRuntimeRequirements`, plus three generated runtime files and a `parserHooks.js` module that exposes the `module.hot` parser surface to other plugins.
- Lazy compilation runs a real HTTP(S) server in-process: `lazyCompilationBackend.js` creates a server (protocol from `options.protocol`, `https` when configured), tracks `activeModules` refcounts keyed by module, and destroys sockets on close.
- `experiments.buildHttp` turns on `HttpUriPlugin`, which fetches `http:`/`https:` modules, honours `options.proxy` or `http_proxy`/`HTTP_PROXY`, enforces an `allowedUris` policy on both the initial URL and every redirect, and records resolutions into a lockfile (`lockfileLocation`) with bodies cached under `<lockfile>.data`.
- Output-shaping plugins are grouped in `lib/output/`: `CopyPlugin` (the `output.copy` option), `CleanPlugin` (the `output.clean` option, which unlinks and rmdirs inside `output.path`), `BannerPlugin`, and `ManifestPlugin`.
- `DotenvPlugin` is a first-class option (`dotenv` at the top level) that vendors dotenv v17.4.2 parsing and dotenv-expand v13.0.0 expansion, reading `.env`, `.env.local`, `.env.[mode]`, `.env.[mode].local` and feeding the values through `DefinePlugin`.
Data flow
Input boundaries are the config object (validated against JSON Schema), the input filesystem behind `InputFileSystem`, and optionally the network (`HttpUriPlugin`) or a data/virtual URL scheme. A request becomes a module through `NormalModuleFactory.create` (resolve → rules match → factory hooks), is read and transformed by the loader chain in `LoaderRunner`, then parsed into `Dependency` objects that populate the `ModuleGraph`. At seal time the `ModuleGraph` is frozen and `buildChunkGraph` fills a `ChunkGraph`; code generation renders each module through its `Generator` and `DependencyTemplate`s into `Source` objects, which are hashed, assembled into assets, passed through the `processAssets` stage pipeline, and finally written by `Compiler.emitAssets` through `OutputFileSystem`. Everything durable — module graph, code generation results, resolver results — round-trips through `lib/serialization/` into the filesystem cache pack.
- Input normalization is two distinct layers: `normalization.js` canonicalizes the user's config shape (shorthand → full form, 828 lines) and `defaults.js` fills mode/target-dependent values (2,889 lines); `applyWebpackOptionsBaseDefaults` runs before plugins and `applyWebpackOptionsDefaults` after.
- `NormalModuleFactory.create` builds a single mutable `ResolveData` object carrying `contextInfo`, `request`, `phase`, import `attributes`, `createData`, and three `LazySet`s of file/missing/context dependencies, then drives it through `beforeResolve` → `factorize` → `resolve` → `afterResolve` → `createModule` → `module`.
- Loader selection is a compiled rule set: `NormalModuleFactory`'s constructor compiles `options.defaultRules` and `options.rules` together via `ruleSetCompiler.compile`, so `module.rules` matching is a precompiled matcher rather than a per-request scan.
- Source text enters through `NormalModule._doBuild`, which calls the vendored `runLoaders` with a `processResource` callback; the loader chain is executed as a pitching pass then a normal pass (`iteratePitchingLoaders` → `processResource` → unwind), and each loader module is loaded by `loadLoader`.
- The loader chain can bypass parsing entirely: a loader may return `webpackAST` in its `additionalData`, which `LoaderRunner` types as `AdditionalDataKnown.webpackAST` — "the AST webpack should parse instead of the source".
- Cacheability is data that flows back out of the loader chain: `RunLoaderResult` carries `cacheable`, `notCacheableReasons`, and the `fileDependencies`/`contextDependencies`/`missingDependencies` lists that `FileSystemInfo` later snapshots to decide staleness.
- State ownership is explicit: the `Compilation` owns `modules`, `entries`, `chunks`, the `ModuleGraph` and `ChunkGraph`, and five work queues (`factorizeQueue`, `buildQueue`, `rebuildQueue`, `processDependenciesQueue`, `addModuleQueue`) which `seal`'s `finalCallback` clears on completion.
- `seal` freezes the module graph with `this.moduleGraph.freeze("seal")` before building chunks, then materializes one `Chunk` + `Entrypoint` per entry and seeds `chunkGraphInit` with each entry's resolved modules, validating `dependOn`/`runtime` conflicts and circular `dependOn` into `compilation.errors`.
- After chunking, the ordered tail of `seal` is `processRuntimeRequirements` → `_attachPendingRuntimeModules` → `createHash` (which returns the code-generation jobs) → `_runCodeGenerationJobs` → `createModuleAssets` → `createChunkAssets` → `processAssets` → freeze `this.assets` → `summarizeDependencies` → `afterSeal`.
- `this.assets` is frozen at the end of `processAssets` — literally `Object.freeze` when `backCompat` is off, and a `soonFrozenObjectDeprecation` proxy carrying `DEP_WEBPACK_COMPILATION_ASSETS` when it is on — so post-seal asset mutation is a hard boundary.
- The output boundary is `Compiler.emitAssets`, which writes at most 15 assets concurrently, recomputes `immutable` when a query/hash suffix is stripped, special-cases Windows drive-absolute target paths via `WINDOWS_ABS_PATH_REGEXP`, and refuses to write two assets whose paths differ only by casing or query string.
- Emitted-asset writes are deduplicated through two caches on the compiler — `_assetEmittingWrittenFiles` (path → generation) and `_assetEmittingSourceCache` (Source → `{ sizeOnlySource, writtenTo }`) — so an unchanged `Source` in a watch rebuild is compared rather than rewritten.
Behavior traces
Four flows are worth walking: a one-shot `webpack(config, cb)` run, a single module's resolve→build→parse path, a watch rebuild, and the persistent-cache store/restore path. Each is a callback chain rather than promises, with `tapable` hooks at every joint and `logger.time`/`timeEnd` pairs marking the phases you see in `stats` profiling output. The names below are the actual functions in call order.
- Startup (one-shot): `webpack(options, cb)` → `create()` → schema check → `createCompiler` → `compiler.run(cb)`; `run` calls `cache.endIdle` if idle, then `hooks.beforeRun` → `hooks.run` → `readRecords` → `compile(onCompiled)`.
- `Compiler.compile` runs `newCompilationParams()` → `hooks.beforeCompile` → `hooks.compile` → `newCompilation(params)` → `hooks.make` → `hooks.finishMake` → `process.nextTick` → `compilation.finish` → `compilation.seal` → `hooks.afterCompile`, timing each phase with the `webpack.Compiler` logger.
- Completion: `onCompiled` checks `hooks.shouldEmit`; if emitting it defers a `process.nextTick`, calls `emitAssets`, re-enters `compile` when `needAdditionalPass` is true, then `emitRecords` → build `new Stats(compilation)` → `hooks.done` → `cache.storeBuildDependencies` → `finalCallback`, which sets `cache.beginIdle()` and fires `hooks.afterDone`.
- The callback form of `webpack()` always closes the compiler for you — `compiler.run((err, stats) => compiler.close((err2) => callback(err || err2, stats)))` — which is why plugin authors using the no-callback form must call `close` themselves.
- Module creation: `Compilation.handleModuleCreation` → `factorizeQueue` → `NormalModuleFactory.create` → `hooks.beforeResolve` (a `false` result short-circuits to an ignored module) → `hooks.factorize`/`resolveResource` → `hooks.afterResolve` → `hooks.createModule`/`createModuleClass` → `hooks.module` waterfall.
- Module build: `NormalModule.build` → `_doBuild` → `runLoaders` (pitch pass, `processResource`, normal pass) → on success the parser runs and dependencies are collected; every failure path funnels through `markModuleAsErrored(err)` followed by `_initBuildHash(compilation)` so an errored module still has a hash.
- Parser and generator instances are resolved per module type through `NormalModuleFactory.getParser`/`createParser` and `getGenerator`/`createGenerator`, cached in `parserCache`/`generatorCache` as `Map<type, WeakMap<options, instance>>`, and a type's one-time async setup runs through the `prepareModuleType` HookMap with in-flight callers queued.
- Seal, in order: clear queues in `finalCallback`, optionally re-attach `ChunkGraph` per module for `backCompat`, `hooks.seal`, loop `hooks.optimizeDependencies` until it returns falsy, `hooks.beforeChunks`, `moduleGraph.freeze("seal")`, entry→chunk creation with `assignDepths`, then the optimize/hash/codegen/assets tail.
- Watch rebuild: `Watching._invalidate` → `_go(fileTimeInfoEntries, contextTimeInfoEntries, changedFiles, removedFiles)` → `compiler.compile` → `_done(err, compilation)`, which re-enters `_go` when `this.invalid` was set during the run; `_done` is pre-bound in the constructor and the first pass is kicked off by `_invalidate` when `_initial`.
- Persistence: `IdleFileCachePlugin` taps `cache.hooks.store` to queue `strategy.store(identifier, etag, data)` and `cache.hooks.get` to `strategy.restore(...)`, then flushes on an idle timer whose delay is the smaller of `idleTimeout` and `idleTimeoutForInitialStore`, with `idleTimeoutAfterLargeChanges` as a third setting.
- A cache pack is read lazily by location: `Pack.get(identifier, etag)` looks the identifier up in `itemInfo`, finds its `location`, and only then materializes that `PackContent`'s map — so restoring one module does not deserialize the whole pack.
- Shutdown: `Compiler.close` first closes any active `watching`, fires `hooks.shutdown`, then defers `_releaseUnusedCompilationData(lastCompilation)` behind `Promise.resolve().then(...)` so a `close()` made inside the run callback cannot release `codeGenerationResults` before `afterDone` fires on the same stack, and finally calls `cache.shutdown(callback)`.
Testing signals
Jest is the only runner and it must be invoked through the `test:base` wrapper, which supplies `--experimental-vm-modules` (bare jest crashes the ESM and test262 suites). Coverage is broad and integration-first: ~2,257 leaf `configCases/` directories, 148 `*.unittest.js` files, 144 `statsCases/`, 37 `hotCases/`, 23 `watchCases/`, 44 `benchmarkCases/`, plus four upstream spec corpora pulled in as git submodules. The same `configCases/` corpus is re-run under ~20 option variants declared in one file, and again under `cache.type: "filesystem"` by `ConfigCacheTestCases`, which is what makes cache correctness a tested property rather than a hope. The main gaps are environment-dependent: browser tiers, Deno/Bun runtimes and spec corpora only run where their prerequisites exist, and three `lib/*Minify.js` files are explicitly excluded from coverage instrumentation.
- `jest.config.js` matches five patterns (`test/*.test.js`, `*.basictest.js`, `*.longtest.js`, `unitCases/*.unittest.js`, `specCases/*.spectest.js`), uses a custom `testEnvironment` (`test/harness/patch-node-env.js`), a custom `snapshotResolver`, a 30s default timeout and `forceExit`.
- Three `minify` entry points are deliberately excluded from coverage — `lib/css/cssMinify.js`, `lib/html/htmlMinify.js`, `lib/javascript/jsMinify.js` — because they are serialized to the minimizer worker pool as source and instrumentation counters would not resolve there.
- The suite is built from thin shims over four drivers: `test/*.test.js`/`*.basictest.js`/`*.longtest.js` each call `describeCases(config)` from `test/templates/{TestCases,ConfigTestCases,HotTestCases,WatchTestCases}.js` — e.g. `ConfigTestCases.basictest.js` is five lines.
- The option-set matrix lives in one file, `test/templates/variants.js`: `normal`, `development`, `production`, `production-global-used`, `module` (target node14 + `module: true`), `hot`, nine devtool variants, `minimized-source-map`, and `cache-pack` — one `test/` shim per entry because jest parallelizes per file.
- Scale: 2,257 leaf directories under `test/configCases/` across 107 categories, 148 files in `test/unitCases/`, 144 `statsCases/`, 37 `hotCases/`, 23 `watchCases/`, 44 `benchmarkCases/`, 4 `typesCases/` and 1 `memoryLimitCases/`.
- Cache correctness is enforced by re-running the whole config corpus: `ConfigCacheTestCases.basictest.js` runs every `configCases/` case with `cache.type: "filesystem"` and fails on `Pack got invalid because of write to: <identifier>`, so a class missing `makeSerializable` surfaces as a test failure rather than as a user's slow rebuild.
- Spec conformance is imported wholesale via four git submodules under `test/external/` — test262, html5lib-tests, WPT (~161k files, shallow-cloned) and css-parsing-tests — each with its own `specCases/*.spectest.js` runner and its own CI job.
- acorn's own corpus is vendored rather than submoduled (`test/fixtures/acorn-corpus.json`) because acorn's npm tarball ships no tests; `test/unitCases/WebpackParser.unittest.js` both replays it against webpack's parser and regenerates it under `WEBPACK_UPDATE_ACORN_CORPUS=1`.
- Non-Node runtimes are covered by dedicated wrappers: `test:base:deno` runs jest under Deno with an explicit permission list and an import map, `test:base:bun` runs it under `bun --bun --smol --workerThreads` with a preload file, both routed through `test/harness/runtimeCrashReporter.js`.
- Assertion helpers are centralized in `test/helpers/` (40+ files): `checkArrayExpectation` for per-case `errors.js`/`warnings.js`, `deprecationTracking`/`expectNoDeprecations`, `FakeDocument`/`createFakeWorker`/`createFakeWorklet` for browser-API simulation, and `launchBrowser` for the syntax-equivalence tiers.
- CI gates the expensive suites on three cheap ones — `integration`, `test262`, `parser`, `syntax-equivalence`, the Deno/Bun matrix and the benchmark shards all declare `needs: [lint, basic, unit]`, so a red lint leaves most of the run reporting `skipped` rather than a result.
- The integration matrix retries once before failing (`yarn cover:integration:${part} --ci … || yarn cover:integration:${part} --ci … -f`) and only uploads coverage on the non-legacy Node versions, so Node 10/12/14/16/18/20 legs run `test:integration` without instrumentation.
Risk map
webpack's real security surface is that it executes arbitrary third-party code by design — loaders, plugins and config files all run with full Node privileges in the developer's process — so the interesting risks are the paths that widen that, not the bundler core. Concretely: `HttpUriPlugin` fetches remote code over the network, `lazyCompilationBackend` opens a listening HTTP server, `CleanPlugin` deletes files, `DotenvPlugin` reads secrets into the bundle, and the cache deserializer reconstructs arbitrary registered classes from disk. Operationally, the largest fragility is size and coupling: `Compilation.js` is 6,724 lines with a seal pipeline whose phase ordering is load-bearing, the three `syntax-parser.js` files total ~46k lines with V8-inlining-budget constraints baked into specific methods, and several correctness properties (cache round-trip, generated-file freshness, deep-import compatibility) are enforced only by conventions that a careless edit silently violates.
- Arbitrary code execution is the design, not a bug: `loadLoader` requires loader modules from the resolved graph and `runLoaders` invokes them with a full loader context, so any dependency reachable as a loader runs with the developer's privileges — there is no sandbox in this path.
- `HttpUriPlugin` is the highest-value security path: it fetches module source over `http:`/`https:`, routes through `options.proxy` or the `http_proxy`/`HTTP_PROXY` environment variables, and its `allowedUris` policy must be re-checked after every redirect — the redirect check at L773-790 is the control that stops an allowed host from redirecting to an arbitrary one.
- Fetched remote bodies are written to disk under `cacheLocation` (defaulting to `<lockfileLocation>.data`) keyed by a computed `key`, so a poisoned or unvalidated response becomes a persisted build input.
- `lazyCompilationBackend` binds a real HTTP/HTTPS listener in the dev process and keeps a `Set` of live sockets; its request handler mutates an `activeModules` refcount from client-supplied keys, so exposure of that port exposes compile triggers.
- `CleanPlugin` performs `unlink` and `rmdir` under `output.path` based on a filesystem diff, with `keep` (a `SyncBailHook` plus user matcher) and `dry` as the only brakes — a misconfigured `output.path` makes this a destructive operation on a directory the user did not intend.
- `DotenvPlugin` reads `.env`, `.env.local`, `.env.[mode]` and `.env.[mode].local` and feeds the parsed, `dotenv-expand`-expanded values into `DefinePlugin` — i.e. straight into the emitted bundle — so a missing `prefix` filter ships secrets to the browser.
- The cache deserializer instantiates classes named by strings read off disk: `registerLoader(/^webpack\/lib\//, …)` maps a request to a loader in the generated `internalSerializables`, and an unknown request only produces a `console.warn` — a cache file is therefore a trusted input.
- Deserialization re-enters constructors with no arguments, so a constructor that dereferences a parameter throws during restore and manifests only as a `Restoring failed for <identifier> from pack` debug line — a silent-by-default failure mode documented as a recurring trap.
- Blast-radius hotspot #1 is `lib/Compilation.js` (6,724 lines): a single class owning the module graph, chunk graph, five async queues, the whole seal pipeline, asset emission and the hook surface — any reordering inside `seal` changes what plugins observe.
- Blast-radius hotspot #2 is the parser trio: `javascript/syntax-parser.js` (17,844 lines), `css/syntax-parser.js` (14,973) and `html/syntax-parser.js` (13,197), whose hot methods (`readWord`, `readString`, `finishToken`) are sized to fit V8's ~460-byte inlining budget — adding one argument to `readWord` once cost 1.4%.
- Fragile coupling: five `lib/*/data.js` modules and every `types.d.ts`/`declarations/*.d.ts`/`schemas/**/*.check.js` file are generated, and the `lint` job fails with only `… need to be updated` — a message that reads like a code problem even when the cause is a reworded JSDoc paragraph.
- Moving any file in `lib/` risks breaking the published ecosystem's deep imports; the only record of which paths are load-bearing is `tooling/deep-webpack-imports.json` (with `perKeyword`, `requests` and a `removed` map), checked by a CI job and mirrored by `test/unitCases/deepPathShims.unittest.js`.
Extension points
Extension is the architecture: everything user-visible is a plugin that taps `tapable` hooks, and webpack's own features are implemented the same way user plugins are. The four main seams are (1) the hook objects on `Compiler`, `Compilation` and `NormalModuleFactory`, including `HookMap`s keyed by module type; (2) the `Dependency` + `DependencyTemplate` + `makeSerializable` triple for new dependency kinds; (3) `RuntimeGlobals` + `RuntimeModule` + `runtimeRequirementInTree` for new runtime code; and (4) the plugin-based `RuleSetCompiler` for new `module.rules` vocabulary. Generators of tables and types are themselves extension points — a new CSS math function or HTML attribute is a descriptor added to a `tooling/generate-*-data.js` SUPPLEMENT, not code added to a printer.
- `NormalModuleFactory.hooks` is the richest factory seam: `resolve`, `factorize`, `beforeResolve`, `afterResolve`, `createModule`, `module`, plus five `HookMap`s keyed by scheme or module type (`resolveForScheme`, `resolveInScheme`, `prepareModuleType`, `createParser`/`parser`, `createGenerator`/`generator`, `createModuleClass`), several annotated with `@since` versions.
- New URL schemes plug in through `resolveForScheme`/`resolveInScheme` — the pattern used by `DataUriPlugin`, `FileUriPlugin`, `HttpUriPlugin` and `VirtualUrlPlugin` in `lib/schemes/`.
- A new module language is a parser/generator pair registered on the type-keyed HookMaps and cached per options object in `parserCache`/`generatorCache`; `lib/module/Parser.js` and `lib/module/Generator.js` are the base classes, both re-exported publicly as `webpack.Parser` / `webpack.Generator`.
- A new dependency type is the documented triple: a `Dependency` subclass in `lib/dependencies/`, a `DependencyTemplate` registered into `compilation.dependencyTemplates`, and a `makeSerializable(Class, "webpack/lib/…")` call — the pattern appears in 144 files.
- New runtime code plugs in via `lib/runtime/RuntimeGlobals.js` (97 exported symbols), a `RuntimeModule` subclass, and a tap on `runtimeRequirementInTree`/`additionalTreeRuntimeRequirements`; `RuntimePlugin` is what injects modules for the requirements a build collects.
- `module.rules` vocabulary is itself pluggable: `RuleSetCompiler` is constructed from rule plugins (`BasicMatcherRulePlugin`, `ObjectMatcherRulePlugin`, `GlobMatcherRulePlugin`, `BasicEffectRulePlugin`, `UseEffectRulePlugin`), so a new condition or effect keyword is a new plugin rather than a branch in the matcher.
- Target support is a table-driven registry: adding a target means adding a `[name, description, regExp, handler]` tuple to `TARGETS` in `lib/config/target.js`, and `mergeTargetProperties` plus `UNSUPPORTED_WHEN_ABSENT` decide how multi-target arrays like `["web", "node"]` resolve each capability.
- Chunk-loading, wasm-loading and library formats are enabled through dedicated dispatch plugins — `EnableChunkLoadingPlugin`, `EnableWasmLoadingPlugin`, `EnableLibraryPlugin` — which `WebpackOptionsApply` calls per configured type, so a new format registers itself with one of these rather than editing the option branch.
- Stats output is extensible through the factory/preset/printer split: `DefaultStatsFactoryPlugin` builds the object graph, `DefaultStatsPresetPlugin` defines named presets, and `DefaultStatsPrinterPlugin` renders it — each a separate tap point.
- Module Federation exposes its own hook surface in `lib/container/moduleFederationHooks.js` and `lib/hmr/parserHooks.js` does the same for HMR, so third-party federation/HMR tooling taps a published hook object rather than reaching into the plugin.
- Caches are pluggable at the `Cache`/`CacheFacade` layer: `MemoryCachePlugin`, `MemoryWithGcCachePlugin`, `IdleFileCachePlugin` and `ResolverCachePlugin` all tap `compiler.cache.hooks.{get,store,storeBuildDependencies}`, so an alternative backing store is a plugin.
- Per-construct minifier behaviour is table-driven by design: the generators own the descriptors (`MATH_FUNCTION_FOLD` in `tooling/generate-css-data.js`, `SUPPLEMENT`/`PARSER_TABLES` in `generate-html-data.js`) and the printers implement only the shared engine keyed by them, so adding a foldable function is adding a generator entry.
Contributor handoff
This repo has an unusually strict, written contract in `AGENTS.md` (symlinked as `CLAUDE.md`) with `> [!REQUIRED]` sections that reviewers enforce literally — read it before touching anything. The three rules that bite hardest: never invoke bare `jest` (use `yarn test:base`), never hand-edit generated files (run `yarn fix:special` and diff), and never sweep whole suites locally (push and read CI). Edits are safe inside `lib/` subdirectories and `test/`; they are dangerous at `lib/` root (deep-import breakage), inside generated `data.js`/`types.d.ts`/`*.check.js`, and in the hot parser methods sized to V8's inlining budget. Every user-facing change needs a `.changeset/NNN-*.md` entry and, for bug fixes, a failing test written first.
- Read first, in order: `AGENTS.md` (the binding contract, including the mandatory PR template), `TESTING_DOCS.md` (suite layout and how to run one case), `lib/index.js` (the public surface), `lib/config/WebpackOptionsApply.js` (option→plugin map), and `lib/Compilation.js#L3612-3999` (the seal pipeline everything else hangs off).
- `lib/` is CommonJS only with types as JSDoc `@typedef`/`@import` — no `import`/`export` syntax, no TypeScript syntax in `.js` files — and the annotations are compiled to `types.d.ts` by `yarn fix:special`.
- Adding or renaming an option requires four ordered edits plus regeneration: `schemas/WebpackOptions.json` → `lib/config/defaults.js` → `lib/config/normalization.js` → the consuming site → `yarn fix:special`, then the snapshots the option's name leaks into (`test/__snapshots__/Cli.basictest.js.snap`, `Defaults.unittest.js`, `Validation.unittest.js`, the browserslist config cases).
- Never hand-edit the generated set: `types.d.ts`, `declarations/**/*.d.ts`, `schemas/**/*.check.{js,d.ts}`, generated runtime code, and `lib/{css,html,javascript}/data.js`. A new lookup table belongs in the matching `tooling/generate-*-data.js`, derived from a published dataset where possible.
- Run targeted tests only: `yarn test:base --testPathPatterns="<pattern>"` or `yarn test:basic --testPathPatterns="ConfigTestCases" --testNamePattern="<category> <case>"`. Never pipe a jest run through `grep`/`tail` — that discards the exit code and a red run reads as green.
- A snapshot can live in two suites: `ConfigTestCases` and `ConfigCacheTestCases` both snapshot `configCases/`, and `--testPathPatterns=ConfigTestCases` misses the second — update with `yarn test:basic --testNamePattern="<case>" -u` and no path filter.
- Writing a `configCases/` case: it is a mini project (`index.js` with assertions, `webpack.config.js`), the emitted bundle is actually executed in a `vm`, and optional `errors.js`/`warnings.js`/`test.filter.js`/`test.config.js` files control expected diagnostics, skipping and `findBundle`.
- Never silence the cache suite with `test.filter.js` returning `!config.cache`; the sanctioned escape is an `infrastructure-log.js` returning `[/Pack got invalid because of write to/]` for cases whose subject is a build error (a module with `this.error` set always rebuilds by design).
- Moving a file out of `lib/` root has six carriers to rewrite — `require`/`require.resolve`, `@import` JSDoc, `@typedef {import(...)}`, `tsType` in schemas, the `makeSerializable` request, and paths written into `tooling/generate-runtime-code.js` or `eslint.config.mjs` — and `yarn find-deep-imports:check` must be run before the commit.
- Comment budget is enforced by lint: a plain `//` or `/* */` comment is capped at three lines by the `webpack/comment-length` rule (JSDoc tag blocks, the file-header block, and `// WHY:` comments are the exemptions), and `examples/` is the only directory excluded.
- Deferred work uses exactly one wording — `TODO in the next major release: <what to do>` — never a version number, so `grep -rn "TODO in the next major release" codecov.yml bin hot lib setup test tooling` lists the whole backlog.
- Never enumerate module/source/dependency types in `lib/`: ask the object (`module.getSourceTypes()`, `chunkGraph.getModuleSourceTypes(module)`, `moduleGraph.getParentModule(dependency)`) or match the class (`dependency instanceof URLDependency`), and when a branch must name a type, write it so an unknown type takes the safe side.
Languages
| Language | Files | Share of source |
|---|---|---|
| JavaScript | 16040 | 99% |
| CSS | 976 | docs/config |
| JSON | 688 | docs/config |
| HTML | 410 | docs/config |
| Markdown | 242 | docs/config |
| TypeScript | 108 | 1% |
| YAML | 25 | docs/config |
| Shell | 4 | 0% |
Start reading here
Probable source entrypoint: lib/ — inferred from the directory tree and manifest, not the upstream docs.
Source roots
lib/ (771 files)
Repository structure
| Top-level path | Files |
|---|---|
test/ | 2199 |
lib/ | 771 |
examples/ | 763 |
schemas/ | 122 |
.changeset/ | 42 |
tooling/ | 25 |
.github/ | 21 |
hot/ | 12 |
Docs found: AGENTS.mdCONTRIBUTING.mdREADME.md
Package
package.json — webpack v5.111.1, 80 declared dependencies.
@babel/core@babel/preset-react@changesets/cli@changesets/get-github-info@codspeed/core@mdn/browser-compat-data@puppeteer/browsers@types/convert-source-map@types/estree@types/graceful-fs@types/jest@types/json-schema
CI & config entrypoints
README.md— Repository readmeAGENTS.md— Agent instructions.github/workflows/benchmarks.yml— GitHub Actions workflow.github/workflows/code-size.yml— GitHub Actions workflow.github/workflows/codeql.yml— GitHub Actions workflow.github/workflows/deep-imports.yml— GitHub Actions workflow.github/workflows/dependabot.yml— GitHub Actions workflow.github/workflows/dependency-review.yml— GitHub Actions workflow.github/workflows/examples.yml— GitHub Actions workflow.github/workflows/labeler.yml— GitHub Actions workflow
Structural health signals
Average score 6.5/10 across 96 analyzed source files; 47 hotspots surfaced.
lib/optimize/SideEffectsFlagPlugin.js— 962 lines, score 4 (Large file, Deep nesting)lib/cache/PackFileCacheStrategy.js— 1669 lines, score 4.7 (Large file, Deep nesting)lib/schemes/HttpUriPlugin.js— 1527 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
87a62dec23042026-09-22 — refactor: move the last single-owner helpers out of lib/util (#22253)95930fdc7cb62026-09-22 — test(javascript): assert what load() owes, not what Deno can import (#22252)dd69da694de72026-09-22 — build(ci): install the JavaScript comparison corpus on every pull request (#22250)306c34ffe0b92026-09-22 — fix(html): read a foreign script body as character data (#22249)69cc45e3d86c2026-09-22 — fix(css): read an rgb() separator grammar before folding it (#22251)