Workflow

From a comment to the file that rendered it

When someone clicks an element and leaves a comment, the most useful thing Pointer can hand the developer is which file rendered that element. In a production build, that answer has to be stamped in at build time.

Why a build plugin is needed

In development, React and Vue attach source metadata to the live component tree as a side effect of dev-mode compilation, and the widget reads it with zero setup: a click on a button comes back as src/components/Button.tsx. A production build strips that metadata — minified, dev-mode-free markup carries no file names at all. Without help, a production click resolves to no file, and the developer is left grepping the codebase by element classes.

The Vite plugin puts a plain static attribute into the markup itself, which survives minification because it is just markup. The trade it makes is deliberate: the page exposes only an opaque 8-hex-character hash — never your file paths — and the hash-to-file mapping lives on your machine, in a local manifest.

How the stamping works

Add the plugin to your Vite config (it is opt-in):

import pointerSource from 'pointer-feedback/vite';

export default {
  plugins: [pointerSource({ enabled: true })],
};

It works with React (.jsx/.tsx) and Vue (.vue) files. The Babel toolchain it needs (@babel/parser, @babel/traverse, @babel/generator) ships as transitive dependencies of @vitejs/plugin-react, so a normal React+Vite project installs nothing extra. A file that cannot be parsed is skipped with a warning — stamping never breaks the build.

At build time, the plugin stamps the root host elements of every component — a fragment contributes all its top-level roots, and a component that only renders another component contributes nothing, so the stamp lands on the component that actually owns the markup:

<div class="card" data-component-source="a3f9c2b1">…</div>

The hash is sha1(repo-relative path + "#" + export name), truncated to 8 hex characters. Two properties matter:

Alongside the stamps, the build writes a local manifest — .pointer/manifest.json — mapping every hash to its file and component name, and stamps <html data-build-sha="…"> from the current commit, which the deployments page puts to work.

The manifest stays on your machine

.pointer/manifest.json is generated and gitignored — it is never committed and never uploaded. A production page exposes only the opaque hashes; the file paths exist in exactly one place: the manifest on the developer's machine that resolves them.

From a hash back to a file

When a comment is captured, it records the stamped hash of the clicked element. The CLI resolves it against the local manifest:

pointer get 42 --json

The JSON view carries the comment plus a resolvedSource object, and pointer apply injects the same resolution into the prompt as Source: src/components/Card.tsx (Card) — the AI opens the right file instead of searching.

The three resolution outcomes

Outcome Meaning What you get
manifest The hash is in the current manifest The file path and component name
stale The hash is gone from the current manifest but present in the previous one — the component was renamed or moved since the comment was captured The previous name, as a search hint: search for "Card"
unknown No manifest, or neither file knows the hash No file — the apply falls back to the element snapshot and classes

The previous manifest is the key to the middle row: on every build, the old manifest.json is rotated to manifest.prev.json (never merged), so the map that was current when the comment was captured survives one rename behind the new one.

A stale hash does not block the apply

Renaming a component is a normal thing to do in a live codebase, and feedback captured before the rename must not be stranded by it. When a hash resolves as stale, the apply prompt says so explicitly and keeps going:

Source: UNRESOLVED — source hash a3f9c2b1 is not in the current manifest
        (renamed or moved since this comment was captured).
  Fallback: search for "Card" in the codebase, then edit the element the comment describes.

The previous name from manifest.prev.json is the one fact that turns a dead end into a single grep.

Rebuilding the manifest: pointer map --from-source

The manifest is a by-product of running a build, which means it only exists where someone has run one. Two situations leave a developer without a manifest and needing it now:

Both are fixed the same way, offline and without a build:

pointer map --from-source

It walks your .jsx/.tsx/.vue files (skipping node_modules, build output and the like), runs the same stamping analysis over the source, and writes the same manifest the plugin would — rotating any existing one to manifest.prev.json first, so the rename case keeps the old names reachable. Nothing is stamped on disk: the output is the map, not the markup. A file that fails to parse is skipped and named, never fatal to the run.