Tools
Dev tool
@isbatak/sourcery
Hold a hotkey, point at anything on the page and click to open the line of JSX that rendered it in your editor.
pnpm add -D @isbatak/sourceryDemo
Sourcery adds the source location to every JSX element while you develop. Hold ⌘ ⇧, hover the page to see which file and line rendered each element, and click to jump there in your editor.
styled() call behind an elementnext build, vite build or your production bundleNext.js 15+ and Vite 6+, on Turbopack or webpack.
The Next.js adapter covers both bundlers: next dev with Turbopack, which is the default, and next dev --webpack. It
only changes the config while the development server runs, so next build gets your config back untouched. Outside
Next.js you can add the webpack rule or the Turbopack rules yourself.
The Vite plugin runs for vite dev only. It works with plain Vite apps and with frameworks built on Vite, like TanStack
Start and React Router.
Copy this prompt into Claude Code, Cursor or any other coding agent. It installs sourcery, works out how your project is built and asks you a few questions before it changes anything.
Set up @isbatak/sourcery in this project. In development it adds the source location to every JSX element, so holding ⌘⇧ (Ctrl⇧ on Windows and Linux) and clicking an element opens the line that rendered it in the editor. Docs: https://github.com/isBatak/isbatak-labs/tree/main/packages/tools/sourcery
1. Look at the project first, without changing anything:
- The package manager, from the lockfile.
- The bundler. Sourcery supports Next.js 15 or newer (Turbopack and webpack), Vite 6 or newer (including frameworks built on it, like TanStack Start and React Router), hand-written Turbopack rules, and plain webpack. If the project uses anything else, tell me it isn't supported yet and stop.
- For Next.js: the config file, any plugins already wrapping the config, and whether it uses the App Router or the Pages Router.
- Whether it uses Panda CSS: a panda.config file, or imports from styled-system/jsx or another Panda jsx entry.
- For Vite: the config file, and whether the app is served from an index.html or a framework renders the HTML (TanStack Start, React Router).
- A module that runs in the browser on every page, for the injectTo option: a "use client" providers component rendered by the root layout, pages/_app.tsx, src/routes/__root.tsx in TanStack Start, app/root.tsx in React Router, or the client entry such as src/main.tsx. A Vite app served from index.html doesn't need one.
2. Tell me what you found, then ask me these in one message:
- Which module injectTo should point at, suggesting the one you found. Skip this for a Vite app served from index.html. With the App Router and no such component, offer to create components/providers.tsx with "use client" that returns its children, and render it in app/layout.tsx.
- Only if the project uses Panda CSS: whether to turn on panda mode, where holding ⌥ as well opens the styled() call behind an element instead of the line that uses it.
- Which editor to open: detect it automatically (recommended), or always use one of code, cursor, windsurf, zed, sublime, webstorm, idea.
3. Once I answer, set it up:
- Install @isbatak/sourcery as a dev dependency with the project's package manager.
- Next.js: import withSourcery from "@isbatak/sourcery/next" and resolve from "node:path", then export withSourcery(nextConfig, { injectTo: resolve("components/providers.tsx") }). It returns a config function, so keep it the outermost wrapper, around any other plugins.
- Vite: import { sourcery } from "@isbatak/sourcery/vite" and add sourcery() to plugins. With TanStack Start, React Router or another framework that renders the HTML, pass { injectTo: resolve("src/routes/__root.tsx") } (or the module you agreed on) with resolve from "node:path".
- Hand-written Turbopack rules: const options = await prepareOptions({ injectTo }) from "@isbatak/sourcery", then rules: mergeTurbopackRules(existingRules, createTurbopackRules(options)) from "@isbatak/sourcery/turbopack".
- Plain webpack: export (env, argv) => withSourceryWebpack(config, { enabled: argv.mode === "development", injectTo }) from "@isbatak/sourcery/webpack".
- Add panda: true or editor: "<id>" to the options only if I asked for them.
- Don't touch anything else. Sourcery is development only and leaves production builds alone.
4. Check it works: start the dev server and look for "sourcery hold ⌘⇧ and click an element to open its source" in its output, then stop it. If you can't run it, ask me to. Finish by telling me how to use it: hold ⌘⇧ (Ctrl⇧ outside macOS), hover the page, click an element.Pick your setup. In each case, injectTo points at a module that runs in the browser on every page, where sourcery adds
its small client.
Wrap your config with withSourcery and point injectTo at a client component every page renders, like the providers
of your root layout.
import { resolve } from "node:path"
import type { NextConfig } from "next"
import { withSourcery } from "@isbatak/sourcery/next"
const nextConfig: NextConfig = {
reactStrictMode: true,
}
export default withSourcery(nextConfig, {
injectTo: resolve("components/providers.tsx"),
})"use client"
import type { ReactNode } from "react"
export function Providers({ children }: { children: ReactNode }) {
return children
}import type { ReactNode } from "react"
import { Providers } from "../components/providers"
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}With the Pages Router, point injectTo at pages/_app.tsx instead.
Add the plugin. When the app is served from an index.html, sourcery adds its client there, so injectTo isn't needed.
import react from "@vitejs/plugin-react"
import { sourcery } from "@isbatak/sourcery/vite"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [react(), sourcery()],
})Frameworks that render their own HTML, like TanStack Start and React Router, need injectTo pointing at the root route.
Sourcery reads each file before other plugins change it, so its place in plugins doesn't matter.
import { resolve } from "node:path"
import { sourcery } from "@isbatak/sourcery/vite"
import { tanstackStart } from "@tanstack/react-start/plugin/vite"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [tanstackStart(), react(), sourcery({ injectTo: resolve("src/routes/__root.tsx") })],
})With React Router, point injectTo at app/root.tsx. Paths are recorded relative to Vite's root.
If you build your Turbopack rules yourself, create sourcery's rules and merge them in. mergeTurbopackRules puts them
first, so sourcery reads your original source before other loaders change it.
import { resolve } from "node:path"
import { prepareOptions } from "@isbatak/sourcery"
import { createTurbopackRules, mergeTurbopackRules } from "@isbatak/sourcery/turbopack"
const options = await prepareOptions({ injectTo: resolve("components/providers.tsx") })
export default {
turbopack: {
rules: mergeTurbopackRules(yourRules, createTurbopackRules(options)),
},
}prepareOptions starts the small server that opens your editor and returns the options the loader needs. The rules only
run in development and skip node_modules.
For a plain webpack setup, wrap the config with withSourceryWebpack. It adds one enforce: "pre" rule for your
.jsx, .tsx, .js, .mjs, .ts and .mts files.
import { resolve } from "node:path"
import { withSourceryWebpack } from "@isbatak/sourcery/webpack"
export default (env, argv) =>
withSourceryWebpack(config, {
enabled: argv.mode === "development",
injectTo: resolve("src/main.tsx"),
})Webpack accepts a config that returns a promise, which is what withSourceryWebpack gives back.
Start your development server as usual. It prints sourcery hold ⌘⇧ and click an element to open its source once the
server is ready.
Components get a location too, where they are used. A component that spreads its props onto the element it renders, like
most design systems do, passes the location on, so a <Button> points at the line that renders it.
Sourcery finds your editor on its own. Hover an editor to see how it is opened.
It picks the editor in this order:
editor option or the SOURCERY_EDITOR environment variable.VISUAL or EDITOR environment variable.On macOS, editors with a are opened through it. Any other editor works too when you name its command:
SOURCERY_EDITOR=nvim-qt pnpm devPass panda: true to also record where each Panda component is defined. Hold ⌥ as well and the outline turns
pink: a click then opens the styled() call behind the element instead of the line that uses it.
export default withSourcery(nextConfig, {
injectTo: resolve("components/providers.tsx"),
panda: true,
})Sourcery adds the location to the defaultProps of styled() and of the withProvider, withContext and
withRootProvider functions from createSlotRecipeContext. It only touches imports from a Panda jsx entry such as
styled-system/jsx. List your own with panda: { modules: ["@acme/design-system/jsx"] }.
| Prop | Type | Default |
|---|---|---|
enabled | boolean Whether sourcery runs. The Next.js adapter turns it on for next dev, the others when NODE_ENV is development. | in development |
injectTo | string Absolute path of the module the browser client is injected into, usually a root client component. | — |
exclude | string[] Path fragments to skip. node_modules is always skipped. | — |
hotKeys | HotKey[] Keys held together to start inspecting. | ["metaKey", "shiftKey"] on macOS, ["ctrlKey", "shiftKey"] elsewhere |
editor | string Editor to open: a known id ( code, cursor, zed, webstorm, …), a command or a path. Detected when unset. | — |
port | number First port tried for the local open-in-editor server. | 5678 |
root | string Directory source paths are recorded relative to. | process.cwd() |
attribute | string Attribute written on every element. | "data-sourcery" |
ignoreTags | string[] Extra element or component names to leave untagged, on top of Fragment, Suspense, script, style, template and slot. | — |
panda | boolean | { modules?: string[] | undefined } Also record where Panda CSS styled(), withProvider(), withContext() and withRootProvider() components are
defined. Hold ⌥ with the hotkeys to open that definition. modules lists the import paths treated as Panda's jsx
entry, matched as suffixes. | false |
Check the terminal of your development server. When sourcery cannot tell which editor to use, it prints a hint there.
Set the editor explicitly, for example SOURCERY_EDITOR=cursor, or pass editor: "cursor".
Elements rendered from an HTML string, like highlighted code, can't carry an attribute, and neither can content compiled outside your bundler, such as MDX built ahead of time. Hovering them outlines the nearest parent that has a location. Components that don't pass their props on to what they render only get a location for the elements inside them.
Sourcery takes the editor from the terminal the server was started in. Start it from the editor you want, or set
SOURCERY_EDITOR.
Nothing to do: the server moves on to the next free port and the browser client is told which one it got. Use the port
option to start somewhere else.
No. The Next.js adapter only changes the config for next dev, the Vite plugin only runs for vite dev, and the
Turbopack rules only run in development. The webpack helper is on when NODE_ENV is development; pass enabled to
decide yourself.