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

@moatless/inspector

v0.3.0

Published

Runtime inspector for AI-assisted UI development - console interception, element inspection, component tagging, element annotation, and bidirectional iframe communication

Readme

@moatless/inspector

Runtime instrumentation library for AI-assisted UI development. Injects a client script into preview iframes that enables bidirectional communication with the parent frame: element inspection, console forwarding, route tracking, and chat panel control.

How it works

The package has three sides:

  • Build-time (tagger) — Babel/AST transformer that annotates every JSX element with data-component-id and data-component-name attributes so the runtime can identify components from DOM clicks.
  • Runtime (client) — A browser IIFE (dist/client.js) injected into the preview iframe. Communicates with the parent via postMessage.
  • Annotation runtime — A second client (dist/annotation-client.js) for picking, drawing on and commenting about elements, also usable as ESM by a host that supplies its own transport. See Element annotation.

Server-side injection (primary use)

The preview-server proxy injects the client script into HTML responses before they reach the iframe. Use the two utilities exported from the package root:

import { getInspectorScript, injectScriptIntoHtml } from '@moatless/inspector'

// Read dist/client.js from disk and substitute the server ID placeholder
const script = getInspectorScript({ serverId: 'my-server-123' })

// Splice a <script> tag before </body> (falls back to </html> or end of string)
const instrumentedHtml = injectScriptIntoHtml(html, script)

serverId is stamped into the client and echoed back in the INSPECTOR_SCRIPT_LOADED message so the parent can identify which server instance the iframe belongs to.

Component tagging

Add data-component-id="<path>:<line>:<col>" and data-component-name attributes to every JSX element at build time. Required for the element inspector to map DOM clicks back to source locations.

Vite:

// vite.config.ts
import componentTagger from '@moatless/inspector/vite-tagger'

export default defineConfig({
  plugins: [componentTagger()], // devOnly: true by default
})

webpack / Next.js:

// next.config.js
module.exports = {
  webpack(config) {
    config.module.rules.push({
      test: /\.(jsx|tsx)$/,
      exclude: /node_modules/,
      use: [{ loader: '@moatless/inspector/webpack' }],
    })
    return config
  },
}

Standalone transformer (e.g. in a custom build pipeline):

import { transformComponentTags } from '@moatless/inspector/tagger'

const { code, map } = transformComponentTags(source, filePath, rootDir)

Vite dev-server injection

For injecting the client directly via the Vite dev server (instead of a proxy), use the HTML plugin:

import inspectorPlugin from '@moatless/inspector/vite'

export default defineConfig({
  plugins: [inspectorPlugin({ serverId: 'vite-dev' })],
})

Element annotation

A second client, dist/annotation-client.js, lets a host pick, draw on and comment about elements in the previewed page — richer than the element inspector, and it needs no tagger, because it resolves component sources by walking React fibers at runtime.

The Vite plugin injects it alongside the inspector client, so a project already calling inspectorPlugin() gets it with no config change. It is roughly 80 kB of inlined script; a project that only needs the Moatless UI element selector can leave it out with inspectorPlugin({ annotation: false }).

The two clients do not know about each other. Each talks to whatever framed the page over postMessage and ignores traffic it does not recognise — the inspector filters on source: 'preview-iframe', the annotation client on source: 't3code:preview-annotation' — so the one whose host is on the other side of the frame stays idle.

Hosting it yourself

The runtime is surface-agnostic: it needs a pipe, not a browser. postMessage is one; T3 Code's desktop app supplies an Electron ipcRenderer pipe instead and runs the same code in a <webview> preload.

import { attachPreviewAnnotationRuntime } from '@moatless/inspector/preview-annotation/runtime'
import type { AnnotationTransport } from '@moatless/inspector/preview-annotation/transport'

attachPreviewAnnotationRuntime({
  send: (channel, ...args) => ipcRenderer.send(channel, ...args),
  on: (channel, listener) => ipcRenderer.on(channel, wrap(listener)),
  off: (channel, listener) => ipcRenderer.off(channel, wrap(listener)),
} satisfies AnnotationTransport)

The host side of the conversation needs the channel names and the envelope helpers, and the shapes that come back:

