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

domdomdom

v0.1.1

Published

DOM-aware JS evaluator. Pipe code via stdin to run against any HTML page (URL, local file, or inline). Lightweight happy-dom alternative to Playwright for non-layout, non-interactive tasks.

Readme

domdomdom

Evaluate JavaScript against an HTML page from the command line. Pipe in code, get back the truth, cue dramatic chipmunk!

echo "return document.querySelectorAll('h1').length" | domdomdom https://example.com

Powered by happy-dom. No browser binary, no Playwright install, no MCP server. Two TypeScript files, ~12KB on npm.

Why this exists

happy-dom almost works on Bun out of the box. Then you hit four walls. domdomdom fixes them so you don't have to:

  1. Built-ins are missing. happy-dom's BrowserWindow on Bun starts with Object, Math, JSON, parseInt, SyntaxError etc. set to undefined. Its VMGlobalPropertyScript tries to copy them from globalThis, but inside Script.runInContext globalThis refers to the (empty) inner scope. Every querySelector throws TypeError because SyntaxError isn't on window. domdomdom enumerates Object.getOwnPropertyNames(globalThis) from the host realm and assigns each to the page window.
  2. file:// doesn't fetch. page.goto('file:///abs/path.html') rejects. domdomdom reads HTML manually and uses page.content = plus page.url = to set up the page.
  3. IIFE bundles silently break. happy-dom's HTML parser wraps every <script> body in function anonymous($happy_dom) { ... }. Top-level var foo = (() => { ... })() becomes a function-local — never reaches window. domdomdom extracts <script src> tags before page.content and runs them via page.evaluate() (uses Script.runInContext directly, preserves real script-top-level scope).
  4. ES modules can't import. <script type="module" src="./foo.js"> can't be fetched from disk. domdomdom maps a synthetic http:// origin to the page directory via happy-dom's virtualServers so relative imports work.

Each of these is a one-line fix once you've found it. Finding them took an afternoon.

When to use this vs. alternatives

| You want | Use this | | ------------------------------------------- | ------------------ | | Run a snippet against a real page, fast | domdomdom | | Test code that uses document, window | domdomdom | | Verify an IIFE bundle attaches to window | domdomdom | | Layout, computed styles, screenshots | Playwright | | Run untrusted JS safely | Playwright (sandbox) or a worker pool | | Parse HTML without executing scripts | linkedom (faster) | | Module bundling / build tooling | bun build / esbuild |

Install

# global install
bun add -g domdomdom
npm install -g domdomdom

# one-off, no install
bunx domdomdom ./page.html
npx domdomdom https://example.com

# clone for development
git clone https://github.com/scruffymongrel/domdomdom && cd domdomdom && bun link

Runtime requirements

  • Bun ≥ 1.3 — works out of the box.
  • Node ≥ 23.6 (LTS: 24+) — uses Node's built-in TypeScript stripping. The shebang silences the experimental-feature warning automatically. Node 22 LTS users need node --experimental-strip-types set in NODE_OPTIONS, or just install via Bun.

No build step. The published package ships .ts source directly; both runtimes execute it natively.

CLI

domdomdom [options] [URL_OR_PATH]

| Source | Interpretation | | ---------------- | ------------------------------------------------- | | http(s)://... | fetched via happy-dom | | ./path.html | read from disk; relative scripts/modules resolved | | --html '<...>' | inline HTML | | (none) | about:blank |

| Code source | Interpretation | | --------------- | ---------------------------------------------------- | | stdin | default; auto-return if a single expression | | --script <f> | read user code from a file (no auto-return) |

Flags

| Flag | Effect | | ---------------- | ----------------------------------------------------------- | | --inject <f> | preload a JS file in the window before user code; repeatable | | --module | evaluate user code as ES module (allows top-level import) | | --user-agent | override navigator.userAgent | | --viewport WxH | override page viewport (e.g. 1024x768) | | --timeout <ms> | time limit; 0 disables; default 5000 | | --no-console | drop console.* output instead of capturing it | | --json | emit one JSON line: { ok, result?, error?, logs } | | -h, --help | show help |

Output contract

Default (human): result on stdout, console.* on stderr ([log] / [warn] / etc), errors on stderr (EVAL ERROR: ...).

--json: single line on stdout, nothing else. Captured logs included.

Exit codes: 0 ok · 1 eval error · 2 timeout · 3 setup/usage error.

Examples

# one-liner expression against about:blank
echo "1 + 2" | domdomdom

# query a real page
echo "return [...document.querySelectorAll('a')].map(a => a.href).slice(0, 5)" \
  | domdomdom https://news.ycombinator.com

# verify an IIFE bundle exposes its export on window
echo "return typeof window.MyLib" | domdomdom ./dist/test.html

# preload a stub before running test code
echo "return fetch('/api/x').then(r => r.json())" | \
  domdomdom --inject ./test/stubs.js

# structured output for an agent
echo "return document.title" | domdomdom --json https://example.com
# {"ok":true,"result":"Example Domain","logs":[]}

Library

Same engine, programmatic:

