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:
- It is derived from path and name only — never file contents or line numbers — so editing a file does not invalidate comments captured months ago. It changes only when the file moves or the component is renamed, which is exactly when the old identity stopped being true.
- It is relative to the git root and POSIX-separated, so the same repository produces the same hashes on every machine and CI runner.
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.
.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:
-
A fresh clone. The manifest is generated and gitignored, so a clone
starts without one — and
applyanddoctordeliberately do not run a build to fix that (a build is slow and has side effects nobody asked for). - A rename that staled every hash. After renaming components, the hashes stamped into existing comments no longer resolve against a manifest built from the new names.
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.