import {
  START_PICK_CHANNEL,
  CANCEL_PICK_CHANNEL,
  ELEMENT_PICKED_CHANNEL,
  GUEST_READY_CHANNEL,
  PING_CHANNEL,
  previewAnnotationMessage,
  readPreviewAnnotationMessage,
} from '@moatless/inspector/preview-annotation/protocol'
import type { PreviewAnnotationPayload } from '@moatless/inspector/preview-annotation/types'

Both are re-exported from @moatless/inspector/preview-annotation for convenience. The types are plain interfaces with no validator attached — a host that needs one (T3 Code wraps them in Effect schemas at its IPC boundary) brings its own.

A guest announces itself on GUEST_READY_CHANNEL when its script runs, and answers PING_CHANNEL with the same, so a host that mounted late can still find it. Until a host hears one of those, it has no way to know whether the page it framed carries the runtime at all.

Editing the overlay styles

src/preview-annotation/styles.generated.ts is Tailwind output, not source. After changing src/preview-annotation/annotation.css or any class name in runtime.ts, regenerate it:

bun run build:css

In-iframe API: window.__moatless

The injected client exposes a global so apps running inside the preview iframe can communicate with the parent frame programmatically. The global is set during bootstrap (after DOM ready), so check for its existence before calling.

Detecting inspector presence

if (window.__moatless) {
  // Inspector script is loaded
}

window.__moatless.isAvailable()

Returns true if the page has any elements with data-component-id attributes — meaning components were tagged at build time and element inspection is functional.

if (window.__moatless?.isAvailable()) {
  // Safe to show "click to inspect" UI
}

window.__moatless.isSelectorActive()

Returns true if the element selector (click-to-inspect) mode is currently active in the parent frame.

if (window.__moatless?.isSelectorActive()) {
  // Selector is on — user can click elements to pick them
}

window.__moatless.toggleChat()

Collapses or expands the chat panel in the parent (kiosk mode). Useful for embedding a "show/hide assistant" button inside the preview app itself.

window.__moatless?.toggleChat()

The parent listens for the TOGGLE_CHAT postMessage and drives the resizable chat panel accordingly.

Combined usage example

const m = window.__moatless
if (m) {
  console.log('Inspector loaded')
  console.log('Elements tagged:', m.isAvailable())
  console.log('Selector active:', m.isSelectorActive())
}

Other features

All of the following run automatically once the client script is injected — no configuration required.

| Feature | Description | |---|---| | Console forwarding | console.log/warn/error/info/debug are intercepted and forwarded to the parent as console messages, shown in the Console tab. | | Error forwarding | Uncaught error and unhandledrejection events are forwarded to the parent. | | Element inspector | Clicking an element (when the inspector is active) walks the DOM to find the nearest data-component-id and sends ELEMENT_CLICKED with source location and element bounds. | | Route tracking | Patches history.pushState/replaceState and listens to popstate/hashchange to send ROUTE_CHANGE on every navigation. | | Heartbeat | Sends a HEARTBEAT postMessage every 5 seconds so the parent can detect if the script becomes unreachable. |

postMessage protocol

All messages carry source: 'preview-iframe'. The parent filters by this field.

Iframe → parent (outbound):

| type | Payload | When | |---|---|---| | INSPECTOR_SCRIPT_LOADED | { version, serverId } | On bootstrap | | HEARTBEAT | { timestamp } | Every 5 s | | INSPECTOR_AVAILABILITY | { isAvailable, elementCount } | Reply to CHECK_AVAILABILITY_REQUEST | | ELEMENT_CLICKED | { componentId, path, line, column, componentName, elementRect } | User clicks while inspector is active | | ROUTE_CHANGE | { path, timestamp } | Every client-side navigation | | console | { level, message, args, stack? } | Every console.* call | | TOGGLE_CHAT | — | window.__moatless.toggleChat() call |

Parent → iframe (inbound):

| type | Effect | |---|---| | TOGGLE_SELECTOR | Enable/disable hover+click inspection mode | | CHECK_AVAILABILITY_REQUEST | Count [data-component-id] elements, reply with INSPECTOR_AVAILABILITY | | ADD_TO_SELECTION | Highlight elements matching a componentId with a blue dashed overlay | | REMOVE_FROM_SELECTION | Remove highlight for a componentId | | CLEAR_SELECTION | Remove all selection overlays | | NAVIGATE | Navigate the iframe to a given path |