npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

playwright-browser-harness

v0.3.0

Published

Playwright harness that bundles your code, serves it under a COOP/COEP-toggleable server, drives it in a real browser, and returns structured results/timings — for testing real browser-storage persistence (IndexedDB/OPFS), cross-origin isolation, Workers,

Readme

playwright-browser-harness

Run bundled TypeScript inside a real browser under Playwright — drive it, assert results, measure performance — with the things browser storage needs: COOP/COEP cross-origin isolation (SharedArrayBuffer / OPFS), Web Workers, persistence across reloads, a reset between tests.

Hybrid distribution. The machinery (mountHarness/driver, the COOP/COEP server, the esbuild build, the page glue, the contract) ships in this package and is imported, never copied. A tiny init CLI scaffolds only the per-project files you edit anyway (a starter cut.ts, a spec, a playwright.config.ts).

Install

pnpm add -D playwright-browser-harness @playwright/test esbuild typescript @types/node
pnpm exec playwright install chromium

@playwright/test and esbuild are peerDependencies — the consumer drives their versions. @sqlite.org/sqlite-wasm is not a dependency; it is only a fixture/consumer dep when you test wasm-SQLite.

buffer is an optional peerDependency — only needed (and only installed) when you ask for the 'buffer' Node polyfill (see Explicit Node polyfills):

pnpm add -D buffer   # only if you use nodePolyfills: ['buffer', ...]

tsconfig (required)

{
  "compilerOptions": {
    "moduleResolution": "bundler",       // resolve 'playwright-browser-harness/contract'
    "allowImportingTsExtensions": true,  // the contract ships as raw .ts and is bundled
    "lib": ["ES2022", "DOM", "DOM.Iterable", "WebWorker"]
  }
}

Scaffold a starter (optional)

npx playwright-browser-harness init tests        # writes tests/cut.ts, tests/example.spec.ts, playwright.config.ts
pnpm exec playwright test
npx playwright-browser-harness update tests      # report drift of scaffolded files vs current templates

Named exports

The package's . entry exports the high-level driver plus the lower-level building blocks, all as documented named exports:

import {mountHarness, buildBundle, startServer} from 'playwright-browser-harness';
  • mountHarness(page, opts) — the usual entry point (bundle + serve + drive).
  • buildBundle(opts) — esbuild-bundle a cut (+ optional worker) to an outdir.
  • startServer({root, coi}) — the tiny COOP/COEP-toggleable static server.

Use buildBundle / startServer directly when you want to drive bundling and serving yourself (e.g. a custom Playwright global-setup) instead of prebuilt.

The in-page glue is importable too, so your own bundler can include it:

  • playwright-browser-harness/contract — raw .ts contract (captureEnv, timed, types).
  • playwright-browser-harness/page-entry — raw .ts page glue (for esbuild/bundler consumers).
  • playwright-browser-harness/page-bootstrap — plain .js page glue (loadable directly in the browser).

Use

// tests/cut.ts — your code-under-test, BUNDLED INTO THE BROWSER
import type {CodeUnderTest} from 'playwright-browser-harness/contract';
import {captureEnv, timed} from 'playwright-browser-harness/contract';
const cut: CodeUnderTest = { name: 'mine', async run(ctx){ /* ... */ return {results:{},timings:[],errors:[],env:captureEnv()}; } };
export default cut;
// tests/x.spec.ts — Node side
import {test, expect} from '@playwright/test';
import {mountHarness} from 'playwright-browser-harness';

test('persists', async ({page}) => {
  const h = await mountHarness(page, {cut: resolve(__dirname,'cut.ts'), coi: false});
  const w = await h.run({phase:'write', params:{n:100}});
  await h.reload();
  const r = await h.run({phase:'read', params:{n:100}});
  expect(r.results.survived).toBe(true);
  await h.dispose();
});

MountOptions: cut (esbuild path) or entry/noBundle (prebuilt JS, no esbuild) or prebuilt (prebuilt dir); coi (default true), worker, wasmDirs, assets, plus the bundling escape-hatches nodePolyfills and esbuild (see below).

Explicit Node polyfills

Many web3 / crypto libraries (ethereumjs, tevm, and anything pulling in readable-stream / safe-buffer) transitively import 'buffer' and touch process / global / events / stream. esbuild's platform:'browser' will not resolve those, so the bundle fails with Could not resolve "buffer".

There is no catch-all preset. A named "web3" preset is a leaky abstraction: it would shim buffer/process/global but not events/stream/util, so you'd fall back to the esbuild escape-hatch anyway. Instead you declare exactly what you need.

nodePolyfills?: ('buffer' | 'process' | 'global')[] (default [])

The three Node builtins the harness can shim itself, with zero opinions. Each entry is a self-contained fragment — ['buffer'] shims ONLY buffer; it does not touch process or global. [] or omitted = nothing.

| entry | what it does | |---|---| | 'buffer' | alias buffer / node:buffer → the buffer npm package, and inject a Buffer global (also assigned onto globalThis). Requires the optional buffer peer dep (pnpm add -D buffer). | | 'process' | inject a minimal process stub: {env:{}, browser:true, version:'', nextTick}. | | 'global' | set define: { global: 'globalThis' }. |

Anything beyond these three (events, stream, util, crypto, …) is the consumer's job via the esbuild pass-through below.

