Skip to content

Commit 52fe28e

Browse files
konardclaude
andcommitted
Merge branch 'main' into issue-19-793570d5e6b2
Resolved conflicts by combining changes: - Kept babelPluginName constant from issue-19 - Accepted attribute rename from "path" to "data-path" from main - Accepted normalizeModuleId function from main - Accepted configurable attributeName option from main - Removed ViteBabelState type as it's not used (issue-19 goal) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2 parents 659070b + c2e6ef5 commit 52fe28e

17 files changed

Lines changed: 409 additions & 81 deletions

README.md

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
11
# @prover-coder-ai/component-tagger
22

3-
Vite plugin that adds a single `path` attribute to every JSX opening tag.
3+
Vite and Babel plugin that adds a `data-path` attribute to every JSX opening tag, enabling component source location tracking.
44

5-
Example output:
5+
## Example output
66

77
```html
8-
<h1 path="src/App.tsx:22:4">Hello</h1>
8+
<h1 data-path="src/App.tsx:22:4">Hello</h1>
99
```
1010

1111
Format: `<relative-file-path>:<line>:<column>`
1212

13+
## Features
14+
15+
-**Idempotent**: adds `data-path` only if it doesn't already exist
16+
-**HTML5 compliant**: uses standard `data-*` attributes
17+
-**Configurable**: customize the attribute name via options
18+
-**Dual plugin support**: works with both Vite and Babel
19+
20+
## Installation
21+
22+
```bash
23+
npm install @prover-coder-ai/component-tagger
24+
```
25+
1326
## Usage
1427

28+
### Vite Plugin
29+
1530
```ts
1631
import { defineConfig, type PluginOption } from "vite"
1732
import { componentTagger } from "@prover-coder-ai/component-tagger"
@@ -23,3 +38,84 @@ export default defineConfig(({ mode }) => {
2338
return { plugins }
2439
})
2540
```
41+
42+
**With custom attribute name:**
43+
44+
```ts
45+
const plugins = [
46+
isDevelopment && componentTagger({ attributeName: "data-component-path" })
47+
].filter(Boolean) as PluginOption[]
48+
```
49+
50+
### Babel Plugin (e.g., Next.js)
51+
52+
Add to your `.babelrc`:
53+
54+
```json
55+
{
56+
"presets": ["next/babel"],
57+
"env": {
58+
"development": {
59+
"plugins": ["@prover-coder-ai/component-tagger/babel"]
60+
}
61+
}
62+
}
63+
```
64+
65+
**With options:**
66+
67+
```json
68+
{
69+
"presets": ["next/babel"],
70+
"env": {
71+
"development": {
72+
"plugins": [
73+
[
74+
"@prover-coder-ai/component-tagger/babel",
75+
{
76+
"rootDir": "/custom/root",
77+
"attributeName": "data-component-path"
78+
}
79+
]
80+
]
81+
}
82+
}
83+
}
84+
```
85+
86+
## Options
87+
88+
### Vite Plugin Options
89+
90+
```ts
91+
type ComponentTaggerOptions = {
92+
/**
93+
* Name of the attribute to add to JSX elements.
94+
* @default "data-path"
95+
*/
96+
attributeName?: string
97+
}
98+
```
99+
100+
### Babel Plugin Options
101+
102+
```ts
103+
type ComponentTaggerBabelPluginOptions = {
104+
/**
105+
* Root directory for computing relative paths.
106+
* @default process.cwd()
107+
*/
108+
rootDir?: string
109+
/**
110+
* Name of the attribute to add to JSX elements.
111+
* @default "data-path"
112+
*/
113+
attributeName?: string
114+
}
115+
```
116+
117+
## Behavior Guarantees
118+
119+
- **Idempotency**: If `data-path` (or custom attribute) already exists on an element, no duplicate is added
120+
- **Default attribute**: `data-path` is used when no `attributeName` is specified
121+
- **Standard compliance**: Uses HTML5 `data-*` custom attributes by default

packages/app/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# @prover-coder-ai/component-tagger
22

3+
## 1.0.25
4+
5+
### Patch Changes
6+
7+
- chore: automated version bump
8+
39
## 1.0.24
410

511
### Patch Changes

