All skills
microsoft avatar

/playwright-dev

@08a37f2
by microsoftmicrosoft/playwright97k stars
6,529

Explains how to develop Playwright - add APIs, MCP tools, CLI commands, and vendor dependencies.

Use this Skill: https://skilld.dev/gh/microsoft/playwright/playwright-dev

This session only. Nothing lands on disk.

vendor.md

≈2.8k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Vendor Dependencies & Bundling

Playwright ships a small number of node_modules inlined into a handful of pre-built "bundle" files under lib/. Everything else is either a source file compiled per-file, or loaded at runtime from one of the bundles. This doc covers how the bundling works, how to add or move a vendored package, and how the dependency checker enforces the contract.

The Bundles

playwright-core

Output Entry Purpose
lib/utilsBundle.js src/utilsBundle.ts Vendored npm packages (debug, mime, ws, yauzl, yazl, @modelcontextprotocol/sdk, graceful-fs, …). The single home for third-party runtime code in playwright-core.
lib/coreBundle.js src/coreBundle.ts Re-exports of playwright-core's own modules (client, iso, utils, cli, server, registry, …) as namespaces. Inlines almost all playwright-core source except utilsBundle.
lib/server/electron/loader.js src/server/electron/loader.ts Tiny Electron preload shim.

The dynamicImportToRequirePlugin in utils/build/build.js rewrites vendored npm imports at bundle time. For example, a playwright-core source file containing

import debug from 'debug';

gets rewritten to

const debug = require('./utilsBundle').debug;

before the bundler sees it — so the vendored package never gets inlined into coreBundle.js. The mapping from npm package name to utilsBundle export key lives in utils/build/utilsBundleMapping.js.

playwright

Output Entry Purpose
lib/transform/babelBundle.js src/transform/babelBundle.ts Wraps @babel/core, @babel/traverse, @babel/code-frame, plugins. Shared by every consumer that needs babel.
lib/transform/esmLoader.js src/transform/esmLoader.ts Node ESM loader registered via node:module.register(). Output sits next to babelBundle.js so its ./babelBundle sibling require resolves correctly.
lib/common/index.js src/common/index.ts Barrel of common/* + transform/* (compilationCache, test, configLoader, fixtures, globals, …). State-holding singletons (currentTestInfo, memoryCache, …) live here.
lib/runner/index.js src/runner/index.ts Barrel of runner/* + reporters/* + plugins/*.
lib/matchers/expect.js src/matchers/expect.ts Jest-style matchers with expect inlined.
lib/worker/workerProcessEntry.js src/worker/workerProcessEntry.ts Entry point spawned per test worker.
lib/loader/loaderProcessEntry.js src/loader/loaderProcessEntry.ts Entry point for the test file loader sub-process.
lib/runner/uiModeReporter.js src/runner/uiModeReporter.ts Loaded by require.resolve from testServer; passed to child workers as a file path.

The common and runner bundles externalize ../transform/babelBundle (among other things) so babel code is not duplicated across them. The lib/transform/transform.ts module uses libPath('transform', 'babelBundle') (absolute path via package.ts root) to load the babel bundle at runtime, so it works regardless of which bundle has inlined it.

Per-file emits (no bundle)

Files outside the bundled entries are compiled 1:1 by esbuild and land under lib/ mirroring their source layout. The per-file step in utils/build/build.js lists the specific directories for the playwright package (cli/, agents/, mcp/, root *.ts, and a few targeted files like runner/uiModeReporter.ts). Other packages (playwright-test, html-reporter, trace-viewer, …) are compiled by the generic per-package loop.

Bundle Sidecars

Every bundled output has two sidecar files next to it:

  • <bundle>.js.txt — human-readable report listing inlined files (sorted by path, with per-file KB sizes), externals, and total bytes. Written by utils/build/bundle_report.js.
  • <bundle>.js.LICENSE — third-party license texts for every npm package whose source got inlined. Populated from license-checker, memoized once per build invocation. Consumed by the top-level ThirdPartyNotices.txt files, which just point readers at the per-bundle sidecars.

Both sidecars are included in the published npm package (controlled by packages/*/.npmignore).

