@meridial/swc-plugin
v0.1.3
Published
SWC plugin that stamps React JSX elements with stable deterministic data-meridial-id attributes
Downloads
396
Maintainers
Readme
meridial-swc-plugin
Next js SWC plugin that stamps React JSX elements with stable, deterministic data-meridial-id attributes at build time for the Meridial in-app guide system.
How it works
The plugin traverses every JSX element in every source file and injects a data-meridial-id attribute whose value is the first 8 hex characters of a SHA-256 hash derived from:
sha256("{relativePath}:{componentName}:{astPath}:{elementType}")[0..8]The same source code always produces the same IDs, so guide recordings survive rebuilds.
What gets stamped ?
Only React component calls are stamped by default — JSX elements whose names start with an uppercase letter (e.g. <Button>, <Card>, <Dialog>). Native HTML elements (<div>, <span>, <button>, etc.) are skipped unless explicitly opted-in via allowedElements.
This keeps the DOM lean. IDs still reach native elements through React's prop-spread mechanism:
// button.tsx — plugin stamps the <Button> call site in page.tsx
function Button({ className, ...props }) {
return (
// Native <button> is skipped by default.
// data-meridial-id="b1" arrives via {...props} from the parent.
<button className={className} {...props} />
)
}
// page.tsx
export default function Page() {
return (
<div>
<Button data-meridial-id="e138a7ec" className="primary">Save</Button>
<Button data-meridial-id="765146cc" className="secondary">Cancel</Button>
</div>
)
}DOM output:
<button data-meridial-id="e138a7ec" class="primary">Save</button>
<button data-meridial-id="765146cc" class="secondary">Cancel</button>e138a7ec from page.tsx overrides whatever the Button definition would have stamped, because {...props} is spread after any static attributes on the root element. Both instances are uniquely identifiable.
Installation
npm install --save-dev @meridial/swc-pluginUsage
Next.js
// next.config.js
module.exports = {
experimental: {
swcPlugins: [
["@meridial/swc-plugin", {
rootPath: process.cwd()
}]
]
}
}Configuration
All options are optional.
{
"attribute": "data-meridial-id",
"hashLength": 8,
"rootPath": "/absolute/path/to/project",
"ignoredComponents": ["HeadlessComponent", "AnalyticsWrapper"],
"ignoredFiles": ["**/test/**", "**/*.test.tsx", "**/*.spec.tsx"],
"allowedElements": ["button", "input", "a"]
}| Option | Type | Default | Description |
| ------------------- | ---------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| attribute | string | "data-meridial-id" | Data attribute name written to the DOM |
| hashLength | number | 8 | Number of hex characters from the SHA-256 hash (max 64) |
| rootPath | string | undefined | Project root used to compute relative file paths for hashing. Omitting it uses the absolute path, which breaks reproducibility across machines. |
| ignoredComponents | string[] | [] | React component names whose return JSX is not stamped |
| ignoredFiles | string[] | [] | Glob patterns for files to skip entirely (tests, Storybook, etc.) |
| allowedElements | string[] | [] | Native HTML element names to stamp despite being in the default ignore list |
allowedElements — opting HTML elements back in
By default every standard HTML element is skipped to avoid bloating the DOM with attributes on every <div> and <span>. If you use native elements directly as guide targets (no component wrapper), add them here:
{
"allowedElements": ["button", "input", "a", "select", "textarea"]
}With this config, a bare <button> in a component return will receive a data-meridial-id just like a React component would.
The complete list of default-ignored HTML elements (those excluded from stamping by default) can be found in src/element_filter.rs. This list includes all standard HTML tags.
ID stability
Two things invalidate an existing ID:
- The file is moved or renamed (changes
relativePath) - The JSX tree structure changes so the element's
astPathshifts
IDs are not invalidated by:
- Renaming props or variables
- Adding/removing sibling non-JSX content (comments, expressions)
- Changing the element's children
Dynamic lists (.map)
JSX inside .map(), .flatMap(), .forEach(), or .reduce() callbacks is not stamped because a single source location would produce multiple DOM nodes with the same ID. The Meridial recorder handles this by walking up to the nearest stamped ancestor.
Component spread requirement
For the parent-overrides-child mechanism to work, components must spread rest props onto their root element — a standard React best practice already required for ref forwarding and aria/data attribute pass-through:
// Required pattern
function Card({ title, ...props }) {
return <div {...props}>{title}</div> // ✅ spread on root
}
// Does NOT participate in override chain
function Card({ title, onClick }) {
return <div onClick={onClick}>{title}</div> // ❌ no spread
}Components that don't spread props keep their definition-site ID. The recorder falls back to the nearest stamped ancestor.