import { evaluate } from 'domdomdom'

const r = await evaluate('return document.title', {
  html: '<title>hi</title>',
  timeout: 1000,
})
if (r.ok) console.log(r.result)

evaluate(code, opts?)

interface EvaluateOptions {
  source?: string         // URL or local file path
  html?: string           // inline HTML (mutually exclusive with source)
  baseDir?: string        // resolve <script src> and inject paths against this
  timeout?: number        // ms; 0 disables; default 5000
  module?: boolean        // treat user code as ES module
  inject?: string[]       // preload JS files in window before user code
  userAgent?: string      // navigator.userAgent override
  viewport?: { width: number; height: number }
  quietConsole?: boolean  // drop console.* instead of capturing
}

type EvaluateResult =
  | { ok: true;  result: unknown; logs: ConsoleEntry[] }
  | { ok: false; error: EvaluateError; logs: ConsoleEntry[] }

type EvaluateError =
  | { kind: 'eval';    message: string; stack?: string }
  | { kind: 'timeout'; message: string }
  | { kind: 'setup';   message: string; stack?: string }

toCloneable(value)

JSON-stringify-safe transform. Cycles → "[Circular]". Functions, BigInt, Symbol, undefined → tagged strings. DOM nodes → plain objects. Use this if you want a result you can post over a wire.

Agent integration

domdomdom was built for LLM agents to drive — --json plus stdin/stdout-only contracts mean it works behind a plain Bash tool without an MCP server, persistent browser, or context overhead. The repo ships an Agent Skill at skills/domdomdom/SKILL.md that teaches the agent when to reach for the tool and how to read its output.

Claude Code

domdomdom is a Claude Code plugin (.claude-plugin/plugin.json in this repo) listed in the scruffymongrel marketplace. From inside Claude Code:

/plugin marketplace add scruffymongrel/claude-plugins
/plugin install domdomdom@scruffymongrel

Restart Claude Code. The skill auto-loads when the user's prompt matches its trigger ("evaluate JS against this page", "test if the bundle exposes X on window", "extract X from this HTML", etc.). Users can also invoke explicitly with /domdomdom.

Other agents (Cursor, Aider, Codex CLI, Copilot, etc.)

The skill follows the Agent Skills open standard — an emerging cross-agent format that's just SKILL.md with YAML frontmatter. After installing domdomdom (npm i -g domdomdom), the skill ships at $(npm root -g)/domdomdom/skills/domdomdom/. Copy it into your agent's skill directory:

cp -r "$(npm root -g)/domdomdom/skills/domdomdom" <your-agent>/skills/
# or, from a clone:
cp -r ./skills/domdomdom <your-agent>/skills/

For agents without skill support, paste this into your system prompt (covers ~90% of usage):

To execute JS against an HTML page, pipe code via stdin to domdomdom --json --timeout <ms> followed by the URL/path or --html '<...>'. Single-line expressions auto-return; multi-line code requires return explicitly. Parse stdout as JSON; check .ok first. Captured console.* output is in logs[].

Output contract for agents

Stdout is one JSON line. Branch on .ok:

// success
{ "ok": true, "result": <any>, "logs": [{ "level": "log"|"warn"|"error"|"info"|"debug", "message": "..." }] }

// failure
{ "ok": false, "error": { "kind": "eval"|"timeout"|"setup", "message": "...", "stack": "..." }, "logs": [...] }

Exit codes (0 ok / 1 eval / 2 timeout / 3 setup) give a cheap pre-check before parsing.

When to reach for it

Verifying a built bundle exposes its export on window · extracting structured data from a fetched HTML page · running a DOM snippet without spinning up Playwright · smoke-testing <script> evaluation in CI.

When not to

Layout, screenshots, click/scroll interaction, or untrusted-code isolation. Use Playwright.

Limits

  • No layout. getComputedStyle().getPropertyValue('height') returns '' for unstyled elements. happy-dom doesn't render. For layout-dependent assertions, use Playwright.
  • Synchronous infinite loops. timeout catches async hangs (long fetches, unresolved promises, slow setIntervals). It can't kill a while(true){} because the host event loop is shared with the page's V8 isolate. Wrap the CLI in timeout 5s domdomdom ... for a hard ceiling.
  • Bare module specifiers. import 'lodash' from inside a <script type="module"> won't resolve — happy-dom needs a resolveNodeModules config, which we don't currently surface. Relative imports (import './foo.js') work.
  • Source maps. Stack traces refer to evaluated-script offsets, not your original .ts files.
  • outerHTML round-trips drop reactive inline styles. If a custom element sets inline styles in attributeChangedCallback (e.g. this.style.display = 'grid'), assigning outerHTML can clobber pre-existing inline styles in the markup. Real browsers preserve them. Don't trust el.style.getPropertyValue(...) after a happy-dom outerHTML round-trip if the SUT has reactive style assignments.

Development

bun install
bun test            # 60 tests, 100% line + function coverage on engine and CLI
bun run typecheck   # tsc --noEmit
bun run quality     # both

License

MIT.