Adding a Vendored NPM Dependency

Three pieces need to line up when adding a new npm package that you want inlined into utilsBundle (i.e., loaded through require('./utilsBundle').<key>):

  1. Install the package. Add it to the root package.json devDependencies. The monorepo root is where esbuild resolves modules from; the workspace root's node_modules/<pkg> is what gets inlined into utilsBundle.js.

  2. Export it from src/utilsBundle.ts. Pick one of:

    import fooLibrary from 'foo';
    export const foo = fooLibrary;             // default
    
    import * as fooLibrary from 'foo';
    export const foo = fooLibrary;             // namespace
    
    export { namedSymbol } from 'foo';         // named

    Type-only exports (export type { X } from 'foo') are valid and don't affect runtime.

  3. Add a mapping entry to utils/build/utilsBundleMapping.js:

    'foo': { default: 'foo' },
    // or:
    'foo': { namespace: 'foo' },
    // or:
    'foo': { named: { namedSymbol: 'fooNamedSymbol' } },
    • default — matches import foo from 'foo' and rewrites to require('./utilsBundle').foo.
    • namespace — matches import * as foo from 'foo'.
    • named — matches import { namedSymbol } from 'foo' and rewrites to const { fooNamedSymbol: namedSymbol } = require('./utilsBundle').
    • Multiple forms can coexist in one entry (see yauzl).
    • The map key is the exact npm specifier as written in source (including subpaths like '@babel/core' or 'colors/safe').
  4. Update DEPS.list. The file or its enclosing folder's DEPS.list must authorize node_modules/<pkg> — otherwise npm run flint's check_deps step complains about the disallowed external dependency. If the DEPS.list authorizes it, the package.json-dependencies check also gets skipped for that file.

  5. Run npm run flint. It runs check_deps, tsc, eslint, and doc in parallel. A missing mapping typically surfaces as node_modules/ references leaking into coreBundle.js — the build fails hard via assertCoreBundleHasNoNodeModules().

In-tree Third-Party Helpers

Some vendored code isn't a published npm package but lives in-tree at packages/playwright-core/src/server/utils/third_party/ (e.g. extractZip.ts, lockfile.ts). These are TypeScript files, not node_modules. They're exposed to callers via two different routes:

  • Through coreBundle.utils. Re-exported from src/server/utils/index.ts via export * from './third_party/extractZip' etc. Callers import via the @utils/* path alias:
    import { extractZip } from '@utils/third_party/extractZip';
    The alias is rewritten at bundle time to require('playwright-core/lib/coreBundle').utils.extractZip.
  • Transitive npm deps via utilsBundle. When a third_party TS file imports an npm package (e.g., lockfile.ts imports graceful-fs, retry, signal-exit), those are still rewritten through utilsBundle — so the mapping in utilsBundleMapping.js must list them too.

DEPS.list

Every directory under packages/*/src/ has a DEPS.list constraining its imports. Three kinds of entries:

