Skip to main content

Getting started

Install

hrserve drives a real Chromium through Playwright, so the browser needs to be present once:

npx playwright install chromium

Then run it directly — no install step needed:

npx hrserve ./public --url http://localhost:3000/

Or add it to a project:

npm install --save-dev hrserve

Command line

npx hrserve [dir] --url http://localhost:3000/
OptionDescription
--urlBase URL to serve at (default http://localhost:3000/)
--mock-dirDirectory of mock API routes, run in-process
--mock-pathPath glob handled by --mock-dir / --proxy (default /api/**)
--proxySend --mock-path requests without a mock route to this origin
--profileStart from a saved profile's cookies and storage
--save-profileOn Ctrl-C, save this session's cookies and storage under this name
--devtools, -dOpen devtools on start
--verbose, -vLog request routing and CDP events
--width, --heightBrowser window size
--script-reloadHow to apply changed JavaScript: auto (default), evaluate, import or off

Two extra commands:

npx hrserve profiles # list saved profiles
npx hrserve mcp # run the MCP server for agents

Programmatic use

hrserve does not launch the browser for you when used as a library — you own it, which means you decide headless vs headed, and you can reuse one browser for many servers.

import { chromium } from "playwright";
import { createServer } from "hrserve";

const browser = await chromium.launch({ headless: false });
const server = createServer(browser);

server.on("patch", ({ fileName, mimeType, applied, reason }) => {
console.log(`${fileName} (${mimeType})`, applied ? "patched" : `not applied: ${reason}`);
});

const page = await server.serve({
url: "http://localhost:3000/",
dir: "./public",
});

// later
await server.close(); // stops watching; the browser is yours to close
await browser.close();

serve() resolves with the Playwright Page after the first navigation, so you can drive it however you like.

What gets patched

TypeMechanismNotes
text/cssCSS.setStyleSheetTextValidated first — invalid CSS is reported, not applied
text/htmlDOM.setOuterHTMLReplaces the document; scroll and focus are not preserved
JavaScriptRe-run (classic scripts) or re-import (ES modules)Preceded by a script-patch event so you can clean up first
ImagesURL cache-busting in the live DOM/CSSOMPNG, JPEG, GIF, SVG, WebP

Reacting to JavaScript changes

Chromium removed live editing of JavaScript sources in Chrome 145, so a running script's body can no longer be swapped in place. hrserve re-runs the new source instead — indirect eval for classic scripts, import() of a cache-busted URL for ES modules — which means top-level side effects run again. The script-patch event fires before that happens, so your code can dispose of the old version:

window.addEventListener("script-patch", (event) => {
teardown(); // remove listeners, cancel timers, unmount
event.detail.accept((exports) => render(exports.App)); // optional: use the re-run's result
// event.preventDefault(); // optional: handle the update yourself
});

See JavaScript hot reload for the full contract, the scriptReload option and the limitations of re-running.

The patch event tells you what actually happened, which matters when a change looks applied but wasn't:

server.on("patch", ({ applied, reason }) => {
// applied: false, reason: "css-invalid: ..." | "stylesheet-not-loaded"
// | "hot-update-threw: ..." | "cancelled-by-page; ..."
});

Development

npm ci
npx playwright install chromium # needed once for browser tests
npm run build # tsc → dist/ (also the type check)
npm test # unit + browser tests
npm run lint # biome
npm run dev:ts # run the CLI from TypeScript sources