packages/app/babel.cjs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
* "plugins": ["@prover-coder-ai/component-tagger/babel"]
1212
* }
1313
*/
14-
// CHANGE: provide CommonJS entry point for Babel plugin.
15-
// WHY: Babel configuration often requires CommonJS modules.
16-
// REF: issue-12
14+
// CHANGE: provide CommonJS entry point for Babel plugin with configurable attributeName.
15+
// WHY: Babel configuration often requires CommonJS modules; support custom attribute names.
16+
// REF: issue-12, issue-14
1717
// FORMAT THEOREM: forall require: require(babel.cjs) -> PluginFactory
1818
// PURITY: SHELL
1919
// EFFECT: n/a
@@ -23,7 +23,7 @@
2323
const path = require("node:path")
2424

2525
const babelPluginName = "component-path-babel-tagger"
26-
const componentPathAttributeName = "path"
26+
const componentPathAttributeName = "data-path"
2727
const jsxFilePattern = /\.(tsx|jsx)(\?.*)?$/u
2828

2929
const isJsxFile = (id) => jsxFilePattern.test(id)
@@ -59,21 +59,22 @@ module.exports = function componentTaggerBabelPlugin({ types: t }) {
5959
return
6060
}
6161

62-
// Skip if already has path attribute
63-
if (attrExists(node, componentPathAttributeName, t)) {
64-
return
65-
}
66-
67-
// Compute relative path from root
62+
// Compute relative path from root and get attribute name
6863
const opts = state.opts || {}
6964
const rootDir = opts.rootDir || state.cwd || process.cwd()
65+
const attributeName = opts.attributeName || componentPathAttributeName
7066
const relativeFilename = path.relative(rootDir, filename)
7167

68+
// Skip if already has the specified attribute (idempotency)
69+
if (attrExists(node, attributeName, t)) {
70+
return
71+
}
72+
7273
const { column, line } = node.loc.start
7374
const value = formatComponentPathValue(relativeFilename, line, column)
7475

7576
node.attributes.push(
76-
t.jsxAttribute(t.jsxIdentifier(componentPathAttributeName), t.stringLiteral(value))
77+
t.jsxAttribute(t.jsxIdentifier(attributeName), t.stringLiteral(value))
7778
)
7879
}
7980
}

packages/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@prover-coder-ai/component-tagger",
3-
"version": "1.0.24",
3+
"version": "1.0.25",
44
"description": "Component tagger Vite plugin and Babel plugin for JSX metadata",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

packages/app/src/core/component-path.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,46 @@ const jsxFilePattern = /\.(tsx|jsx)(\?.*)?$/u
1212
// COMPLEXITY: O(1)/O(1)
1313
export const babelPluginName = "component-path-babel-tagger"
1414

15-
// CHANGE: define canonical attribute name for component path tagging.
16-
// WHY: reduce metadata to a single attribute while keeping full source location.
17-
// QUOTE(TZ): "\u0421\u0430\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0442\u0435\u043a\u0443\u0449\u0435\u043c app \u043d\u043e \u0432\u043e\u0442 \u0447\u0442\u043e \u0431\u044b \u0435\u0433\u043e \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0434\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u043f\u0440\u043e\u0435\u043a\u0442 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0430\u0448 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0430\u043f\u043f \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c"
18-
// REF: user-2026-01-14-frontend-consumer
15+
/**
16+
* Normalizes a module ID by stripping query parameters.
17+
*
18+
* Vite and other bundlers may append query parameters to module IDs
19+
* (e.g., "src/App.tsx?import" or "src/App.tsx?v=123"). This function
20+
* returns the clean file path without query string.
21+
*
22+
* @param id - Module ID (may include query parameters).
23+
* @returns Clean path without query string.
24+
*
25+
* @pure true
26+
* @invariant ∀ id: normalizeModuleId(id) does not contain '?'
27+
* @complexity O(n) time / O(1) space where n = |id|
28+
*/
29+
// CHANGE: centralize query stripping as a pure function in core.
30+
// WHY: unify module ID normalization in one place as requested in issue #18.
31+
// QUOTE(ТЗ): "Вынести stripQuery() (или normalizeModuleId()) в core, использовать в Vite и (при желании) в isJsxFile."
32+
// REF: REQ-18 (issue #18)
1933
// SOURCE: n/a
20-
// FORMAT THEOREM: forall a in AttributeName: a = "path"
34+
// FORMAT THEOREM: ∀ id: normalizeModuleId(id) = id.split('?')[0]
35+
// PURITY: CORE
36+
// EFFECT: n/a
37+
// INVARIANT: result contains no query string
38+
// COMPLEXITY: O(n)/O(1)
39+
export const normalizeModuleId = (id: string): string => {
40+
const queryIndex = id.indexOf("?")
41+
return queryIndex === -1 ? id : id.slice(0, queryIndex)
42+
}
43+
44+
// CHANGE: rename attribute from "path" to "data-path" for HTML5 compliance.
45+
// WHY: data-* attributes are standard HTML5 custom data attributes, improving compatibility.
46+
// QUOTE(issue-14): "Rename attribute path → data-path (breaking change)"
47+
// REF: issue-14
48+
// SOURCE: https://html.spec.whatwg.org/multipage/dom.html#custom-data-attribute
49+
// FORMAT THEOREM: forall a in AttributeName: a = "data-path"
2150
// PURITY: CORE
2251
// EFFECT: n/a
2352
// INVARIANT: attribute name remains stable across transforms
2453
// COMPLEXITY: O(1)/O(1)
25-
export const componentPathAttributeName = "path"
54+
export const componentPathAttributeName = "data-path"
2655

