@prover-coder-ai/component-tagger 1.0.24 → 1.0.26

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/README.md CHANGED
@@ -1,17 +1,32 @@
1
1
  # @prover-coder-ai/component-tagger
2
2
 
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.
4
4
 
5
- Example output:
5
+ ## Example output
6
6
 
7
7
  ```html
8
- <h1 path="src/App.tsx:22:4">Hello</h1>
8
+ <h1 data-path="src/App.tsx:22:4">Hello</h1>
9
9
  ```
10
10
 
11
11
  Format: `<relative-file-path>:<line>:<column>`
12
12
 
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
+
13
26
  ## Usage
14
27
 
28
+ ### Vite Plugin
29
+
15
30
  ```ts
16
31
  import { defineConfig, type PluginOption } from "vite"
17
32
  import { componentTagger } from "@prover-coder-ai/component-tagger"
@@ -23,3 +38,84 @@ export default defineConfig(({ mode }) => {
23
38
  return { plugins }
24
39
  })
25
40
  ```
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
package/babel.cjs CHANGED
@@ -11,9 +11,9 @@
11
11
  * "plugins": ["@prover-coder-ai/component-tagger/babel"]
12
12
  * }
13
13
  */
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
17
17
  // FORMAT THEOREM: forall require: require(babel.cjs) -> PluginFactory
18
18
  // PURITY: SHELL
19
19
  // EFFECT: n/a
@@ -22,7 +22,7 @@
22
22
 
23
23
  const path = require("node:path")
24
24
 
25
- const componentPathAttributeName = "path"
25
+ const componentPathAttributeName = "data-path"
26
26
  const jsxFilePattern = /\.(tsx|jsx)(\?.*)?$/u
27
27
 
28
28
  const isJsxFile = (id) => jsxFilePattern.test(id)
@@ -58,21 +58,22 @@ module.exports = function componentTaggerBabelPlugin({ types: t }) {
58
58
  return
59
59
  }
60
60
 
61
- // Skip if already has path attribute
62
- if (attrExists(node, componentPathAttributeName, t)) {
63
- return
64
- }
65
-
66
- // Compute relative path from root
61
+ // Compute relative path from root and get attribute name
67
62
  const opts = state.opts || {}
68
63
  const rootDir = opts.rootDir || state.cwd || process.cwd()
64
+ const attributeName = opts.attributeName || componentPathAttributeName
69
65
  const relativeFilename = path.relative(rootDir, filename)
70
66
 
67
+ // Skip if already has the specified attribute (idempotency)
68
+ if (attrExists(node, attributeName, t)) {
69
+ return
70
+ }
71
+
71
72
  const { column, line } = node.loc.start
72
73
  const value = formatComponentPathValue(relativeFilename, line, column)
73
74
 
74
75
  node.attributes.push(
75
- t.jsxAttribute(t.jsxIdentifier(componentPathAttributeName), t.stringLiteral(value))
76
+ t.jsxAttribute(t.jsxIdentifier(attributeName), t.stringLiteral(value))
76
77
  )
77
78
  }
78
79
  }
@@ -1,4 +1,19 @@
1
- export declare const componentPathAttributeName = "path";
1
+ /**
2
+ * Normalizes a module ID by stripping query parameters.
3
+ *
4
+ * Vite and other bundlers may append query parameters to module IDs
5
+ * (e.g., "src/App.tsx?import" or "src/App.tsx?v=123"). This function
6
+ * returns the clean file path without query string.
7
+ *
8
+ * @param id - Module ID (may include query parameters).
9
+ * @returns Clean path without query string.
10
+ *
11
+ * @pure true
12
+ * @invariant ∀ id: normalizeModuleId(id) does not contain '?'
13
+ * @complexity O(n) time / O(1) space where n = |id|
14
+ */
15
+ export declare const normalizeModuleId: (id: string) => string;
16
+ export declare const componentPathAttributeName = "data-path";
2
17
  /**
3
18
  * Checks whether the Vite id represents a JSX or TSX module.
4
19
  *
@@ -1,15 +1,43 @@
1
1
  const jsxFilePattern = /\.(tsx|jsx)(\?.*)?$/u;
2
- // CHANGE: define canonical attribute name for component path tagging.
3
- // WHY: reduce metadata to a single attribute while keeping full source location.
4
- // 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"
5
- // REF: user-2026-01-14-frontend-consumer
2
+ /**
3
+ * Normalizes a module ID by stripping query parameters.
4
+ *
5
+ * Vite and other bundlers may append query parameters to module IDs
6
+ * (e.g., "src/App.tsx?import" or "src/App.tsx?v=123"). This function
7
+ * returns the clean file path without query string.
8
+ *
9
+ * @param id - Module ID (may include query parameters).
10
+ * @returns Clean path without query string.
11
+ *
12
+ * @pure true
13
+ * @invariant ∀ id: normalizeModuleId(id) does not contain '?'
14
+ * @complexity O(n) time / O(1) space where n = |id|
15
+ */
16
+ // CHANGE: centralize query stripping as a pure function in core.
17
+ // WHY: unify module ID normalization in one place as requested in issue #18.
18
+ // QUOTE(ТЗ): "Вынести stripQuery() (или normalizeModuleId()) в core, использовать в Vite и (при желании) в isJsxFile."
19
+ // REF: REQ-18 (issue #18)
6
20
  // SOURCE: n/a
