@excellent-so/screen-map
v0.2.2
Published
The map of your running app. One command screenshots every route, graphs how they connect, and finds the screens your nav forgot.
Maintainers
Readme
@excellent-so/screen-map
The map of your running app. Point it at your dev server and in one command you get a screenshot of every route, a graph of how they connect, and a place to leave notes that become tracked work. It finds the screens your nav forgot.
npx @excellent-so/screen-map build && npx @excellent-so/screen-map openZero config on a standard Next.js App Router app. Zero runtime dependencies. Fully
local — no cloud, no account. (Installs a screen-map command.)
Why
Your app is too big to hold in your head, any static map is stale the day it's
drawn, and a UX finding has nowhere durable to land. screen-map:
- Enumerates every route from your framework router — including orphan pages nothing links to, which a link-crawler structurally can't find.
- Screenshots each one by driving your real running app with headless Chrome.
- Graphs the navigation from your source (
<Link>/router.push/redirect) and validates it against a live crawl — surfacing orphans (no way in) and dead-ends (no way out). - Gives feedback a home — a local viewer with a screen board, a flow graph, durable multi-select notes, before/after diffing, and a note → agent → refresh review loop.
- Routes findings into tracked work — export a note to markdown, a GitHub issue, or (for Excellent users) a task on the ship/verify gate.
No existing tool combines those. Storybook renders hand-authored stories, not
routes. Chromatic/Percy diff snapshots in the cloud. SEO crawlers graph a deployed
URL and miss code-only routes. agent-browser screenshots a URL an agent names — no
enumeration, no graph, no durable notes. The fusion is the point.
Install & run
# 1. First touch — zero install
npx @excellent-so/screen-map build # enumerate → resolve → graph → capture → crawl ∪ walk
npx @excellent-so/screen-map open # serve the viewer and open it
# 2. For your team — pin it as a devDependency
npm i -D @excellent-so/screen-map
# then add to package.json "scripts": { "map": "screen-map build && screen-map open" }
# 3. Global — a one-liner from anywhere
npm i -g @excellent-so/screen-map # installs the `screen-map` commandRequires Node ≥ 20, a dev server (either one you keep running, or let Screen
Map run its own — see Self-hosted dev server),
and a system Chrome/Chromium (it finds Chrome, Chromium, Edge, or Brave
automatically; --download-chrome fetches one on demand — never a hidden postinstall,
never a bundled browser).
Commands
| Command | What it does |
|---|---|
| screen-map build | The full pipeline → .screenmap/screenmap.json |
| screen-map open | Serve the viewer and open it in your browser |
| screen-map serve | Serve the viewer (notes save to disk) |
| screen-map regraph | Fast, Chrome-free rebuild reusing the last shots |
| screen-map recapture --ids=a,b | Re-screenshot specific screens (the review-loop refresh) |
| screen-map route | Turn review notes into tracked work (see Findings) |
| screen-map diff | What changed since the last build |
| screen-map init | Scaffold a screen-map.config.mjs |
| screen-map doctor | Report config / Chrome / output-adapter status |
Useful build flags: --no-capture (fast, offline graph), --no-crawl
(router-walk only), --only=g1,g2 (restrict groups), --concurrency=N,
--limit=N, --download-chrome, --managed-server (run its own dev server — see
below).
Configuration
Zero-config works for a standard Next.js App Router app. To customize, drop a
screen-map.config.{mjs,js,json} in your project root (screen-map init scaffolds
one):
export default {
appDir: "app", // or "src/app"; auto-detected if omitted
devBaseUrl: "http://localhost:3000", // SCREENMAP_BASE overrides
framework: "next-app-router", // discovery adapter
discovery: ["router-walk", "crawl"], // the union (see below)
groups: (route) => route.split("/")[1] || "root", // section taxonomy
navBackbone: [], // optional global-nav targets ("/settings", …)
noChromeRoots: [], // route roots with no global chrome (auth, …)
coldStartPattern: null, // optional regex for empty-state hints
flows: [], // optional declared state-machine flows
stateSweep: true, // auto-discover hidden state screens (below)
statePreview: null, // escape hatch for gated states: {param,value}
output: { adapter: "markdown" }, // "markdown" | "github" | "excellent"
server: null, // let it run its OWN dev server (see below)
routeUrls: {}, // pin dynamic routes to concrete URLs (see below)
};Everything except appDir/devBaseUrl degrades gracefully to nothing when unset —
the generic path never depends on any of it.
Pinning dynamic routes (routeUrls). Screen Map resolves [id]/[slug] routes
by substituting seed values, then by scraping a real detail link off the nearest index
page. When neither reaches a route — an overloaded [id] param (one seed can't fit both
/runs/[id] and /leads/[id]), a detail route with no scrapeable index (/o/[uid]), or
a section-scoped route whose real index lives elsewhere — pin it to a concrete URL:
// screen-map.config.mjs → routeUrls, or a gitignored .screenmap/route-urls.json
routeUrls: {
"/leads/[id]": "/leads/42",
"/o/[uid]": "/o/01H…",
},Self-hosted dev server (--managed-server)
By default Screen Map captures against a dev server you keep running. For long
or unattended runs that coupling is fragile: if the shared server wedges partway
through, every remaining screenshot fails. Give it a server block and it will spin
up its own disposable dev server, capture against it, and tear it down fully:
export default {
server: {
command: "next dev", // your dev command (argv; string is whitespace-split)
readyPath: "/", // polled until the server answers
// port: 0, // 0 → auto-pick an ephemeral port (never collides)
// distDirEnv: "MYAPP_DIST", // env var your app reads to relocate its build dir,
// distDir: ".next-screen-map", // so two Next dev servers don't share one `.next`
// env: { MYAPP_DATA_DIR: "/tmp/scratch" }, // point the app at a throwaway DB
// prepare: "node scripts/seed-scratch-db.mjs", // one-time setup before boot
// autostart: true, // use the managed server on every run (no flag)
},
};screen-map build --managed-server # boot it, capture, tear it down
screen-map build --managed-server --server-port=4700 # pin the portThe managed server gets an ephemeral port (never collides with your own), an
optional isolated build dir, a health watchdog (a wedged server is killed and
respawned on the same port so the capture URL never changes), a retry pass for any
shots that failed during a hiccup, and full teardown (process-group kill wired to
stop()/SIGINT/SIGTERM/exit — nothing leaks even on Ctrl-C). Set autostart: true
to make it the default. This is what makes an overnight screen-map build reliable.
State-sweep: the screens a route walk can't see
The router walk gives one screenshot per route. But many routes hide N screens
behind a state axis on a single route — a query param the page reads
(/tour?mode=preview&step=3), or a dynamic [param] gated by app state
(/welcome/[step] → info…done). Older tools captured only the bare URL: one frame
of a multi-screen flow.
stateSweep (ON by default) discovers those states from the route's own source —
the string-union type, the VALID_STEPS/as const allow-list, or the array a numeric
param indexes — and fans the route into one screen per state. No per-route declaration:
point it at a project it's never seen and its wizards, tabs, and multi-step flows expand
automatically. It only fans a route when it infers a confident value domain, so a
free-text ?q= never explodes into noise.
stateSweep: { enabled: true, maxPerRoute: 24, maxTotal: 400 }, // or just `true` / `false`Two things a hand-authored flows entry conflated, separated here: what the states
are (discovered — no authoring) and how to force a gated one. A route that redirects
unless it's in a valid state needs one generic escape hatch — statePreview appends
?<param>=<value> to gated states so they render on a live server:
statePreview: { param: "preview", value: "1" }, // your app's dev-preview conventionQuery-param routes that honor their own params (the common case) need nothing at all.
Declared flows still win where set — the sweep skips any route a flow already owns.
Discovery: router-walk ∪ crawl
Discovery is the union of two adapters:
- Router-walk enumerates every route from the filesystem router, including orphans. This is the moat — the reason to use this over an SEO crawler.
- Running-server crawl renders the real app (dynamic
[slug]pages render for real), works beyond Next, and its link edges validate the static graph.
Their difference is the payoff: walk ∖ crawl-reached is the proven orphan
report — a route that exists in code but nothing links to, demonstrated, not just
inferred.
Findings
A review note you leave in the viewer becomes a portable finding — the exact screens, live URLs, screenshots, and graph signals (orphan / dead-end). Route it:
screen-map route --adapter=markdown # append to .screenmap/findings.md + stdout (default)
screen-map route --adapter=github # open a GitHub issue (needs GITHUB_TOKEN)
screen-map route --adapter=excellent # a task on Excellent's ship/verify gate (needs excellent-mcp)screen-map route --dry-run prints the findings without sending. Screen Map never
holds a Slack/Linear token — the excellent adapter lands one finding and
Excellent fans it out through its own integrations hub.
Editor extension
Screen Map ships a VS Code / Open VSX extension — a thin host that runs the CLI and renders the viewer inside your editor. Works in Cursor, Windsurf, and VSCodium too.
Output
Everything lands in .screenmap/ (gitignore it) — the JSON artifact, the shots, your
notes, and the self-contained viewer.
License
MIT © Excellent
