6.9 KiB
Vendoring (Bundling) a New Dependency
Playwright vendors third-party npm packages by bundling them with esbuild into self-contained files. This isolates dependencies, prevents version conflicts, and keeps the published packages lean.
Architecture Overview
Each bundle lives under packages/<package>/bundles/<name>/ and consists of three parts:
- Bundle directory (
bundles/<name>/) — has its ownpackage.jsonwith the dependencies to vendor, plus asrc/<name>BundleImpl.tsentry point that imports and re-exports them. - Build configuration in
utils/build/build.js— an esbuild entry that bundles the impl file into a single minified CJS file. - Wrapper file (
src/<name>Bundle.ts) — a thin typed wrapper thatrequire()s the built bundle impl and re-exports symbols with TypeScript types.
Data flow:
bundles/<name>/package.json (declares npm deps)
→ npm ci → node_modules/
bundles/<name>/src/<name>BundleImpl.ts (imports from node_modules, re-exports)
→ esbuild (bundle + minify) →
lib/<name>BundleImpl.js (single self-contained file)
←
src/<name>Bundle.ts (typed wrapper, require('./...BundleImpl'))
→ esbuild (normal compile) →
lib/<name>Bundle.js (used by application code)
Step-by-Step: Adding a New Bundle
Decide which package it belongs to
packages/playwright-core/bundles/— for core browser automation deps (networking, compression, protocols, etc.)packages/playwright/bundles/— for test runner deps (assertion libs, transpilers, file watchers, etc.)
1. Create the bundle directory
packages/<package>/bundles/<name>/
├── package.json
└── src/
└── <name>BundleImpl.ts
2. Create package.json
Minimal private package with only the deps you want to bundle:
{
"name": "<name>-bundle",
"version": "0.0.1",
"private": true,
"dependencies": {
"some-lib": "^1.2.3"
},
"devDependencies": {
"@types/some-lib": "^1.2.0"
}
}
Then run npm install inside the bundle directory to generate package-lock.json.
3. Create src/<name>BundleImpl.ts
This is the esbuild entry point. Import from node_modules and re-export:
// For default exports:
import someLibrary from 'some-lib';
export const someLib = someLibrary;
// For named exports:
export { SomeClass } from 'some-lib';
// For namespace imports:
import * as someLibrary from 'some-lib';
export const someLib = someLibrary;
// For vendored/third-party code that can't be bundled:
const custom = require('./third_party/custom');
export const customThing = custom;
4. Register the bundle in utils/build/build.js
Add an entry to the bundles array (around line 246):
bundles.push({
modulePath: 'packages/<package>/bundles/<name>',
entryPoints: ['src/<name>BundleImpl.ts'],
// Use outdir for a single .js file alongside other lib files:
outdir: 'packages/<package>/lib',
// OR use outfile for output in a subdirectory (needed if bundle has non-JS assets):
// outfile: 'packages/<package>/lib/<name>BundleImpl/index.js',
// Optional: deps that should NOT be bundled (must be installed at runtime):
// external: ['express'],
// Optional: redirect imports to custom implementations:
// alias: { 'some-module': 'custom-impl.ts' },
});
outdir vs outfile:
outdir— output goes tolib/<name>BundleImpl.js(most bundles use this)outfile— output goes tolib/<name>BundleImpl/index.js(use when you need to copy companion files like binaries next to the bundle)
5. Create the typed wrapper src/<name>Bundle.ts
This file lives in the main package source (NOT in the bundle directory). It provides TypeScript types while loading the bundled code at runtime:
// packages/<package>/src/<name>Bundle.ts
// (or src/subdir/<name>Bundle.ts if it belongs in a subdirectory)
export const someLib: typeof import('../bundles/<name>/node_modules/some-lib')
= require('./<name>BundleImpl').someLib;
export const SomeClass: typeof import('../bundles/<name>/node_modules/some-lib').SomeClass
= require('./<name>BundleImpl').SomeClass;
// Re-export types if needed:
export type { SomeType } from '../bundles/<name>/node_modules/some-lib';
The pattern is: typeof import('../bundles/<name>/node_modules/...') for the type, require('./<name>BundleImpl').<export> for the value.
If the wrapper lives in a subdirectory (e.g. src/common/<name>Bundle.ts), adjust the outdir accordingly so the BundleImpl ends up next to the compiled wrapper:
// in build.js
outdir: 'packages/<package>/lib/common',
6. Build and verify
npm run build
Or if watch is running, it will pick up changes automatically.
7. Use the bundle in application code
Import from the wrapper file, never from the bundle directory or node_modules directly:
import { someLib } from '../<name>Bundle';
Existing Bundles Reference
playwright-core bundles
| Bundle | Deps | Output |
|---|---|---|
utils |
colors, commander, debug, diff, dotenv, graceful-fs, https-proxy-agent, jpeg-js, mime, minimatch, open, pngjs, progress, proxy-from-env, socks-proxy-agent, ws, yaml | lib/utilsBundleImpl/index.js |
zip |
yauzl, yazl, get-stream, debug | lib/zipBundleImpl.js |
mcp |
@modelcontextprotocol/sdk, zod, zod-to-json-schema | lib/mcpBundleImpl/index.js |
playwright bundles
| Bundle | Deps | Output |
|---|---|---|
utils |
chokidar, enquirer, json5, source-map-support, stoppable, unified, remark-parse | lib/utilsBundleImpl.js |
babel |
~30 @babel/* packages | lib/transform/babelBundleImpl.js |
expect |
expect, jest-matcher-utils | lib/common/expectBundleImpl.js |
Advanced Patterns
Adding a dep to an existing bundle
If the dep logically belongs with an existing bundle (e.g. a new utility lib → utils bundle):
- Add the dependency to the existing
bundles/<name>/package.json - Run
npm installin that bundle directory - Add the import/export to the existing
src/<name>BundleImpl.ts - Add the typed re-export to the existing
src/<name>Bundle.ts
Vendored third-party code
If a package can't be bundled by esbuild (e.g. it uses dynamic requires or has runtime file dependencies), place a modified copy in bundles/<name>/src/third_party/ and require it from the BundleImpl. See bundles/zip/src/third_party/extract-zip.js for an example.
External dependencies
Use external: ['pkg'] in the build.js config when a dependency should NOT be bundled — e.g. optional peer deps that users install themselves. These must be available at runtime in the consumer's node_modules.
Module aliases
Use alias: { 'module-name': 'local-file.ts' } to replace a dependency with a custom local implementation. The alias path is relative to the bundle's modulePath. See the mcp bundle's raw-body alias for an example.