7
- // FORMAT THEOREM: forall a in AttributeName: a = "path"
21
+ // FORMAT THEOREM: id: normalizeModuleId(id) = id.split('?')[0]
22
+ // PURITY: CORE
23
+ // EFFECT: n/a
24
+ // INVARIANT: result contains no query string
25
+ // COMPLEXITY: O(n)/O(1)
26
+ export const normalizeModuleId = (id) => {
27
+ const queryIndex = id.indexOf("?");
28
+ return queryIndex === -1 ? id : id.slice(0, queryIndex);
29
+ };
30
+ // CHANGE: rename attribute from "path" to "data-path" for HTML5 compliance.
31
+ // WHY: data-* attributes are standard HTML5 custom data attributes, improving compatibility.
32
+ // QUOTE(issue-14): "Rename attribute path → data-path (breaking change)"
33
+ // REF: issue-14
34
+ // SOURCE: https://html.spec.whatwg.org/multipage/dom.html#custom-data-attribute
35
+ // FORMAT THEOREM: forall a in AttributeName: a = "data-path"
8
36
  // PURITY: CORE
9
37
  // EFFECT: n/a
10
38
  // INVARIANT: attribute name remains stable across transforms
11
39
  // COMPLEXITY: O(1)/O(1)
12
- export const componentPathAttributeName = "path";
40
+ export const componentPathAttributeName = "data-path";
13
41
  /**
14
42
  * Checks whether the Vite id represents a JSX or TSX module.
15
43
  *
@@ -22,7 +50,7 @@ export const componentPathAttributeName = "path";
22
50
  */
23
51
  // CHANGE: centralize JSX file detection as a pure predicate.
24
52
  // WHY: keep file filtering in the functional core for testability.
25
- // 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"
53
+ // QUOTE(TZ): "Сам компонент должен быть в текущем app но вот что бы его протестировать надо создать ещё один проект который наш текущий апп будет подключать"
26
54
  // REF: user-2026-01-14-frontend-consumer
27
55
  // SOURCE: n/a
28
56
  // FORMAT THEOREM: forall id in ModuleId: isJsxFile(id) -> matches(id, jsxFilePattern)
@@ -45,7 +73,7 @@ export const isJsxFile = (id) => jsxFilePattern.test(id);
45
73
  */
46
74
  // CHANGE: provide a pure formatter for component location payloads.
47
75
  // WHY: reuse a single, deterministic encoding for UI metadata.
48
- // 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"
76
+ // QUOTE(TZ): "Сам компонент должен быть в текущем app но вот что бы его протестировать надо создать ещё один проект который наш текущий апп будет подключать"
49
77
  // REF: user-2026-01-14-frontend-consumer
50
78
  // SOURCE: n/a
51
79
  // FORMAT THEOREM: forall p,l,c: formatComponentPathValue(p,l,c) = concat(p, ":", l, ":", c)
@@ -9,6 +9,10 @@ export type JsxTaggerContext = {
9
9
  * Relative file path from the project root.
10
10
  */
11
11
  readonly relativeFilename: string;
12
+ /**
13
+ * Name of the attribute to add (defaults to "data-path").
14
+ */
15
+ readonly attributeName: string;
12
16
  };
