semantic-inspector 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 semantic-inspector contributors
3
+ Copyright (c) 2026 ghost-vk and contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,13 +1,23 @@
1
1
  # semantic-inspector
2
2
 
3
- A dev-only React inspector for vibe-coding. Hit a hotkey to enter inspect mode:
4
- hovering highlights the element under the cursor and shows its component name +
5
- `file:line`. **Click** copies that text identifier to the clipboard;
6
- **Shift+click** copies a PNG screenshot of just that element. Built for pasting
7
- precise UI context into an AI chat in seconds.
3
+ [![npm version](https://img.shields.io/npm/v/semantic-inspector.svg)](https://www.npmjs.com/package/semantic-inspector)
4
+ [![CI](https://github.com/ghost-vk/semantic-inspector/actions/workflows/ci.yml/badge.svg)](https://github.com/ghost-vk/semantic-inspector/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/semantic-inspector.svg)](./LICENSE)
8
6
 
9
- Stack: Vite + `@vitejs/plugin-react` + React 18/19. Zero runtime cost in
10
- production you gate where it mounts.
7
+ A dev-only React inspector for vibe-coding. Hit a hotkey to enter inspect mode: hovering highlights
8
+ the element under the cursor and shows its component name + `file:line:col`. **Click** copies that
9
+ text identifier to the clipboard; **Shift+click** copies a PNG screenshot of just that element.
10
+ Built for pasting precise UI context into an AI chat in seconds.
11
+
12
+ Stack: Vite + `@vitejs/plugin-react` + React 18/19. Designed to add **no production runtime cost
13
+ when you gate and lazy-load it** (see [Mount it](#2-mount-it-behind-your-own-dev-flag-ideally-lazy)) —
14
+ `modern-screenshot` is loaded lazily and the source-stamping plugin runs only on the dev server.
15
+
16
+ ## Demo
17
+
18
+ <video src="https://github.com/ghost-vk/semantic-inspector/raw/main/docs/demo.mp4" controls muted loop width="600"></video>
19
+
20
+ _Player not loading? [Watch the demo.](https://github.com/ghost-vk/semantic-inspector/raw/main/docs/demo.mp4)_
11
21
 
12
22
  ## Install
13
23
 
@@ -15,31 +25,49 @@ production — you gate where it mounts.
15
25
  npm i -D semantic-inspector
16
26
  ```
17
27
 
18
- `react` / `react-dom` are peer deps (>=18). `vite` is an optional peer — only
19
- needed if you use the Vite plugin entry point.
28
+ Peer dependencies:
29
+
30
+ - `react` / `react-dom` (`>=18`) — required.
31
+ - `vite` (`>=5`) — optional, only for `semantic-inspector/vite`.
32
+ - `@babel/core` (`>=7.25`) — optional, only for `semantic-inspector/vite` or
33
+ `semantic-inspector/babel`. Most Vite + React projects already have it; if not:
34
+ ```sh
35
+ npm i -D @babel/core
36
+ ```
37
+ Pure-runtime consumers (`<SemanticInspector/>` only) don't need it.
20
38
 
21
39
  ## How it works
22
40
 
23
- Source locations come from a **build-time stamp**, not React internals. A Babel
24
- pass adds `data-loc="<path>:<line>"` and `data-comp="<Component>"` to JSX host
25
- elements (`div`, `section`, …). The runtime reads those DOM attributes, so it
26
- stays robust across React versions. If a node isn't stamped (prod build, foreign
27
- node), it degrades gracefully: fiber `displayName` → filename → tag name.
41
+ Source locations come from a **build-time stamp**, not React internals. A Babel pass adds
42
+ `data-loc="<path>:<line>:<col>"` and `data-comp="<Component>"` to JSX host elements (`div`,
43
+ `section`, …). The runtime reads those DOM attributes, so it stays robust across React versions. If
44
+ a node isn't stamped (prod build, foreign node), it degrades gracefully: fiber `displayName` →
45
+ filename → tag name.
46
+
47
+ ```mermaid
48
+ flowchart LR
49
+ A[".tsx source"] -->|"Babel: stampLocVite / stampLocBabel<br/>(dev only)"| B["DOM with data-loc / data-comp"]
50
+ B -->|"hover → elementFromPoint"| C["resolveTarget<br/>closest([data-loc])"]
51
+ C --> D["Overlay highlight + tip"]
52
+ C -->|"click"| E["clipboard: text"]
53
+ C -->|"Shift+click"| F["clipboard: PNG (modern-screenshot)"]
54
+ ```
28
55
 
29
56
  ## Three entry points
30
57
 
31
- | Import | What it is |
32
- | ------------------------------- | ---------------------------------------------------------------------- |
33
- | `semantic-inspector` | `<SemanticInspector/>` — the overlay + hotkey + clipboard runtime. |
34
- | `semantic-inspector/vite` | `stampLocVite()` — Vite plugin that stamps `data-loc` / `data-comp`. |
35
- | `semantic-inspector/babel` | Raw Babel plugin, for the babel variant of `@vitejs/plugin-react`. |
58
+ | Import | What it is |
59
+ | -------------------------- | ------------------------------------------------------------------ |
60
+ | `semantic-inspector` | `<SemanticInspector/>` + `useInspector()` — overlay/hotkey/clipboard runtime. |
61
+ | `semantic-inspector/vite` | `stampLocVite()` — Vite plugin that stamps `data-loc` / `data-comp`. |
62
+ | `semantic-inspector/babel` | `{ stampLocBabel }` — raw Babel plugin, for the Babel variant of `@vitejs/plugin-react`. |
36
63
 
37
64
  ## Usage
38
65
 
39
66
  ### 1. Stamp source locations (Vite plugin)
40
67
 
41
- `@vitejs/plugin-react` **v6** transpiles via oxc (no Babel hook), so stamp with a
42
- separate `pre` plugin:
68
+ `@vitejs/plugin-react` **v6** transpiles via oxc (no Babel hook), so stamp with a separate `pre`
69
+ plugin. **This is the recommended path.** The plugin runs only on the dev server (`apply: 'serve'`),
70
+ so `data-loc` / `data-comp` never reach a production build.
43
71
 
44
72
  ```ts
45
73
  import react from '@vitejs/plugin-react';
@@ -52,15 +80,24 @@ export default defineConfig({
52
80
  });
53
81
  ```
54
82
 
55
- `rootDir` is the base for the relative path written into `data-loc`.
56
-
57
- On the **Babel variant** of plugin-react you can skip the separate pre-pass:
83
+ On the **Babel variant** of plugin-react you can skip the separate pre-pass by adding the plugin to
84
+ plugin-react's Babel options instead. Use **one** approach, not both. Note this forces plugin-react
85
+ onto Babel for all files (slower than the oxc + pre-pass above), so prefer option 1 unless you're
86
+ already on the Babel variant. Gate it to development so stamps stay out of production:
58
87
 
59
88
  ```ts
60
89
  import react from '@vitejs/plugin-react';
61
- import stampLoc from 'semantic-inspector/babel';
62
-
63
- react({ babel: { plugins: [[stampLoc, { rootDir: process.cwd() }]] } });
90
+ import { stampLocBabel } from 'semantic-inspector/babel';
91
+
92
+ export default defineConfig(({ command }) => ({
93
+ plugins: [
94
+ react({
95
+ babel: {
96
+ plugins: command === 'serve' ? [[stampLocBabel, { rootDir: process.cwd() }]] : []
97
+ }
98
+ })
99
+ ]
100
+ }));
64
101
  ```
65
102
 
66
103
  ### 2. Mount it (behind your own dev flag, ideally lazy)
@@ -81,33 +118,67 @@ const SemanticInspector = lazy(() =>
81
118
  }
82
119
  ```
83
120
 
84
- ## Props
121
+ ## API
85
122
 
86
- | prop | default | purpose |
87
- | ------------ | ------------------------ | ---------------------------------------- |
88
- | `hotkey` | `'Alt+Shift+S'` | toggle inspect mode (Esc exits) |
89
- | `formatText` | `` `${comp} — ${loc}` `` | format of the text copied on click |
90
- | `onCopy` | — | callback after a copy (telemetry/toasts) |
91
- | `onError` | — | callback on clipboard/screenshot failure |
123
+ ### `<SemanticInspector>` props
92
124
 
93
- ## Notes
125
+ | prop | default | purpose |
126
+ | ------------ | ------------------------ | ----------------------------------------- |
127
+ | `hotkey` | `'Alt+Shift+S'` | toggle inspect mode (Esc always exits) |
128
+ | `formatText` | `` `${comp} — ${loc}` `` | format of the text copied on click; receives `{ comp, loc }` (`loc` may be `null`) |
129
+ | `onCopy` | — | called after a successful copy |
130
+ | `onError` | — | called on a clipboard/screenshot failure |
94
131
 
95
- - `navigator.clipboard` requires a secure context (localhost / https) and a
96
- user gesture that's why copy happens on **click**, not hover.
97
- - Screenshots use `modern-screenshot` (DOM→canvas): cross-origin `<img>` without
98
- CORS and some exotic CSS may not render.
99
- - On a prod React build without `data-loc`, the name falls back to the fiber
100
- (minified) or tag — degraded mode. Full mode needs the build-time stamp.
132
+ `useInspector(props)` is also exported for building a custom overlay; it returns
133
+ `{ active, target }`. Note: used raw (not via `<SemanticInspector>`), it has no default `onError`,
134
+ so failures only surface via `console.warn` unless you pass one.
101
135
 
102
- ## Development
136
+ #### Callback payloads
103
137
 
104
- ```sh
105
- npm install
106
- npm test # vitest
107
- npm run typecheck
108
- npm run build # tsup -> dist/ (esm + cjs + d.ts)
109
- ```
138
+ - `onCopy('text', payload)` — `payload` is the copied string.
139
+ - `onCopy('screenshot', payload)` — `payload` is the **component name** (the PNG goes to the
140
+ clipboard, not to the callback).
141
+ - `onError(kind, err)` — `err` is the underlying error (`unknown`).
142
+
143
+ ### Plugin options (`stampLocVite` / `stampLocBabel`)
144
+
145
+ | option | default | applies to | purpose |
146
+ | ---------- | ---------------- | ------------ | ---------------------------------------------- |
147
+ | `rootDir` | `process.cwd()` | both | base for the relative path written into `data-loc` |
148
+ | `include` | `/\.[jt]sx$/` | `/vite` only | which module ids get stamped |
149
+ | `attrLoc` | `'data-loc'` | both | attribute name for `path:line:col` |
150
+ | `attrComp` | `'data-comp'` | both | attribute name for the component name |
151
+
152
+ Files outside `rootDir` degrade to their basename, so an absolute filesystem path never leaks into
153
+ the stamped DOM.
154
+
155
+ ## Hotkey format
156
+
157
+ `Modifier+...+Key`. Modifiers: `Alt`, `Shift`, `Ctrl` (or `Control`), `Meta` (or `Cmd`). The final
158
+ token is the key. Matching is case-insensitive and also matches the physical `event.code`, so
159
+ layout-shifted glyphs work (e.g. `Ctrl+Shift+/` matches even though Shift produces `?`). Digit and
160
+ punctuation keys are supported (`Alt+1`, `Ctrl+/`). `Esc` always exits inspect mode.
161
+
162
+ ## Troubleshooting
163
+
164
+ | Symptom | Cause | Fix |
165
+ | --- | --- | --- |
166
+ | Nothing copies, no error | `navigator.clipboard` needs a secure context | Use `localhost` or `https://`, not `http://192.168.x.x` |
167
+ | Screenshot is blank/partial | CORS-tainted canvas / unsupported CSS | Serve images with CORS headers; some CSS (cross-origin `<img>`, exotic filters) won't rasterize |
168
+ | `loc` shows `no source` / name is minified | Node wasn't stamped (prod build, or plugin not registered) | Register the stamper (Usage 1) and confirm it isn't gated out in dev |
169
+ | Hotkey does nothing | Focus is in an input, or a typo in the combo | Use a `Modifier+Key` combo (see above); try the default `Alt+Shift+S` |
170
+
171
+ ## Browser support
172
+
173
+ Works in current Chromium, Edge, Firefox, and Safari. Image-clipboard (Shift+click screenshot)
174
+ requires a `ClipboardItem`-capable browser; text copy works everywhere with a secure context.
175
+
176
+ ## Contributing
177
+
178
+ See [CONTRIBUTING.md](./CONTRIBUTING.md). In short: `npm ci`, then
179
+ `npm run lint && npm run typecheck && npm test && npm run build`. `dist/` is generated — build once
180
+ after cloning before `npm link`.
110
181
 
111
182
  ## License
112
183
 
113
- MIT
184
+ [MIT](./LICENSE) © ghost-vk
package/dist/babel.cjs CHANGED
@@ -1,64 +1,12 @@
1
1
  'use strict';
2
2
 
3
- // src/stampLocBabel.ts
4
- function isHostElement(name) {
5
- return name.type === "JSXIdentifier" && /^[a-z]/.test(name.name);
6
- }
7
- function hasAttr(el, attrName) {
8
- return el.attributes.some(
9
- (a) => a.type === "JSXAttribute" && a.name.type === "JSXIdentifier" && a.name.name === attrName
10
- );
11
- }
12
- function nearestComponentName(path) {
13
- let p = path;
14
- while (p) {
15
- const node = p.node;
16
- if (node.type === "FunctionDeclaration" && node.id && /^[A-Z]/.test(node.id.name)) {
17
- return node.id.name;
18
- }
19
- if (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
20
- const parent = p.parentPath?.node;
21
- if (parent?.type === "VariableDeclarator" && parent.id.type === "Identifier" && /^[A-Z]/.test(parent.id.name)) {
22
- return parent.id.name;
23
- }
24
- }
25
- p = p.parentPath;
26
- }
27
- return null;
28
- }
29
- function stampLocBabel(babel, opts = {}) {
30
- const t = babel.types;
31
- const attrLoc = opts.attrLoc ?? "data-loc";
32
- const attrComp = opts.attrComp ?? "data-comp";
33
- const rootDir = opts.rootDir ?? process.cwd();
34
- const attr = (name, value) => t.jsxAttribute(t.jsxIdentifier(name), t.stringLiteral(value));
35
- const toRel = (file) => {
36
- let root = rootDir;
37
- while (root.endsWith("/")) root = root.slice(0, -1);
38
- const rel = file.startsWith(root + "/") ? file.slice(root.length + 1) : file;
39
- return rel.split("\\").join("/");
40
- };
41
- return {
42
- name: "stamp-loc",
43
- visitor: {
44
- JSXOpeningElement(path, state) {
45
- const node = path.node;
46
- if (!isHostElement(node.name)) return;
47
- const filename = state.file.opts.filename;
48
- const loc = node.loc;
49
- if (!filename || !loc) return;
50
- if (!hasAttr(node, attrLoc)) {
51
- node.attributes.push(attr(attrLoc, `${toRel(filename)}:${loc.start.line}`));
52
- }
53
- if (!hasAttr(node, attrComp)) {
54
- const comp = nearestComponentName(path);
55
- if (comp) node.attributes.push(attr(attrComp, comp));
56
- }
57
- }
58
- }
59
- };
60
- }
3
+ var chunkX6NMJJ2M_cjs = require('./chunk-X6NMJJ2M.cjs');
61
4
 
62
- module.exports = stampLocBabel;
5
+
6
+
7
+ Object.defineProperty(exports, "stampLocBabel", {
8
+ enumerable: true,
9
+ get: function () { return chunkX6NMJJ2M_cjs.stampLocBabel; }
10
+ });
63
11
  //# sourceMappingURL=babel.cjs.map
64
12
  //# sourceMappingURL=babel.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/stampLocBabel.ts"],"names":[],"mappings":"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","file":"babel.cjs","sourcesContent":["import type { NodePath, PluginObj, types as BabelTypes } from '@babel/core';\n\nexport interface StampLocOptions {\n /** Имя атрибута пути. Default 'data-loc'. */\n attrLoc?: string;\n /** Имя атрибута компонента. Default 'data-comp'. */\n attrComp?: string;\n /** База для относительного пути в data-loc. Default process.cwd(). */\n rootDir?: string;\n}\n\nfunction isHostElement(name: BabelTypes.JSXOpeningElement['name']): boolean {\n return name.type === 'JSXIdentifier' && /^[a-z]/.test(name.name);\n}\n\nfunction hasAttr(el: BabelTypes.JSXOpeningElement, attrName: string): boolean {\n return el.attributes.some(\n (a) => a.type === 'JSXAttribute' && a.name.type === 'JSXIdentifier' && a.name.name === attrName\n );\n}\n\n// Ближайшая функция-компонент с PascalCase-именем вверх по дереву:\n// function Foo() {} | const Foo = () => {} | const Foo = function () {}\nfunction nearestComponentName(path: NodePath): string | null {\n let p: NodePath | null = path;\n while (p) {\n const node = p.node;\n if (node.type === 'FunctionDeclaration' && node.id && /^[A-Z]/.test(node.id.name)) {\n return node.id.name;\n }\n if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {\n const parent = p.parentPath?.node;\n if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier' && /^[A-Z]/.test(parent.id.name)) {\n return parent.id.name;\n }\n }\n p = p.parentPath;\n }\n return null;\n}\n\n/**\n * Babel-плагин: вешает data-loc=\"<path>:<line>\" и data-comp=\"<Component>\" на\n * JSX host-элементы (div, section, ...). Рантайм-инспектор читает эти DOM-атрибуты\n * (не React-internals), поэтому устойчив к версии React. Компонентные теги\n * (PascalCase) пропускаем — они не дают собственного DOM-узла.\n */\nexport default function stampLocBabel(babel: { types: typeof BabelTypes }, opts: StampLocOptions = {}): PluginObj {\n const t = babel.types;\n const attrLoc = opts.attrLoc ?? 'data-loc';\n const attrComp = opts.attrComp ?? 'data-comp';\n const rootDir = opts.rootDir ?? process.cwd();\n\n const attr = (name: string, value: string) => t.jsxAttribute(t.jsxIdentifier(name), t.stringLiteral(value));\n\n // path.relative без node:path, чтобы плагин не тянул узловые модули в чужих средах.\n const toRel = (file: string): string => {\n let root = rootDir;\n while (root.endsWith('/')) root = root.slice(0, -1);\n const rel = file.startsWith(root + '/') ? file.slice(root.length + 1) : file;\n return rel.split('\\\\').join('/');\n };\n\n return {\n name: 'stamp-loc',\n visitor: {\n JSXOpeningElement(path, state) {\n const node = path.node;\n if (!isHostElement(node.name)) return;\n\n const filename = state.file.opts.filename;\n const loc = node.loc;\n if (!filename || !loc) return;\n\n if (!hasAttr(node, attrLoc)) {\n node.attributes.push(attr(attrLoc, `${toRel(filename)}:${loc.start.line}`));\n }\n if (!hasAttr(node, attrComp)) {\n const comp = nearestComponentName(path);\n if (comp) node.attributes.push(attr(attrComp, comp));\n }\n }\n }\n };\n}\n"]}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"babel.cjs"}
package/dist/babel.d.cts CHANGED
@@ -1,21 +1,21 @@
1
- import { types, PluginObj } from '@babel/core';
1
+ import { ConfigAPI, types, PluginObj } from '@babel/core';
2
2
 
3
3
  interface StampLocOptions {
4
- /** Имя атрибута пути. Default 'data-loc'. */
4
+ /** Path attribute name. Default 'data-loc'. */
5
5
  attrLoc?: string;
6
- /** Имя атрибута компонента. Default 'data-comp'. */
6
+ /** Component attribute name. Default 'data-comp'. */
7
7
  attrComp?: string;
8
- /** База для относительного пути в data-loc. Default process.cwd(). */
8
+ /** Base for the relative path written into data-loc. Default process.cwd(). */
9
9
  rootDir?: string;
10
10
  }
11
11
  /**
12
- * Babel-плагин: вешает data-loc="<path>:<line>" и data-comp="<Component>" на
13
- * JSX host-элементы (div, section, ...). Рантайм-инспектор читает эти DOM-атрибуты
14
- * (не React-internals), поэтому устойчив к версии React. Компонентные теги
15
- * (PascalCase) пропускаем они не дают собственного DOM-узла.
12
+ * Babel plugin: stamps data-loc="<path>:<line>:<col>" and data-comp="<Component>" onto JSX
13
+ * host elements (div, section, ...). The runtime inspector reads these DOM attributes (not
14
+ * React internals), so it stays robust across React versions. Component tags (PascalCase) are
15
+ * skippedthey don't produce their own DOM node.
16
16
  */
17
- declare function stampLocBabel(babel: {
17
+ declare function stampLocBabel(api: ConfigAPI & {
18
18
  types: typeof types;
19
19
  }, opts?: StampLocOptions): PluginObj;
20
20
 
21
- export { type StampLocOptions, stampLocBabel as default };
21
+ export { type StampLocOptions, stampLocBabel };
package/dist/babel.d.ts CHANGED
@@ -1,21 +1,21 @@
1
- import { types, PluginObj } from '@babel/core';
1
+ import { ConfigAPI, types, PluginObj } from '@babel/core';
2
2
 
3
3
  interface StampLocOptions {
4
- /** Имя атрибута пути. Default 'data-loc'. */
4
+ /** Path attribute name. Default 'data-loc'. */
5
5
  attrLoc?: string;
6
- /** Имя атрибута компонента. Default 'data-comp'. */
6
+ /** Component attribute name. Default 'data-comp'. */
7
7
  attrComp?: string;
8
- /** База для относительного пути в data-loc. Default process.cwd(). */
8
+ /** Base for the relative path written into data-loc. Default process.cwd(). */
9
9
  rootDir?: string;
10
10
  }
11
11
  /**
12
- * Babel-плагин: вешает data-loc="<path>:<line>" и data-comp="<Component>" на
13
- * JSX host-элементы (div, section, ...). Рантайм-инспектор читает эти DOM-атрибуты
14
- * (не React-internals), поэтому устойчив к версии React. Компонентные теги
15
- * (PascalCase) пропускаем они не дают собственного DOM-узла.
12
+ * Babel plugin: stamps data-loc="<path>:<line>:<col>" and data-comp="<Component>" onto JSX
13
+ * host elements (div, section, ...). The runtime inspector reads these DOM attributes (not
14
+ * React internals), so it stays robust across React versions. Component tags (PascalCase) are
15
+ * skippedthey don't produce their own DOM node.
16
16
  */
17
- declare function stampLocBabel(babel: {
17
+ declare function stampLocBabel(api: ConfigAPI & {
18
18
  types: typeof types;
19
19
  }, opts?: StampLocOptions): PluginObj;
20
20
 
21
- export { type StampLocOptions, stampLocBabel as default };
21
+ export { type StampLocOptions, stampLocBabel };
package/dist/babel.js CHANGED
@@ -1,3 +1,3 @@
1
- export { stampLocBabel as default } from './chunk-AAPCI2HO.js';
1
+ export { stampLocBabel } from './chunk-AQYKTX6L.js';
2
2
  //# sourceMappingURL=babel.js.map
3
3
  //# sourceMappingURL=babel.js.map
@@ -1,3 +1,5 @@
1
+ import { relative, isAbsolute, sep } from 'path';
2
+
1
3
  // src/stampLocBabel.ts
2
4
  function isHostElement(name) {
3
5
  return name.type === "JSXIdentifier" && /^[a-z]/.test(name.name);
@@ -24,17 +26,19 @@ function nearestComponentName(path) {
24
26
  }
25
27
  return null;
26
28
  }
27
- function stampLocBabel(babel, opts = {}) {
28
- const t = babel.types;
29
+ function stampLocBabel(api, opts = {}) {
30
+ api.assertVersion(7);
31
+ const t = api.types;
29
32
  const attrLoc = opts.attrLoc ?? "data-loc";
30
33
  const attrComp = opts.attrComp ?? "data-comp";
31
34
  const rootDir = opts.rootDir ?? process.cwd();
32
35
  const attr = (name, value) => t.jsxAttribute(t.jsxIdentifier(name), t.stringLiteral(value));
33
36
  const toRel = (file) => {
34
- let root = rootDir;
35
- while (root.endsWith("/")) root = root.slice(0, -1);
36
- const rel = file.startsWith(root + "/") ? file.slice(root.length + 1) : file;
37
- return rel.split("\\").join("/");
37
+ const rel = relative(rootDir, file);
38
+ if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
39
+ return file.split(/[\\/]/).pop() ?? "unknown";
40
+ }
41
+ return rel.split(sep).join("/");
38
42
  };
39
43
  return {
40
44
  name: "stamp-loc",
@@ -46,7 +50,7 @@ function stampLocBabel(babel, opts = {}) {
46
50
  const loc = node.loc;
47
51
  if (!filename || !loc) return;
48
52
  if (!hasAttr(node, attrLoc)) {
49
- node.attributes.push(attr(attrLoc, `${toRel(filename)}:${loc.start.line}`));
53
+ node.attributes.push(attr(attrLoc, `${toRel(filename)}:${loc.start.line}:${loc.start.column + 1}`));
50
54
  }
51
55
  if (!hasAttr(node, attrComp)) {
52
56
  const comp = nearestComponentName(path);
@@ -58,5 +62,5 @@ function stampLocBabel(babel, opts = {}) {
58
62
  }
59
63
 
60
64
  export { stampLocBabel };
61
- //# sourceMappingURL=chunk-AAPCI2HO.js.map
62
- //# sourceMappingURL=chunk-AAPCI2HO.js.map
65
+ //# sourceMappingURL=chunk-AQYKTX6L.js.map
66
+ //# sourceMappingURL=chunk-AQYKTX6L.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stampLocBabel.ts"],"names":[],"mappings":"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","file":"chunk-AQYKTX6L.js","sourcesContent":["import { isAbsolute, relative, sep } from 'node:path';\nimport type { types as BabelTypes, ConfigAPI, NodePath, PluginObj } from '@babel/core';\n\nexport interface StampLocOptions {\n /** Path attribute name. Default 'data-loc'. */\n attrLoc?: string;\n /** Component attribute name. Default 'data-comp'. */\n attrComp?: string;\n /** Base for the relative path written into data-loc. Default process.cwd(). */\n rootDir?: string;\n}\n\nfunction isHostElement(name: BabelTypes.JSXOpeningElement['name']): boolean {\n return name.type === 'JSXIdentifier' && /^[a-z]/.test(name.name);\n}\n\nfunction hasAttr(el: BabelTypes.JSXOpeningElement, attrName: string): boolean {\n return el.attributes.some(\n (a) => a.type === 'JSXAttribute' && a.name.type === 'JSXIdentifier' && a.name.name === attrName\n );\n}\n\n// Nearest PascalCase function-component ancestor:\n// function Foo() {} | const Foo = () => {} | const Foo = function () {}\nfunction nearestComponentName(path: NodePath): string | null {\n let p: NodePath | null = path;\n while (p) {\n const node = p.node;\n if (node.type === 'FunctionDeclaration' && node.id && /^[A-Z]/.test(node.id.name)) {\n return node.id.name;\n }\n if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {\n const parent = p.parentPath?.node;\n if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier' && /^[A-Z]/.test(parent.id.name)) {\n return parent.id.name;\n }\n }\n p = p.parentPath;\n }\n return null;\n}\n\n/**\n * Babel plugin: stamps data-loc=\"<path>:<line>:<col>\" and data-comp=\"<Component>\" onto JSX\n * host elements (div, section, ...). The runtime inspector reads these DOM attributes (not\n * React internals), so it stays robust across React versions. Component tags (PascalCase) are\n * skipped — they don't produce their own DOM node.\n */\nexport function stampLocBabel(api: ConfigAPI & { types: typeof BabelTypes }, opts: StampLocOptions = {}): PluginObj {\n api.assertVersion(7);\n const t = api.types;\n const attrLoc = opts.attrLoc ?? 'data-loc';\n const attrComp = opts.attrComp ?? 'data-comp';\n const rootDir = opts.rootDir ?? process.cwd();\n\n const attr = (name: string, value: string) => t.jsxAttribute(t.jsxIdentifier(name), t.stringLiteral(value));\n\n // Relative POSIX path from rootDir. Files outside rootDir degrade to their basename so an\n // absolute filesystem path can never leak into the stamped DOM.\n const toRel = (file: string): string => {\n const rel = relative(rootDir, file);\n if (!rel || rel.startsWith('..') || isAbsolute(rel)) {\n return file.split(/[\\\\/]/).pop() ?? 'unknown';\n }\n return rel.split(sep).join('/');\n };\n\n return {\n name: 'stamp-loc',\n visitor: {\n JSXOpeningElement(path, state) {\n const node = path.node;\n if (!isHostElement(node.name)) return;\n\n const filename = state.file.opts.filename;\n const loc = node.loc;\n if (!filename || !loc) return;\n\n if (!hasAttr(node, attrLoc)) {\n node.attributes.push(attr(attrLoc, `${toRel(filename)}:${loc.start.line}:${loc.start.column + 1}`));\n }\n if (!hasAttr(node, attrComp)) {\n const comp = nearestComponentName(path);\n if (comp) node.attributes.push(attr(attrComp, comp));\n }\n }\n }\n };\n}\n"]}
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ var path = require('path');
4
+
5
+ // src/stampLocBabel.ts
6
+ function isHostElement(name) {
7
+ return name.type === "JSXIdentifier" && /^[a-z]/.test(name.name);
8
+ }
9
+ function hasAttr(el, attrName) {
10
+ return el.attributes.some(
11
+ (a) => a.type === "JSXAttribute" && a.name.type === "JSXIdentifier" && a.name.name === attrName
12
+ );
13
+ }
14
+ function nearestComponentName(path) {
15
+ let p = path;
16
+ while (p) {
17
+ const node = p.node;
18
+ if (node.type === "FunctionDeclaration" && node.id && /^[A-Z]/.test(node.id.name)) {
19
+ return node.id.name;
20
+ }
21
+ if (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
22
+ const parent = p.parentPath?.node;
23
+ if (parent?.type === "VariableDeclarator" && parent.id.type === "Identifier" && /^[A-Z]/.test(parent.id.name)) {
24
+ return parent.id.name;
25
+ }
26
+ }
27
+ p = p.parentPath;
28
+ }
29
+ return null;
30
+ }
31
+ function stampLocBabel(api, opts = {}) {
32
+ api.assertVersion(7);
33
+ const t = api.types;
34
+ const attrLoc = opts.attrLoc ?? "data-loc";
35
+ const attrComp = opts.attrComp ?? "data-comp";
36
+ const rootDir = opts.rootDir ?? process.cwd();
37
+ const attr = (name, value) => t.jsxAttribute(t.jsxIdentifier(name), t.stringLiteral(value));
38
+ const toRel = (file) => {
39
+ const rel = path.relative(rootDir, file);
40
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
41
+ return file.split(/[\\/]/).pop() ?? "unknown";
42
+ }
43
+ return rel.split(path.sep).join("/");
44
+ };
45
+ return {
46
+ name: "stamp-loc",
47
+ visitor: {
48
+ JSXOpeningElement(path, state) {
49
+ const node = path.node;
50
+ if (!isHostElement(node.name)) return;
51
+ const filename = state.file.opts.filename;
52
+ const loc = node.loc;
53
+ if (!filename || !loc) return;
54
+ if (!hasAttr(node, attrLoc)) {
55
+ node.attributes.push(attr(attrLoc, `${toRel(filename)}:${loc.start.line}:${loc.start.column + 1}`));
56
+ }
57
+ if (!hasAttr(node, attrComp)) {
58
+ const comp = nearestComponentName(path);
59
+ if (comp) node.attributes.push(attr(attrComp, comp));
60
+ }
61
+ }
62
+ }
63
+ };
64
+ }
65
+
66
+ exports.stampLocBabel = stampLocBabel;
67
+ //# sourceMappingURL=chunk-X6NMJJ2M.cjs.map
68
+ //# sourceMappingURL=chunk-X6NMJJ2M.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stampLocBabel.ts"],"names":["relative","isAbsolute","sep"],"mappings":"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","file":"chunk-X6NMJJ2M.cjs","sourcesContent":["import { isAbsolute, relative, sep } from 'node:path';\nimport type { types as BabelTypes, ConfigAPI, NodePath, PluginObj } from '@babel/core';\n\nexport interface StampLocOptions {\n /** Path attribute name. Default 'data-loc'. */\n attrLoc?: string;\n /** Component attribute name. Default 'data-comp'. */\n attrComp?: string;\n /** Base for the relative path written into data-loc. Default process.cwd(). */\n rootDir?: string;\n}\n\nfunction isHostElement(name: BabelTypes.JSXOpeningElement['name']): boolean {\n return name.type === 'JSXIdentifier' && /^[a-z]/.test(name.name);\n}\n\nfunction hasAttr(el: BabelTypes.JSXOpeningElement, attrName: string): boolean {\n return el.attributes.some(\n (a) => a.type === 'JSXAttribute' && a.name.type === 'JSXIdentifier' && a.name.name === attrName\n );\n}\n\n// Nearest PascalCase function-component ancestor:\n// function Foo() {} | const Foo = () => {} | const Foo = function () {}\nfunction nearestComponentName(path: NodePath): string | null {\n let p: NodePath | null = path;\n while (p) {\n const node = p.node;\n if (node.type === 'FunctionDeclaration' && node.id && /^[A-Z]/.test(node.id.name)) {\n return node.id.name;\n }\n if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {\n const parent = p.parentPath?.node;\n if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier' && /^[A-Z]/.test(parent.id.name)) {\n return parent.id.name;\n }\n }\n p = p.parentPath;\n }\n return null;\n}\n\n/**\n * Babel plugin: stamps data-loc=\"<path>:<line>:<col>\" and data-comp=\"<Component>\" onto JSX\n * host elements (div, section, ...). The runtime inspector reads these DOM attributes (not\n * React internals), so it stays robust across React versions. Component tags (PascalCase) are\n * skipped — they don't produce their own DOM node.\n */\nexport function stampLocBabel(api: ConfigAPI & { types: typeof BabelTypes }, opts: StampLocOptions = {}): PluginObj {\n api.assertVersion(7);\n const t = api.types;\n const attrLoc = opts.attrLoc ?? 'data-loc';\n const attrComp = opts.attrComp ?? 'data-comp';\n const rootDir = opts.rootDir ?? process.cwd();\n\n const attr = (name: string, value: string) => t.jsxAttribute(t.jsxIdentifier(name), t.stringLiteral(value));\n\n // Relative POSIX path from rootDir. Files outside rootDir degrade to their basename so an\n // absolute filesystem path can never leak into the stamped DOM.\n const toRel = (file: string): string => {\n const rel = relative(rootDir, file);\n if (!rel || rel.startsWith('..') || isAbsolute(rel)) {\n return file.split(/[\\\\/]/).pop() ?? 'unknown';\n }\n return rel.split(sep).join('/');\n };\n\n return {\n name: 'stamp-loc',\n visitor: {\n JSXOpeningElement(path, state) {\n const node = path.node;\n if (!isHostElement(node.name)) return;\n\n const filename = state.file.opts.filename;\n const loc = node.loc;\n if (!filename || !loc) return;\n\n if (!hasAttr(node, attrLoc)) {\n node.attributes.push(attr(attrLoc, `${toRel(filename)}:${loc.start.line}:${loc.start.column + 1}`));\n }\n if (!hasAttr(node, attrComp)) {\n const comp = nearestComponentName(path);\n if (comp) node.attributes.push(attr(attrComp, comp));\n }\n }\n }\n };\n}\n"]}