2756
/**
2857
* Checks whether the Vite id represents a JSX or TSX module.
@@ -36,7 +65,7 @@ export const componentPathAttributeName = "path"
3665
*/
3766
// CHANGE: centralize JSX file detection as a pure predicate.
3867
// WHY: keep file filtering in the functional core for testability.
39-
// QUOTE(TZ): "\u0421\u0430\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0442\u0435\u043a\u0443\u0449\u0435\u043c app \u043d\u043e \u0432\u043e\u0442 \u0447\u0442\u043e \u0431\u044b \u0435\u0433\u043e \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0434\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u043f\u0440\u043e\u0435\u043a\u0442 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0430\u0448 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0430\u043f\u043f \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c"
68+
// QUOTE(TZ): "Сам компонент должен быть в текущем app но вот что бы его протестировать надо создать ещё один проект который наш текущий апп будет подключать"
4069
// REF: user-2026-01-14-frontend-consumer
4170
// SOURCE: n/a
4271
// FORMAT THEOREM: forall id in ModuleId: isJsxFile(id) -> matches(id, jsxFilePattern)
@@ -60,7 +89,7 @@ export const isJsxFile = (id: string): boolean => jsxFilePattern.test(id)
6089
*/
6190
// CHANGE: provide a pure formatter for component location payloads.
6291
// WHY: reuse a single, deterministic encoding for UI metadata.
63-
// QUOTE(TZ): "\u0421\u0430\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0442\u0435\u043a\u0443\u0449\u0435\u043c app \u043d\u043e \u0432\u043e\u0442 \u0447\u0442\u043e \u0431\u044b \u0435\u0433\u043e \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0434\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u043f\u0440\u043e\u0435\u043a\u0442 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0430\u0448 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0430\u043f\u043f \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c"
92+
// QUOTE(TZ): "Сам компонент должен быть в текущем app но вот что бы его протестировать надо создать ещё один проект который наш текущий апп будет подключать"
6493
// REF: user-2026-01-14-frontend-consumer
6594
// SOURCE: n/a
6695
// FORMAT THEOREM: forall p,l,c: formatComponentPathValue(p,l,c) = concat(p, ":", l, ":", c)

packages/app/src/core/jsx-tagger.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { types as t, Visitor } from "@babel/core"
22

3-
import { componentPathAttributeName, formatComponentPathValue } from "./component-path.js"
3+
import { formatComponentPathValue } from "./component-path.js"
44

55
/**
66
* Context required for JSX tagging.
@@ -12,6 +12,10 @@ export type JsxTaggerContext = {
1212
* Relative file path from the project root.
1313
*/
1414
readonly relativeFilename: string
15+
/**
16+
* Name of the attribute to add (defaults to "data-path").
17+
*/
18+
readonly attributeName: string
1519
}
1620