13
17
  /**
14
18
  * Checks if a JSX attribute with the given name already exists on the element.
@@ -25,6 +29,7 @@ export declare const attrExists: (node: t.JSXOpeningElement, attrName: string, t
25
29
  /**
26
30
  * Creates a JSX attribute with the component path value.
27
31
  *
32
+ * @param attributeName - Name of the attribute to create.
28
33
  * @param relativeFilename - Relative path to the file.
29
34
  * @param line - 1-based line number.
30
35
  * @param column - 0-based column number.
@@ -32,10 +37,10 @@ export declare const attrExists: (node: t.JSXOpeningElement, attrName: string, t
32
37
  * @returns JSX attribute node with the path value.
33
38
  *
34
39
  * @pure true
35
- * @invariant attribute name is always componentPathAttributeName
40
+ * @invariant attribute name matches the provided attributeName parameter
36
41
  * @complexity O(1)
37
42
  */
38
- export declare const createPathAttribute: (relativeFilename: string, line: number, column: number, types: typeof t) => t.JSXAttribute;
43
+ export declare const createPathAttribute: (attributeName: string, relativeFilename: string, line: number, column: number, types: typeof t) => t.JSXAttribute;
39
44
  /**
40
45
  * Processes a single JSX opening element and adds path attribute if needed.
41
46
  *
@@ -43,12 +48,12 @@ export declare const createPathAttribute: (relativeFilename: string, line: numbe
43
48
  * Both the Vite plugin and standalone Babel plugin use this function.
44
49
  *
45
50
  * @param node - JSX opening element to process.
46
- * @param context - Tagging context with relative filename.
51
+ * @param context - Tagging context with relative filename and attribute name.
47
52
  * @param types - Babel types module.
48
53
  * @returns true if attribute was added, false if skipped.
49
54
  *
50
55
  * @pure false (mutates node)
51
- * @invariant each JSX element has at most one path attribute after processing
56
+ * @invariant each JSX element has at most one instance of the specified attribute after processing
52
57
  * @complexity O(n) where n = number of existing attributes
53
58
  */
54
59
  export declare const processJsxElement: (node: t.JSXOpeningElement, context: JsxTaggerContext, types: typeof t) => boolean;
@@ -1,4 +1,4 @@
1
- import { componentPathAttributeName, formatComponentPathValue } from "./component-path.js";
1
+ import { formatComponentPathValue } from "./component-path.js";
2
2
  /**
3
3
  * Checks if a JSX attribute with the given name already exists on the element.
4
4
  *
@@ -22,6 +22,7 @@ export const attrExists = (node, attrName, types) => node.attributes.some((attr)
22
22
  /**
23
23
  * Creates a JSX attribute with the component path value.
24
24
  *
25
+ * @param attributeName - Name of the attribute to create.
25
26
  * @param relativeFilename - Relative path to the file.
26
27
  * @param line - 1-based line number.
27
28
  * @param column - 0-based column number.
@@ -29,20 +30,20 @@ export const attrExists = (node, attrName, types) => node.attributes.some((attr)
29
30
  * @returns JSX attribute node with the path value.
30
31
  *
31
32
  * @pure true
32
- * @invariant attribute name is always componentPathAttributeName
33
+ * @invariant attribute name matches the provided attributeName parameter
33
34
  * @complexity O(1)
34
35
  */
35
- // CHANGE: extract attribute creation as a pure factory.
36
- // WHY: single point for attribute creation ensures consistency.
37
- // REF: issue-12 (unified interface request)
38
- // FORMAT THEOREM: ∀ f, l, c: createPathAttribute(f, l, c) = JSXAttribute(path, f:l:c)
36
+ // CHANGE: add attributeName parameter for configurable attribute names.
37
+ // WHY: support customizable attribute names while maintaining default "data-path".
38
+ // REF: issue-14 (add attributeName option)
39
+ // FORMAT THEOREM: ∀ n, f, l, c: createPathAttribute(n, f, l, c) = JSXAttribute(n, f:l:c)
39
40
  // PURITY: CORE
40
41
  // EFFECT: n/a
41
- // INVARIANT: output format is always path:line:column
42
+ // INVARIANT: output format is always path:line:column with configurable attribute name
42
43
  // COMPLEXITY: O(1)/O(1)
