@magnuslundstrom/dom-diff
v0.1.0
Published
A Bun + Playwright CLI for subtree-level DOM structure comparison.
Maintainers
Readme
dom-diff
A Bun + Playwright CLI for subtree-level DOM structure comparison.
dom-diff is meant for refactors where selector-sensitive DOM shape must stay stable, especially data-layer or rendering changes that should not break CSS selectors, overrides, or DOM-dependent assumptions.
It is not a visual diff tool. It compares normalized DOM structure under a chosen root selector.
What It Checks
- root selector resolves to exactly one node in both pages
- tag names
- nesting and child order
- normalized text nodes
- attributes, with explicit ignore rules
- class membership, ignoring class-token order
Exit Codes
0: no structural differences1: structural differences found2: runtime or configuration error
Requirements
- Bun
- Playwright Chromium
Install
Install in a consumer project:
bun add -d @magnuslundstrom/dom-diff
bunx playwright install chromiumThen run it with:
bunx dom-diff compareFor local development in this repo:
bun install
npx playwright install chromiumCLI Usage
Ad hoc comparison:
bunx dom-diff compare \
--baseline http://localhost:3000/products/42 \
--candidate http://localhost:3001/products/42 \
--root 'main .product-grid'Useful flags:
--wait-for <selector>--settle-ms <ms>--viewport 1280x800--tag <label>--ignore-attr <name>--ignore-attr-pattern <regex>--ignore-text-selector <selector>--ignore-text-pattern <regex>
The CLI always writes a temp JSON artifact and prints its path.
Typed Config
If dom-diff.config.ts exists, dom-diff compare will load it automatically and run all configured scenarios.
Example:
import { defineConfig } from "@magnuslundstrom/dom-diff/config";
export default defineConfig({
defaults: {
viewport: { width: 1440, height: 900 },
wait: { settleMs: 150 },
ignoreAttributes: ["nonce"],
ignoreAttributePatterns: ["^data-v-"],
ignoreTextSelectors: [".match-clock"],
ignoreTextPatterns: ["^\\d{1,2}:\\d{2}$"]
},
scenarios: [
{
tag: "catalog",
baselineUrl: "http://localhost:3000/products",
candidateUrl: "http://localhost:3001/products",
rootSelector: "main.catalog"
},
{
tag: "checkout",
baselineUrl: "http://localhost:3000/checkout",
candidateUrl: "http://localhost:3001/checkout",
rootSelector: "main.checkout",
wait: {
waitForSelector: "[data-ready='true']"
}
}
]
});Run all scenarios:
bunx dom-diff compareRun one scenario:
bunx dom-diff compare --scenario checkoutIgnoring Dynamic Text
For values like match clocks, countdowns, or elapsed timers, ignore text surgically instead of ignoring all text.
Ignore text inside a selector:
bunx dom-diff compare \
--baseline http://localhost:3000/matches \
--candidate http://localhost:3001/matches \
--root 'main.matches' \
--ignore-text-selector '.match-clock'Ignore text by pattern:
bunx dom-diff compare \
--baseline http://localhost:3000/matches \
--candidate http://localhost:3001/matches \
--root 'main.matches' \
--ignore-text-pattern '^\\d{1,2}:\\d{2}$'These replace matching text values with a stable placeholder while preserving text-node structure.
Development
Typecheck:
bun run checkRun tests:
bun testRun the CLI directly:
bun run ./src/cli.ts compare --baseline http://localhost:3000 --candidate http://localhost:3001 --root mainBuild the publishable package:
bun run buildInspect the package payload:
bun run pack:checkAgent Skill
This package ships a skill at skills/dom-diff-cli/SKILL.md so other agents can learn when and how to use the CLI.
The package is also structured for TanStack Intent-style skill shipping:
skills/is included in published filespackage.jsoncontainsintentmetadata- the skill validates with
bunx intent validate skills
