191 lines
6.9 KiB
Markdown
191 lines
6.9 KiB
Markdown
# 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:
|
|
|
|
1. **Bundle directory** (`bundles/<name>/`) — has its own `package.json` with the dependencies to vendor, plus a `src/<name>BundleImpl.ts` entry point that imports and re-exports them.
|
|
2. **Build configuration** in `utils/build/build.js` — an esbuild entry that bundles the impl file into a single minified CJS file.
|
|
3. **Wrapper file** (`src/<name>Bundle.ts`) — a thin typed wrapper that `require()`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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```typescript
|
|
// 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):
|
|
|
|
```javascript
|
|
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 to `lib/<name>BundleImpl.js` (most bundles use this)
|
|
- `outfile` — output goes to `lib/<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:
|
|
|
|
```typescript
|
|
// 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:
|
|
```javascript
|
|
// in build.js
|
|
outdir: 'packages/<package>/lib/common',
|
|
```
|
|
|
|
### 6. Build and verify
|
|
|
|
```bash
|
|
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:
|
|
|
|
```typescript
|
|
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):
|
|
|
|
1. Add the dependency to the existing `bundles/<name>/package.json`
|
|
2. Run `npm install` in that bundle directory
|
|
3. Add the import/export to the existing `src/<name>BundleImpl.ts`
|
|
4. 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.
|