Copy-paste recipe: ethereumjs / tevm

A typical web3 EVM tree needs all three builtins plus events/stream aliased through the escape hatch:

const h = await mountHarness(page, {
  cut,
  coi: false,                              // pure-compute EVM → no SharedArrayBuffer needed
  nodePolyfills: ['buffer', 'process', 'global'], // the harness's three builtins
  esbuild: {
    // everything the harness intentionally does NOT preset:
    alias: {events: 'events', stream: 'stream-browserify'},
  },
});
pnpm add -D buffer events stream-browserify

Migrating from 0.2.0nodePolyfills shipped in 0.2.0 as true | 'web3'. That form was removed (clean break; 0.2.0 had ≈no users). Replace true / 'web3' with ['buffer', 'process', 'global'].

esbuild escape hatch — esbuild?: { plugins?, inject?, define?, alias?, loader?, external?, tsconfig? }

A pass-through merged into the harness's internal esbuild build, so a consumer whose code-under-test needs a plugin/alias/define/loader no longer has to fork the bundler (replicating the page-entry glue and mounting via prebuilt). Consumer values take precedence (objects shallow-merged with the consumer winning; arrays concatenated after the built-ins). The harness's own __CUT_MODULE__ resolve plugin, the page-entry entry point, and the .wasmcopy loader always remain:

| key | merge behaviour | |---|---| | plugins | concatenated after the built-in __CUT_MODULE__ resolver | | inject | concatenated after the nodePolyfills injects (the chosen builtins, if any) | | define / alias | shallow-merged, consumer key wins | | loader | merged over {'.wasm':'copy'} (e.g. add {'.svg':'dataurl'}) | | external | as given | | tsconfig | path to a tsconfig esbuild should honour |

Doing it entirely by hand (no nodePolyfills)

nodePolyfills is just sugar for these three fragments. If you'd rather not use it at all (e.g. you want one custom shim module), alias buffer to the npm package and inject a Buffer/process shim purely via the escape hatch:

const h = await mountHarness(page, {
  cut,
  coi: false,
  esbuild: {
    alias: {buffer: 'buffer'},          // the npm `buffer` package
    inject: [resolve(__dirname, 'node-shim.js')], // exports Buffer; sets globalThis.Buffer/process
    define: {global: 'globalThis'},
  },
});
// node-shim.js
import {Buffer} from 'buffer';
if (!globalThis.Buffer) globalThis.Buffer = Buffer;
if (!globalThis.process) globalThis.process = {env: {}, browser: true, version: '', nextTick: (cb, ...a) => Promise.resolve().then(() => cb(...a))};
export {Buffer};

Testing a prebuilt artifact (no esbuild)

The default path re-bundles your source with the harness's esbuild. To exercise the exact bytes your own build emitted (tsc / tsup / rollup → dist/), skip esbuild entirely:

entry / noBundle — wrap one prebuilt JS module

Point the harness at an already-built .js module that default-exports a CodeUnderTest. The harness serves it verbatim (with its sibling .map, if present) behind a tiny plain-JS loader — no esbuild, no re-bundle, so source maps point at your dist/, not a harness re-bundle:

import {mountHarness} from 'playwright-browser-harness';

// your own build already produced dist/cut.js (+ dist/cut.js.map)
const h = await mountHarness(page, {entry: resolve('dist/cut.js'), coi: false});
// equivalently: { cut: resolve('dist/cut.js'), noBundle: true }
const w = await h.run({phase: 'write', params: {n: 100}});
await h.reload();
const r = await h.run({phase: 'read', params: {n: 100}});

The served directory contains __cut.js (your module, verbatim), contract.js (the harness's compiled glue), bootstrap.js (the no-esbuild page glue), and index.html.

prebuilt — serve a fully-built directory

If your own bundler already produced a complete directory (its own index.html that boots the harness and sets window.__harness), serve it as-is. The glue is importable so your bundler can include it: playwright-browser-harness/page-entry (raw .ts, for bundler consumers) or playwright-browser-harness/page-bootstrap (plain .js, loadable directly).

// harness starts its own COOP/COEP server for the dir:
const h = await mountHarness(page, {prebuilt: {outdir: resolve('dist/site')}, coi: false});
// or reuse an already-running server (harness won't start/stop one):
const h2 = await mountHarness(page, {prebuilt: {outdir, serverUrl: 'http://127.0.0.1:5173'}});

COOP/COEP (coi) vs your wasm executor

coi toggles cross-origin isolation (COOP/COEP response headers → crossOriginIsolated === true, the precondition for SharedArrayBuffer). Match it to what your wasm needs:

| Workload | coi | Why | |---|---|---| | EVM / pure-compute wasm (ethereumjs, tevm, single-threaded wasm) | false | No SharedArrayBuffer; isolation is pure overhead and can complicate loading. | | Threaded wasm needing SharedArrayBuffer (wasm threads, OPFS sync VFS, sqlite-wasm opfs) | true | SharedArrayBuffer / the sync OPFS VFS require crossOriginIsolated. | | Plain IndexedDB / opfs-sahpool | false | Works without isolation; flip true only to test the isolated path. |

Default is coi:true; EVM / pure-compute executors want coi:false.

See the playwright-browser-test-harness research topic for the full architecture, gotchas, and per-browser OPFS matrix.