43
- export const createPathAttribute = (relativeFilename, line, column, types) => {
44
+ export const createPathAttribute = (attributeName, relativeFilename, line, column, types) => {
44
45
  const value = formatComponentPathValue(relativeFilename, line, column);
45
- return types.jsxAttribute(types.jsxIdentifier(componentPathAttributeName), types.stringLiteral(value));
46
+ return types.jsxAttribute(types.jsxIdentifier(attributeName), types.stringLiteral(value));
46
47
  };
47
48
  /**
48
49
  * Processes a single JSX opening element and adds path attribute if needed.
@@ -51,12 +52,12 @@ export const createPathAttribute = (relativeFilename, line, column, types) => {
51
52
  * Both the Vite plugin and standalone Babel plugin use this function.
52
53
  *
53
54
  * @param node - JSX opening element to process.
54
- * @param context - Tagging context with relative filename.
55
+ * @param context - Tagging context with relative filename and attribute name.
55
56
  * @param types - Babel types module.
56
57
  * @returns true if attribute was added, false if skipped.
57
58
  *
58
59
  * @pure false (mutates node)
59
- * @invariant each JSX element has at most one path attribute after processing
60
+ * @invariant each JSX element has at most one instance of the specified attribute after processing
60
61
  * @complexity O(n) where n = number of existing attributes
61
62
  */
62
63
  // CHANGE: extract unified JSX element processing logic.
@@ -73,12 +74,12 @@ export const processJsxElement = (node, context, types) => {
73
74
  if (node.loc === null || node.loc === undefined) {
74
75
  return false;
75
76
  }
76
- // Skip if already has path attribute (idempotency)
77
- if (attrExists(node, componentPathAttributeName, types)) {
77
+ // Skip if already has the specified attribute (idempotency)
78
+ if (attrExists(node, context.attributeName, types)) {
78
79
  return false;
79
80
  }
80
81
  const { column, line } = node.loc.start;
81
- const attr = createPathAttribute(context.relativeFilename, line, column, types);
82
+ const attr = createPathAttribute(context.attributeName, context.relativeFilename, line, column, types);
82
83
  node.attributes.push(attr);
83
84
  return true;
84
85
  };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { componentPathAttributeName, formatComponentPathValue, isJsxFile } from "./core/component-path.js";
1
+ export { componentPathAttributeName, formatComponentPathValue, isJsxFile, normalizeModuleId } from "./core/component-path.js";
2
2
  export { attrExists, createJsxTaggerVisitor, createPathAttribute, type JsxTaggerContext, processJsxElement } from "./core/jsx-tagger.js";
3
3
  export { componentTaggerBabelPlugin, type ComponentTaggerBabelPluginOptions } from "./shell/babel-plugin.js";
4
- export { componentTagger } from "./shell/component-tagger.js";
4
+ export { componentTagger, type ComponentTaggerOptions } from "./shell/component-tagger.js";
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  // EFFECT: n/a
9
9
  // INVARIANT: exports remain stable for consumers
10
10
  // COMPLEXITY: O(1)/O(1)
11
- export { componentPathAttributeName, formatComponentPathValue, isJsxFile } from "./core/component-path.js";
11
+ export { componentPathAttributeName, formatComponentPathValue, isJsxFile, normalizeModuleId } from "./core/component-path.js";
12
12
  export { attrExists, createJsxTaggerVisitor, createPathAttribute, processJsxElement } from "./core/jsx-tagger.js";
13
13
  export { componentTaggerBabelPlugin } from "./shell/babel-plugin.js";
14
14
  export { componentTagger } from "./shell/component-tagger.js";
@@ -8,6 +8,11 @@ export type ComponentTaggerBabelPluginOptions = {
8
8
  * Defaults to process.cwd().
9
9
  */
10
10
  readonly rootDir?: string;
11
+ /**
12
+ * Name of the attribute to add to JSX elements.
13
+ * Defaults to "data-path".
14
+ */
15
+ readonly attributeName?: string;
11
16
  };
12
17
  type BabelState = {
13
18
  readonly filename?: string;
@@ -1,5 +1,5 @@
1
1
  import { types as t } from "@babel/core";
2
- import { isJsxFile } from "../core/component-path.js";
2
+ import { componentPathAttributeName, isJsxFile } from "../core/component-path.js";
3
3
  import { createJsxTaggerVisitor } from "../core/jsx-tagger.js";
4
4
  import { computeRelativePath } from "../core/path-service.js";
5
5
  /**
@@ -12,14 +12,14 @@ import { computeRelativePath } from "../core/path-service.js";
12
12
  * @invariant returns null when filename is undefined or not a JSX file
13
13
  * @complexity O(n) where n = path length
14
14
  */
15
- // CHANGE: extract context creation for standalone Babel plugin.
16
- // WHY: enable unified visitor to work with Babel state.
17
- // QUOTE(TZ): "А ты можешь сделать что бы бизнес логика оставалось одной?"
18
- // REF: issue-12-comment (unified interface request)
15
+ // CHANGE: add support for configurable attributeName from options.
16
+ // WHY: enable unified visitor to work with Babel state and custom attribute names.
17
+ // QUOTE(issue-14): "Add option attributeName (default: data-path) for both plugins"
18
+ // REF: issue-14
19
19
  // FORMAT THEOREM: ∀ state: getContext(state) = context ↔ isValidState(state)
20
20
  // PURITY: CORE
21
21
  // EFFECT: n/a
22
- // INVARIANT: context contains valid relative path
22
+ // INVARIANT: context contains valid relative path and attribute name
23
23
  // COMPLEXITY: O(n)/O(1)
24
24
  const getContextFromState = (state) => {
25
25
  const filename = state.filename;
@@ -32,9 +32,10 @@ const getContextFromState = (state) => {
32
32
  return null;
33
33
  }
34
34
  // Compute relative path from root using Effect's Path service
35
- const rootDir = state.opts?.rootDir ?? state.cwd ?? "";
35
+ const rootDir = state.opts?.rootDir ?? state.cwd ?? process.cwd();
36
36
  const relativeFilename = computeRelativePath(rootDir, filename);
37
- return { relativeFilename };
37
+ const attributeName = state.opts?.attributeName ?? componentPathAttributeName;
38
+ return { relativeFilename, attributeName };
38
39
  };
39
40
  /**
40
41
  * Creates a Babel plugin that injects component path attributes into JSX elements.
@@ -1,7 +1,18 @@
1
1
  import type { PluginOption } from "vite";
2
+ /**
3
+ * Options for the component tagger Vite plugin.
4
+ */
5
+ export type ComponentTaggerOptions = {
6
+ /**
7
+ * Name of the attribute to add to JSX elements.
8
+ * Defaults to "data-path".
9
+ */
10
+ readonly attributeName?: string;
11
+ };
2
12
  /**
3
13
  * Creates a Vite plugin that injects a single component-path data attribute.
4
14
  *
15
+ * @param options - Configuration options for the plugin.
5
16
  * @returns Vite PluginOption for pre-transform tagging.
6
17
  *
7
18
  * @pure false
@@ -10,4 +21,4 @@ import type { PluginOption } from "vite";
10
21
  * @complexity O(n) time / O(1) space per JSX module
11
22
  * @throws Never - errors are typed and surfaced by Effect
12
23
  */
13
- export declare const componentTagger: () => PluginOption;
24
+ export declare const componentTagger: (options?: ComponentTaggerOptions) => PluginOption;
@@ -1,6 +1,6 @@
1
1
  import { transformAsync, types as t } from "@babel/core";
2
2
  import { Effect, pipe } from "effect";
3
- import { isJsxFile } from "../core/component-path.js";
3
+ import { componentPathAttributeName, isJsxFile, normalizeModuleId } from "../core/component-path.js";
4
4
  import { createJsxTaggerVisitor } from "../core/jsx-tagger.js";
5
5
  import { NodePathLayer, relativeFromRoot } from "../core/path-service.js";
6
6
  class ComponentTaggerError extends Error {
@@ -11,10 +11,6 @@ class ComponentTaggerError extends Error {
11
11
  this.cause = cause;
12
12
  }
13
13
  }
14
- const stripQuery = (id) => {
15
- const queryIndex = id.indexOf("?");
16
- return queryIndex === -1 ? id : id.slice(0, queryIndex);
17
- };
18
14
  const toViteResult = (result) => {
19
15
  if (result === null || result.code === null || result.code === undefined) {
20
16
  return null;
@@ -25,8 +21,8 @@ const toViteResult = (result) => {
25
21
  map: result.map ?? null
26
22
  };
27
23
  };
28
- const makeBabelTagger = (relativeFilename) => {
29
- const context = { relativeFilename };
24
+ const makeBabelTagger = (relativeFilename, attributeName) => {
25
+ const context = { relativeFilename, attributeName };
30
26
  return {
31
27
  name: "component-path-babel-tagger",
32
28
  visitor: createJsxTaggerVisitor(() => context, t)
@@ -54,8 +50,8 @@ const makeBabelTagger = (relativeFilename) => {
54
50
  // EFFECT: Effect<ViteTransformResult | null, ComponentTaggerError, never>
55
51
  // INVARIANT: errors are surfaced as ComponentTaggerError only
56
52
  // COMPLEXITY: O(n)/O(1)
57
- const runTransform = (code, id, rootDir) => {
58
- const cleanId = stripQuery(id);
53
+ const runTransform = (code, id, rootDir, attributeName) => {
54
+ const cleanId = normalizeModuleId(id);
59
55
  return pipe(relativeFromRoot(rootDir, cleanId), Effect.flatMap((relative) => Effect.tryPromise({
60
56
  try: () => transformAsync(code, {
61
57
  filename: cleanId,
@@ -65,7 +61,7 @@ const runTransform = (code, id, rootDir) => {
65
61
  sourceType: "module",
66
62
  plugins: ["typescript", "jsx", "decorators-legacy"]
67
63
  },
68
- plugins: [makeBabelTagger(relative)],
64
+ plugins: [makeBabelTagger(relative, attributeName)],
69
65
  sourceMaps: true
70
66
  }),
71
67
  catch: (cause) => {
@@ -77,6 +73,7 @@ const runTransform = (code, id, rootDir) => {
77
73
  /**
78
74
  * Creates a Vite plugin that injects a single component-path data attribute.
79
75
  *
76
+ * @param options - Configuration options for the plugin.
80
77
  * @returns Vite PluginOption for pre-transform tagging.
81
78
  *
82
79
  * @pure false
@@ -85,17 +82,18 @@ const runTransform = (code, id, rootDir) => {
85
82
  * @complexity O(n) time / O(1) space per JSX module
86
83
  * @throws Never - errors are typed and surfaced by Effect
87
84
  */
88
- // CHANGE: expose a Vite plugin that tags JSX with only path.
89
- // WHY: reduce attribute noise while keeping full path metadata.
90
- // QUOTE(TZ): "Сам компонент должен быть в текущем app но вот что бы его протестировать надо создать ещё один проект который наш текущий апп будет подключать"
91
- // REF: user-2026-01-14-frontend-consumer
85
+ // CHANGE: add attributeName option with default "data-path".
86
+ // WHY: support customizable attribute names while maintaining backwards compatibility.
87
+ // QUOTE(issue-14): "Add option attributeName (default: data-path) for both plugins"
88
+ // REF: issue-14
92
89
  // SOURCE: n/a
93
- // FORMAT THEOREM: forall id: isJsxFile(id) -> transform(id) adds component-path
90
+ // FORMAT THEOREM: forall id: isJsxFile(id) -> transform(id) adds specified attribute
94
91
  // PURITY: SHELL
95
92
  // EFFECT: Effect<ViteTransformResult | null, ComponentTaggerError, never>
96
- // INVARIANT: no duplicate path attributes
93
+ // INVARIANT: no duplicate attributes with the same name
97
94
  // COMPLEXITY: O(n)/O(1)
98
- export const componentTagger = () => {
95
+ export const componentTagger = (options) => {
96
+ const attributeName = options?.attributeName ?? componentPathAttributeName;
99
97
  let resolvedRoot = process.cwd();
100
98
  return {
101
99
  name: "component-path-tagger",
@@ -108,7 +106,7 @@ export const componentTagger = () => {
108
106
  if (!isJsxFile(id)) {
109
107
  return null;
110
108
  }
111
- return Effect.runPromise(pipe(runTransform(code, id, resolvedRoot), Effect.provide(NodePathLayer)));
109
+ return Effect.runPromise(pipe(runTransform(code, id, resolvedRoot, attributeName), Effect.provide(NodePathLayer)));
112
110
  }
113
111
  };
114
112
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prover-coder-ai/component-tagger",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "Component tagger Vite plugin and Babel plugin for JSX metadata",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",