1721
/**
@@ -41,32 +45,34 @@ export const attrExists = (node: t.JSXOpeningElement, attrName: string, types: t
4145
/**
4246
* Creates a JSX attribute with the component path value.
4347
*
48+
* @param attributeName - Name of the attribute to create.
4449
* @param relativeFilename - Relative path to the file.
4550
* @param line - 1-based line number.
4651
* @param column - 0-based column number.
4752
* @param types - Babel types module.
4853
* @returns JSX attribute node with the path value.
4954
*
5055
* @pure true
51-
* @invariant attribute name is always componentPathAttributeName
56+
* @invariant attribute name matches the provided attributeName parameter
5257
* @complexity O(1)
5358
*/
54-
// CHANGE: extract attribute creation as a pure factory.
55-
// WHY: single point for attribute creation ensures consistency.
56-
// REF: issue-12 (unified interface request)
57-
// FORMAT THEOREM: ∀ f, l, c: createPathAttribute(f, l, c) = JSXAttribute(path, f:l:c)
59+
// CHANGE: add attributeName parameter for configurable attribute names.
60+
// WHY: support customizable attribute names while maintaining default "data-path".
61+
// REF: issue-14 (add attributeName option)
62+
// FORMAT THEOREM: ∀ n, f, l, c: createPathAttribute(n, f, l, c) = JSXAttribute(n, f:l:c)
5863
// PURITY: CORE
5964
// EFFECT: n/a
60-
// INVARIANT: output format is always path:line:column
65+
// INVARIANT: output format is always path:line:column with configurable attribute name
6166
// COMPLEXITY: O(1)/O(1)
6267
export const createPathAttribute = (
68+
attributeName: string,
6369
relativeFilename: string,
6470
line: number,
6571
column: number,
6672
types: typeof t
6773
): t.JSXAttribute => {
6874
const value = formatComponentPathValue(relativeFilename, line, column)
69-
return types.jsxAttribute(types.jsxIdentifier(componentPathAttributeName), types.stringLiteral(value))
75+
return types.jsxAttribute(types.jsxIdentifier(attributeName), types.stringLiteral(value))
7076
}
7177

7278
/**
@@ -76,12 +82,12 @@ export const createPathAttribute = (
7682
* Both the Vite plugin and standalone Babel plugin use this function.
7783
*
7884
* @param node - JSX opening element to process.
79-
* @param context - Tagging context with relative filename.
85+
* @param context - Tagging context with relative filename and attribute name.
8086
* @param types - Babel types module.
8187
* @returns true if attribute was added, false if skipped.
8288
*
8389
* @pure false (mutates node)
84-
* @invariant each JSX element has at most one path attribute after processing
90+
* @invariant each JSX element has at most one instance of the specified attribute after processing
8591
* @complexity O(n) where n = number of existing attributes
8692
*/
8793
// CHANGE: extract unified JSX element processing logic.
@@ -103,13 +109,13 @@ export const processJsxElement = (
103109
return false
104110
}
105111

106-
// Skip if already has path attribute (idempotency)
107-
if (attrExists(node, componentPathAttributeName, types)) {
112+
// Skip if already has the specified attribute (idempotency)
113+
if (attrExists(node, context.attributeName, types)) {
108114
return false
109115
}
110116

111117
const { column, line } = node.loc.start
112-
const attr = createPathAttribute(context.relativeFilename, line, column, types)
118+
const attr = createPathAttribute(context.attributeName, context.relativeFilename, line, column, types)
113119

114120
node.attributes.push(attr)
115121
return true

packages/app/src/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
// EFFECT: n/a
99
// INVARIANT: exports remain stable for consumers
1010
// COMPLEXITY: O(1)/O(1)
11-
export { componentPathAttributeName, formatComponentPathValue, isJsxFile } from "./core/component-path.js"
11+
export {
12+
componentPathAttributeName,
13+
formatComponentPathValue,
14+
isJsxFile,
15+
normalizeModuleId
16+
} from "./core/component-path.js"
1217
export {
1318
attrExists,
1419
createJsxTaggerVisitor,
@@ -17,4 +22,4 @@ export {
1722
processJsxElement
1823
} from "./core/jsx-tagger.js"
1924
export { componentTaggerBabelPlugin, type ComponentTaggerBabelPluginOptions } from "./shell/babel-plugin.js"
20-
export { componentTagger } from "./shell/component-tagger.js"
25+
export { componentTagger, type ComponentTaggerOptions } from "./shell/component-tagger.js"

0 commit comments

Comments
 (0)