Syntax Meaning
./somefile.ts, @isomorphic/** Relative or alias source import allowed
node_modules/<pkg> npm package import allowed (exact specifier match)
"strict" No other DEPS inherited; only what's listed is allowed

Section headers [filename.ts] scope rules to a single file. The top-level [*] (or no header) applies to everything in the folder plus subfolders that don't have their own DEPS.list.

A DEPS.list entry of node_modules/<pkg> now shortcuts both layers of the check: the "disallowed external dependency" error AND the "dependencies not declared in package.json" report. The per-file allowlist is the contract — no need to also list the dep in packages/<pkg>/package.json if only one file uses it and it's authorized there.

check_deps.js

utils/check_deps.js walks the TypeScript program, visits every import in src/**, and for each npm specifier:

  1. Skips if the source file's DEPS.list authorizes node_modules/<specifier>.
  2. Otherwise records the top-level package name along with the file path that imported it.
  3. Subtracts peerDependencies, VENDORED_PACKAGES (from utilsBundleMapping.js), and any package that resolves without node_modules/ (a core module or a local file).
  4. Subtracts packages listed in packages/<pkg>/package.json dependencies.
  5. Anything left is reported with the specific file(s) that import it.

The missing-dep error now includes file paths:

Dependencies are not declared in package.json:
  expect
    src/matchers/expect.ts
  @babel/core
    src/transform/babelBundle.ts

Bundle-Level Externalization (onResolve plugins)

Two onResolve plugins in utils/build/build.js normalize relative imports to the sibling bundle at consumer output level:

  • externalizeUtilsBundlePlugin — matches any relative specifier ending in /utilsBundle or /utilsBundle.js (at any depth: ./utilsBundle, ../utilsBundle, ../../utilsBundle) and marks it external with the single spelling ./utilsBundle. This only applies to the coreBundle build because coreBundle inlines source files from all over playwright-core/src/ (different depths) and needs a single consistent external specifier that resolves correctly at runtime from lib/coreBundle.js.
  • The babelBundle case is handled differently — instead of a plugin, consumers' source/output depths are aligned:
    • esmLoader bundle output is placed at lib/transform/esmLoader.js (same folder as babelBundle.js), so ./babelBundle from transform.ts resolves correctly.
    • common and runner bundles declare '../transform/babelBundle' as a static external; their outputs are at lib/common/index.js and lib/runner/index.js, both at depth 1, so the source-relative specifier resolves naturally.
    • transform.ts's own require('./babelBundle') was replaced with require(libPath('transform', 'babelBundle')) — an absolute path computed at runtime via package.ts, which works from any bundle.

Quick Reference

  • To add a new vendored npm dep: root package.json → utilsBundle.ts export → utilsBundleMapping.js entry → DEPS.list → npm run flint.
  • To add a new in-tree third-party helper: drop the .ts file under server/utils/third_party/, re-export from server/utils/index.ts, and use @utils/third_party/<name> at call sites.
  • To add a new bundle entry: add an EsbuildStep in utils/build/build.js, pick output location so relative externals line up with runtime layout, and list externals for every sibling bundle the entry should not inline.
  • To expose a bundle file as a package subpath: add it to the exports field in packages/<pkg>/package.json.
  • To check what's inside a bundle: read the .js.txt sidecar next to the output.

Source: SKILL.md on GitHub

1 warning16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill provides a detailed set of development guides for the Playwright project, covering API implementation, tool creation, and architectural overview. It includes instructions for standard development tasks such as building, testing, and dependency management. While the skill describes the creation of tools that interact with untrusted agent input, it emphasizes the use of validation schemas to manage these boundaries.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    2/5 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 08a37f2. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 4 months ago
  • Testing
  • CLI
  • MCP
  • playwright
  • browser-automation
  • api-development
  • vendor-dependencies
  • webkit
  • monorepo

README badge

README badge for microsoft/playwright/playwright-dev

Explains how to extend Playwright itself — adding client/server APIs, MCP tools, CLI commands, and vendor dependencies. Targets contributors to the Playwright monorepo, not end users building test suites.

Generated from the current SKILL.md.

What does this skill help me do?
This skill explains how to develop Playwright itself — adding APIs, MCP tools, CLI commands, and managing vendor dependencies. It's for contributors working on the Playwright codebase, not for using Playwright as a testing library.
Does this cover the monorepo structure and build commands?
Yes. It references CLAUDE.md for monorepo structure, build/test/lint commands, and coding conventions.
Can I learn how to add new APIs or MCP tools to Playwright?
Yes. The skill includes detailed guides on adding and modifying APIs, implementing client/server logic, and adding MCP tools and CLI commands.
Does this cover WebView or WebKit backend development?
Yes. It includes guides for WebView (iOS Safari) backend work and updating WebKit Safari versions.
Is this for using Playwright as a test framework, or for developing Playwright itself?
This is for developing Playwright itself. If you want to write tests with Playwright, you need a different resource.

Generated from the current SKILL.md. These answers refresh after source changes.