npm package: @meridial/swc-plugin
Description: SWC plugin that stamps React JSX elements with stable deterministic data-meridial-id attributes
Author: Meridial
Latest version: 0.1.3

# 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](https://meridial.dev) 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:

```tsx
// 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:

```html
<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

```bash
npm install --save-dev @meridial/swc-plugin
```

## Usage

### Next.js

```js
// next.config.js
module.exports = {
  experimental: {
    swcPlugins: [
      ["@meridial/swc-plugin", {
        rootPath: process.cwd()
      }]
    ]
  }
}
```

## Configuration

All options are optional.

```json
{
  "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:

```json
{
  "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](src/element_filter.rs). This list includes all standard HTML tags.

## ID stability

Two things invalidate an existing ID:

1. The file is moved or renamed (changes `relativePath`)
2. The JSX tree structure changes so the element's `astPath` shifts

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:

```tsx
// 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.