@prover-coder-ai/component-tagger 1.0.22 → 1.0.25

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 ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * CommonJS entry point for the component-tagger Babel plugin.
3
+ *
4
+ * This file provides a CommonJS-compatible wrapper for the Babel plugin,
5
+ * allowing it to be used with Next.js and other tools that require CJS modules.
6
+ *
7
+ * @example
8
+ * // .babelrc
9
+ * {
10
+ * "presets": ["next/babel"],
11
+ * "plugins": ["@prover-coder-ai/component-tagger/babel"]
12
+ * }
13
+ */
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
+ // FORMAT THEOREM: forall require: require(babel.cjs) -> PluginFactory
18
+ // PURITY: SHELL
19
+ // EFFECT: n/a
20
+ // INVARIANT: exports match Babel plugin signature
21
+ // COMPLEXITY: O(1)/O(1)
22
+
23
+ const path = require("node:path")
24
+
25
+ const componentPathAttributeName = "data-path"
26
+ const jsxFilePattern = /\.(tsx|jsx)(\?.*)?$/u
27
+
28
+ const isJsxFile = (id) => jsxFilePattern.test(id)
29
+
30
+ const formatComponentPathValue = (relativeFilename, line, column) =>
31
+ `${relativeFilename}:${line}:${column}`
32
+
33
+ const attrExists = (node, attrName, t) =>
34
+ node.attributes.some(
35
+ (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: attrName })
36
+ )
37
+
38
+ module.exports = function componentTaggerBabelPlugin({ types: t }) {
39
+ return {
40
+ name: "component-path-babel-tagger",
41
+ visitor: {
42
+ JSXOpeningElement(nodePath, state) {
43
+ const { node } = nodePath
44
+ const filename = state.filename
45
+
46
+ // Skip if no filename
47
+ if (filename === undefined) {
48
+ return
49
+ }
50
+
51
+ // Skip if no location info
52
+ if (node.loc === null || node.loc === undefined) {
53
+ return
54
+ }
55
+
56
+ // Skip if not a JSX/TSX file
57
+ if (!isJsxFile(filename)) {
58
+ return
59
+ }
60
+
61
+ // Compute relative path from root and get attribute name
62
+ const opts = state.opts || {}
63
+ const rootDir = opts.rootDir || state.cwd || process.cwd()
64
+ const attributeName = opts.attributeName || componentPathAttributeName
65
+ const relativeFilename = path.relative(rootDir, filename)
66
+
67
+ // Skip if already has the specified attribute (idempotency)
68
+ if (attrExists(node, attributeName, t)) {
69
+ return
70
+ }
71
+
72
+ const { column, line } = node.loc.start
73
+ const value = formatComponentPathValue(relativeFilename, line, column)
74
+
75
+ node.attributes.push(
76
+ t.jsxAttribute(t.jsxIdentifier(attributeName), t.stringLiteral(value))
77
+ )
78
+ }
79
+ }
80
+ }
81
+ }
@@ -1,4 +1,4 @@
1
- export declare const componentPathAttributeName = "path";
1
+ export declare const componentPathAttributeName = "data-path";
2
2
  /**
3
3
  * Checks whether the Vite id represents a JSX or TSX module.
4
4
  *
@@ -1,15 +1,15 @@
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
6
- // SOURCE: n/a
7
- // FORMAT THEOREM: forall a in AttributeName: a = "path"
2
+ // CHANGE: rename attribute from "path" to "data-path" for HTML5 compliance.
3
+ // WHY: data-* attributes are standard HTML5 custom data attributes, improving compatibility.
4
+ // QUOTE(issue-14): "Rename attribute path data-path (breaking change)"
5
+ // REF: issue-14
6
+ // SOURCE: https://html.spec.whatwg.org/multipage/dom.html#custom-data-attribute
7
+ // FORMAT THEOREM: forall a in AttributeName: a = "data-path"
8
8
  // PURITY: CORE
9
9
  // EFFECT: n/a
10
10
  // INVARIANT: attribute name remains stable across transforms
11
11
  // COMPLEXITY: O(1)/O(1)
12
- export const componentPathAttributeName = "path";
12
+ export const componentPathAttributeName = "data-path";
13
13
  /**
14
14
  * Checks whether the Vite id represents a JSX or TSX module.
15
15
  *
@@ -0,0 +1,75 @@
1
+ import type { types as t, Visitor } from "@babel/core";
2
+ /**
3
+ * Context required for JSX tagging.
4
+ *
5
+ * @pure true
6
+ */
7
+ export type JsxTaggerContext = {
8
+ /**
9
+ * Relative file path from the project root.
10
+ */
11
+ readonly relativeFilename: string;
12
+ /**
13
+ * Name of the attribute to add (defaults to "data-path").
14
+ */
15
+ readonly attributeName: string;
16
+ };
17
+ /**
18
+ * Checks if a JSX attribute with the given name already exists on the element.
19
+ *
20
+ * @param node - JSX opening element to check.
21
+ * @param attrName - Name of the attribute to look for.
22
+ * @returns true if attribute exists, false otherwise.
23
+ *
24
+ * @pure true
25
+ * @invariant returns true iff attribute with exact name exists
26
+ * @complexity O(n) where n = number of attributes
27
+ */
28
+ export declare const attrExists: (node: t.JSXOpeningElement, attrName: string, types: typeof t) => boolean;
29
+ /**
30
+ * Creates a JSX attribute with the component path value.
31
+ *
32
+ * @param attributeName - Name of the attribute to create.
33
+ * @param relativeFilename - Relative path to the file.
34
+ * @param line - 1-based line number.
35
+ * @param column - 0-based column number.
36
+ * @param types - Babel types module.
37
+ * @returns JSX attribute node with the path value.
38
+ *
39
+ * @pure true
40
+ * @invariant attribute name matches the provided attributeName parameter
41
+ * @complexity O(1)
42
+ */
43
+ export declare const createPathAttribute: (attributeName: string, relativeFilename: string, line: number, column: number, types: typeof t) => t.JSXAttribute;
44
+ /**
45
+ * Processes a single JSX opening element and adds path attribute if needed.
46
+ *
47
+ * This is the unified business logic for tagging JSX elements with source location.
48
+ * Both the Vite plugin and standalone Babel plugin use this function.
49
+ *
50
+ * @param node - JSX opening element to process.
51
+ * @param context - Tagging context with relative filename and attribute name.
52
+ * @param types - Babel types module.
53
+ * @returns true if attribute was added, false if skipped.
54
+ *
55
+ * @pure false (mutates node)
56
+ * @invariant each JSX element has at most one instance of the specified attribute after processing
57
+ * @complexity O(n) where n = number of existing attributes
58
+ */
59
+ export declare const processJsxElement: (node: t.JSXOpeningElement, context: JsxTaggerContext, types: typeof t) => boolean;
60
+ /**
61
+ * Creates a Babel visitor for JSX elements that uses the unified tagging logic.
62
+ *
63
+ * This is the shared visitor factory used by both:
64
+ * - Vite plugin (componentTagger) - passes relative filename directly
65
+ * - Standalone Babel plugin - computes relative filename from state
66
+ *
67
+ * @param getContext - Function to extract context from Babel state.
68
+ * @param types - Babel types module.
69
+ * @returns Babel visitor object for JSXOpeningElement.
70
+ *
71
+ * @pure true (returns immutable visitor object)
72
+ * @invariant visitor applies processJsxElement to all JSX opening elements
73
+ * @complexity O(1) for visitor creation
74
+ */
75
+ export declare const createJsxTaggerVisitor: <TState>(getContext: (state: TState) => JsxTaggerContext | null, types: typeof t) => Visitor<TState>;
@@ -0,0 +1,118 @@
1
+ import { formatComponentPathValue } from "./component-path.js";
2
+ /**
3
+ * Checks if a JSX attribute with the given name already exists on the element.
4
+ *
5
+ * @param node - JSX opening element to check.
6
+ * @param attrName - Name of the attribute to look for.
7
+ * @returns true if attribute exists, false otherwise.
8
+ *
9
+ * @pure true
10
+ * @invariant returns true iff attribute with exact name exists
11
+ * @complexity O(n) where n = number of attributes
12
+ */
13
+ // CHANGE: extract attribute existence check as a pure utility.
14
+ // WHY: enable reuse across Vite and Babel plugin implementations.
15
+ // REF: issue-12 (unified interface request)
16
+ // FORMAT THEOREM: ∀ node, name: attrExists(node, name) ↔ ∃ attr ∈ node.attributes: attr.name = name
17
+ // PURITY: CORE
18
+ // EFFECT: n/a
19
+ // INVARIANT: predicate is deterministic for fixed inputs
20
+ // COMPLEXITY: O(n)/O(1)
21
+ export const attrExists = (node, attrName, types) => node.attributes.some((attr) => types.isJSXAttribute(attr) && types.isJSXIdentifier(attr.name, { name: attrName }));
22
+ /**
23
+ * Creates a JSX attribute with the component path value.
24
+ *
25
+ * @param attributeName - Name of the attribute to create.
26
+ * @param relativeFilename - Relative path to the file.
27
+ * @param line - 1-based line number.
28
+ * @param column - 0-based column number.
29
+ * @param types - Babel types module.
30
+ * @returns JSX attribute node with the path value.
31
+ *
32
+ * @pure true
33
+ * @invariant attribute name matches the provided attributeName parameter
34
+ * @complexity O(1)
35
+ */
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)
40
+ // PURITY: CORE
41
+ // EFFECT: n/a
42
+ // INVARIANT: output format is always path:line:column with configurable attribute name
43
+ // COMPLEXITY: O(1)/O(1)
44
+ export const createPathAttribute = (attributeName, relativeFilename, line, column, types) => {
45
+ const value = formatComponentPathValue(relativeFilename, line, column);
46
+ return types.jsxAttribute(types.jsxIdentifier(attributeName), types.stringLiteral(value));
47
+ };
48
+ /**
49
+ * Processes a single JSX opening element and adds path attribute if needed.
50
+ *
51
+ * This is the unified business logic for tagging JSX elements with source location.
52
+ * Both the Vite plugin and standalone Babel plugin use this function.
53
+ *
54
+ * @param node - JSX opening element to process.
55
+ * @param context - Tagging context with relative filename and attribute name.
56
+ * @param types - Babel types module.
57
+ * @returns true if attribute was added, false if skipped.
58
+ *
59
+ * @pure false (mutates node)
60
+ * @invariant each JSX element has at most one instance of the specified attribute after processing
61
+ * @complexity O(n) where n = number of existing attributes
62
+ */
63
+ // CHANGE: extract unified JSX element processing logic.
64
+ // WHY: satisfy user request for single business logic shared by Vite and Babel.
65
+ // QUOTE(TZ): "А ты можешь сделать что бы бизнес логика оставалось одной? Ну типо переиспользуй код с vite версии на babel"
66
+ // REF: issue-12-comment (unified interface request)
67
+ // FORMAT THEOREM: ∀ jsx ∈ JSXOpeningElement: processElement(jsx) → tagged(jsx) ∨ skipped(jsx)
68
+ // PURITY: SHELL (mutates AST)
69
+ // EFFECT: AST mutation
70
+ // INVARIANT: idempotent - processing same element twice produces same result
71
+ // COMPLEXITY: O(n)/O(1)
72
+ export const processJsxElement = (node, context, types) => {
73
+ // Skip if no location info
74
+ if (node.loc === null || node.loc === undefined) {
75
+ return false;
76
+ }
77
+ // Skip if already has the specified attribute (idempotency)
78
+ if (attrExists(node, context.attributeName, types)) {
79
+ return false;
80
+ }
81
+ const { column, line } = node.loc.start;
82
+ const attr = createPathAttribute(context.attributeName, context.relativeFilename, line, column, types);
83
+ node.attributes.push(attr);
84
+ return true;
85
+ };
86
+ /**
87
+ * Creates a Babel visitor for JSX elements that uses the unified tagging logic.
88
+ *
89
+ * This is the shared visitor factory used by both:
90
+ * - Vite plugin (componentTagger) - passes relative filename directly
91
+ * - Standalone Babel plugin - computes relative filename from state
92
+ *
93
+ * @param getContext - Function to extract context from Babel state.
94
+ * @param types - Babel types module.
95
+ * @returns Babel visitor object for JSXOpeningElement.
96
+ *
97
+ * @pure true (returns immutable visitor object)
98
+ * @invariant visitor applies processJsxElement to all JSX opening elements
99
+ * @complexity O(1) for visitor creation
100
+ */
101
+ // CHANGE: create shared visitor factory for both plugin types.
102
+ // WHY: single unified interface as requested by user.
103
+ // QUOTE(TZ): "Сделай единный интерфейс для этого"
104
+ // REF: issue-12-comment (unified interface request)
105
+ // FORMAT THEOREM: ∀ visitor = createVisitor(ctx): visitor processes all JSX elements uniformly
106
+ // PURITY: CORE
107
+ // EFFECT: n/a (visitor application has effects)
108
+ // INVARIANT: visitor behavior is consistent across plugin implementations
109
+ // COMPLEXITY: O(1)/O(1)
110
+ export const createJsxTaggerVisitor = (getContext, types) => ({
111
+ JSXOpeningElement(nodePath, state) {
112
+ const context = getContext(state);
113
+ if (context === null) {
114
+ return;
115
+ }
116
+ processJsxElement(nodePath.node, context, types);
117
+ }
118
+ });
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Path service utilities using Effect-TS.
3
+ *
4
+ * PURITY: SHELL (uses Effect for file system operations)
5
+ * PURPOSE: Centralize Effect-based path operations to avoid code duplication.
6
+ */
7
+ import { Path } from "@effect/platform/Path";
8
+ import { Effect } from "effect";
9
+ /**
10
+ * Computes relative path using Effect's Path service.
11
+ *
12
+ * @param rootDir - Root directory for relative path calculation.
13
+ * @param absolutePath - Absolute file path to convert.
14
+ * @returns Effect that produces relative path string.
15
+ *
16
+ * @pure false
17
+ * @effect Path service access
18
+ * @invariant result is a valid relative path
19
+ * @complexity O(n)/O(1) where n = path length
20
+ */
21
+ export declare const relativeFromRoot: (rootDir: string, absolutePath: string) => Effect.Effect<string, never, Path>;
22
+ /**
23
+ * Synchronously computes relative path using Effect's Path service.
24
+ *
25
+ * @param rootDir - Root directory for relative path calculation.
26
+ * @param absolutePath - Absolute file path to convert.
27
+ * @returns Relative path string.
28
+ *
29
+ * @pure false
30
+ * @effect Path service access (synchronous)
31
+ * @invariant result is a valid relative path
32
+ * @complexity O(n)/O(1) where n = path length
33
+ */
34
+ export declare const computeRelativePath: (rootDir: string, absolutePath: string) => string;
35
+ /**
36
+ * Re-export NodePathLayer for plugins that need to provide it explicitly.
37
+ */
38
+ export { layer as NodePathLayer } from "@effect/platform-node/NodePath";
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Path service utilities using Effect-TS.
3
+ *
4
+ * PURITY: SHELL (uses Effect for file system operations)
5
+ * PURPOSE: Centralize Effect-based path operations to avoid code duplication.
6
+ */
7
+ import { layer as NodePathLayer } from "@effect/platform-node/NodePath";
8
+ import { Path } from "@effect/platform/Path";
9
+ import { Effect, pipe } from "effect";
10
+ /**
11
+ * Computes relative path using Effect's Path service.
12
+ *
13
+ * @param rootDir - Root directory for relative path calculation.
14
+ * @param absolutePath - Absolute file path to convert.
15
+ * @returns Effect that produces relative path string.
16
+ *
17
+ * @pure false
18
+ * @effect Path service access
19
+ * @invariant result is a valid relative path
20
+ * @complexity O(n)/O(1) where n = path length
21
+ */
22
+ // CHANGE: extract common path calculation logic from both plugins.
23
+ // WHY: eliminate code duplication detected by vibecode-linter.
24
+ // REF: lint error DUPLICATE #1
25
+ // FORMAT THEOREM: ∀ (root, path): relativePath(root, path) = Path.relative(root, path)
26
+ // PURITY: SHELL
27
+ // EFFECT: Effect<string, never, Path>
28
+ // INVARIANT: always returns a valid relative path for valid inputs
29
+ // COMPLEXITY: O(n)/O(1)
30
+ export const relativeFromRoot = (rootDir, absolutePath) => pipe(Path, Effect.map((pathService) => pathService.relative(rootDir, absolutePath)));
31
+ /**
32
+ * Synchronously computes relative path using Effect's Path service.
33
+ *
34
+ * @param rootDir - Root directory for relative path calculation.
35
+ * @param absolutePath - Absolute file path to convert.
36
+ * @returns Relative path string.
37
+ *
38
+ * @pure false
39
+ * @effect Path service access (synchronous)
40
+ * @invariant result is a valid relative path
41
+ * @complexity O(n)/O(1) where n = path length
42
+ */
43
+ // CHANGE: provide synchronous variant for Babel plugin (which requires sync operations).
44
+ // WHY: Babel plugins must operate synchronously; Effect.runSync bridges Effect-style code.
45
+ // REF: babel-plugin.ts:65-71
46
+ // FORMAT THEOREM: ∀ (root, path): computeRelativePath(root, path) = runSync(relativePath(root, path))
47
+ // PURITY: SHELL
48
+ // EFFECT: Path service (executed synchronously)
49
+ // INVARIANT: always returns a valid string for valid inputs
50
+ // COMPLEXITY: O(n)/O(1)
51
+ export const computeRelativePath = (rootDir, absolutePath) => pipe(relativeFromRoot(rootDir, absolutePath), Effect.provide(NodePathLayer), Effect.runSync);
52
+ /**
53
+ * Re-export NodePathLayer for plugins that need to provide it explicitly.
54
+ */
55
+ export { layer as NodePathLayer } from "@effect/platform-node/NodePath";
package/dist/index.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export { componentPathAttributeName, formatComponentPathValue, isJsxFile } from "./core/component-path.js";
2
- export { componentTagger } from "./shell/component-tagger.js";
2
+ export { attrExists, createJsxTaggerVisitor, createPathAttribute, type JsxTaggerContext, processJsxElement } from "./core/jsx-tagger.js";
3
+ export { componentTaggerBabelPlugin, type ComponentTaggerBabelPluginOptions } from "./shell/babel-plugin.js";
4
+ export { componentTagger, type ComponentTaggerOptions } from "./shell/component-tagger.js";
package/dist/index.js CHANGED
@@ -9,4 +9,6 @@
9
9
  // INVARIANT: exports remain stable for consumers
10
10
  // COMPLEXITY: O(1)/O(1)
11
11
  export { componentPathAttributeName, formatComponentPathValue, isJsxFile } from "./core/component-path.js";
12
+ export { attrExists, createJsxTaggerVisitor, createPathAttribute, processJsxElement } from "./core/jsx-tagger.js";
13
+ export { componentTaggerBabelPlugin } from "./shell/babel-plugin.js";
12
14
  export { componentTagger } from "./shell/component-tagger.js";
@@ -0,0 +1,64 @@
1
+ import { type PluginObj } from "@babel/core";
2
+ /**
3
+ * Options for the component path Babel plugin.
4
+ */
5
+ export type ComponentTaggerBabelPluginOptions = {
6
+ /**
7
+ * Root directory for computing relative paths.
8
+ * Defaults to process.cwd().
9
+ */
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;
16
+ };
17
+ type BabelState = {
18
+ readonly filename?: string;
19
+ readonly cwd?: string;
20
+ readonly opts?: ComponentTaggerBabelPluginOptions;
21
+ };
22
+ /**
23
+ * Creates a Babel plugin that injects component path attributes into JSX elements.
24
+ *
25
+ * This plugin is designed to be used standalone with build tools that support
26
+ * Babel plugins directly (e.g., Next.js via .babelrc).
27
+ *
28
+ * Uses the unified JSX tagger core that is shared with the Vite plugin,
29
+ * ensuring consistent behavior across both build tools.
30
+ *
31
+ * @returns Babel plugin object
32
+ *
33
+ * @pure false
34
+ * @effect Babel AST transformation
35
+ * @invariant JSX elements are tagged with path attribute containing file:line:column
36
+ * @complexity O(n) where n = number of JSX elements
37
+ *
38
+ * @example
39
+ * // .babelrc or babel.config.js
40
+ * {
41
+ * "plugins": ["@prover-coder-ai/component-tagger/babel"]
42
+ * }
43
+ *
44
+ * @example
45
+ * // Next.js .babelrc with options
46
+ * {
47
+ * "presets": ["next/babel"],
48
+ * "env": {
49
+ * "development": {
50
+ * "plugins": [
51
+ * ["@prover-coder-ai/component-tagger/babel", { "rootDir": "/custom/root" }]
52
+ * ]
53
+ * }
54
+ * }
55
+ * }
56
+ */
57
+ export declare const componentTaggerBabelPlugin: () => PluginObj<BabelState>;
58
+ /**
59
+ * Default export for Babel plugin resolution.
60
+ *
61
+ * Babel resolves plugins by looking for a default export function that
62
+ * returns a plugin object when called.
63
+ */
64
+ export default componentTaggerBabelPlugin;
@@ -0,0 +1,103 @@
1
+ import { types as t } from "@babel/core";
2
+ import { componentPathAttributeName, isJsxFile } from "../core/component-path.js";
3
+ import { createJsxTaggerVisitor } from "../core/jsx-tagger.js";
4
+ import { computeRelativePath } from "../core/path-service.js";
5
+ /**
6
+ * Creates context for JSX tagging from Babel state.
7
+ *
8
+ * @param state - Babel plugin state containing filename and options.
9
+ * @returns JsxTaggerContext or null if context cannot be created.
10
+ *
11
+ * @pure true
12
+ * @invariant returns null when filename is undefined or not a JSX file
13
+ * @complexity O(n) where n = path length
14
+ */
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
+ // FORMAT THEOREM: ∀ state: getContext(state) = context ↔ isValidState(state)
20
+ // PURITY: CORE
21
+ // EFFECT: n/a
22
+ // INVARIANT: context contains valid relative path and attribute name
23
+ // COMPLEXITY: O(n)/O(1)
24
+ const getContextFromState = (state) => {
25
+ const filename = state.filename;
26
+ // Skip if no filename
27
+ if (filename === undefined) {
28
+ return null;
29
+ }
30
+ // Skip if not a JSX/TSX file
31
+ if (!isJsxFile(filename)) {
32
+ return null;
33
+ }
34
+ // Compute relative path from root using Effect's Path service
35
+ const rootDir = state.opts?.rootDir ?? state.cwd ?? process.cwd();
36
+ const relativeFilename = computeRelativePath(rootDir, filename);
37
+ const attributeName = state.opts?.attributeName ?? componentPathAttributeName;
38
+ return { relativeFilename, attributeName };
39
+ };
40
+ /**
41
+ * Creates a Babel plugin that injects component path attributes into JSX elements.
42
+ *
43
+ * This plugin is designed to be used standalone with build tools that support
44
+ * Babel plugins directly (e.g., Next.js via .babelrc).
45
+ *
46
+ * Uses the unified JSX tagger core that is shared with the Vite plugin,
47
+ * ensuring consistent behavior across both build tools.
48
+ *
49
+ * @returns Babel plugin object
50
+ *
51
+ * @pure false
52
+ * @effect Babel AST transformation
53
+ * @invariant JSX elements are tagged with path attribute containing file:line:column
54
+ * @complexity O(n) where n = number of JSX elements
55
+ *
56
+ * @example
57
+ * // .babelrc or babel.config.js
58
+ * {
59
+ * "plugins": ["@prover-coder-ai/component-tagger/babel"]
60
+ * }
61
+ *
62
+ * @example
63
+ * // Next.js .babelrc with options
64
+ * {
65
+ * "presets": ["next/babel"],
66
+ * "env": {
67
+ * "development": {
68
+ * "plugins": [
69
+ * ["@prover-coder-ai/component-tagger/babel", { "rootDir": "/custom/root" }]
70
+ * ]
71
+ * }
72
+ * }
73
+ * }
74
+ */
75
+ // CHANGE: use unified JSX tagger visitor from core module.
76
+ // WHY: share business logic between Vite and Babel plugins as requested.
77
+ // QUOTE(TZ): "А ты можешь сделать что бы бизнес логика оставалось одной? Ну типо переиспользуй код с vite версии на babel. Сделай единный интерфейс для этого"
78
+ // REF: issue-12-comment (unified interface request)
79
+ // SOURCE: https://babeljs.io/docs/plugins
80
+ // FORMAT THEOREM: forall jsx in JSXOpeningElement: transform(jsx) -> tagged(jsx, path)
81
+ // PURITY: SHELL
82
+ // EFFECT: Babel AST mutation
83
+ // INVARIANT: each JSX opening element has at most one path attribute
84
+ // COMPLEXITY: O(n)/O(1)
85
+ export const componentTaggerBabelPlugin = () => ({
86
+ name: "component-path-babel-tagger",
87
+ visitor: createJsxTaggerVisitor(getContextFromState, t)
88
+ });
89
+ /**
90
+ * Default export for Babel plugin resolution.
91
+ *
92
+ * Babel resolves plugins by looking for a default export function that
93
+ * returns a plugin object when called.
94
+ */
95
+ // CHANGE: provide default export for standard Babel plugin resolution.
96
+ // WHY: Babel expects plugins to be functions that return plugin objects.
97
+ // REF: issue-12
98
+ // FORMAT THEOREM: forall babel: require(plugin) -> callable -> PluginObj
99
+ // PURITY: SHELL
100
+ // EFFECT: n/a
101
+ // INVARIANT: default export matches Babel plugin signature
102
+ // COMPLEXITY: O(1)/O(1)
103
+ export default componentTaggerBabelPlugin;
@@ -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,8 +1,8 @@
1
1
  import { transformAsync, types as t } from "@babel/core";
2
- import { layer as NodePathLayer } from "@effect/platform-node/NodePath";
3
- import { Path } from "@effect/platform/Path";
4
2
  import { Effect, pipe } from "effect";
5
- import { componentPathAttributeName, formatComponentPathValue, isJsxFile } from "../core/component-path.js";
3
+ import { componentPathAttributeName, isJsxFile } from "../core/component-path.js";
4
+ import { createJsxTaggerVisitor } from "../core/jsx-tagger.js";
5
+ import { NodePathLayer, relativeFromRoot } from "../core/path-service.js";
6
6
  class ComponentTaggerError extends Error {
7
7
  cause;
8
8
  _tag = "ComponentTaggerError";
@@ -15,18 +15,6 @@ const stripQuery = (id) => {
15
15
  const queryIndex = id.indexOf("?");
16
16
  return queryIndex === -1 ? id : id.slice(0, queryIndex);
17
17
  };
18
- // CHANGE: compute relative paths from the resolved Vite root instead of process.cwd().
19
- // WHY: keep component paths stable across monorepos and custom Vite roots.
20
- // QUOTE(TZ): "Сам компонент должен быть в текущем app но вот что бы его протестировать надо создать ещё один проект который наш текущий апп будет подключать"
21
- // REF: user-2026-01-14-frontend-consumer
22
- // SOURCE: n/a
23
- // FORMAT THEOREM: forall p in Path: relative(root, p) = r -> resolve(root, r) = p
24
- // PURITY: SHELL
25
- // EFFECT: Effect<string, never, Path>
26
- // INVARIANT: output is deterministic for a fixed root
27
- // COMPLEXITY: O(n)/O(1)
28
- const relativeFromRoot = (rootDir, absolutePath) => pipe(Path, Effect.map((pathService) => pathService.relative(rootDir, absolutePath)));
29
- const attrExists = (node, attrName) => node.attributes.some((attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: attrName }));
30
18
  const toViteResult = (result) => {
31
19
  if (result === null || result.code === null || result.code === undefined) {
32
20
  return null;
@@ -37,33 +25,13 @@ const toViteResult = (result) => {
37
25
  map: result.map ?? null
38
26
  };
39
27
  };
40
- // CHANGE: inject a single path attribute into JSX opening elements.
41
- // WHY: remove redundant metadata while preserving the full source location payload.
42
- // 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"
43
- // REF: user-2026-01-14-frontend-consumer
44
- // SOURCE: n/a
45
- // FORMAT THEOREM: forall f in JSXOpeningElement: rendered(f) -> annotated(f)
46
- // PURITY: SHELL
47
- // EFFECT: Effect<ViteTransformResult | null, ComponentTaggerError, Path>
48
- // INVARIANT: each JSX opening element has at most one path attribute
49
- // COMPLEXITY: O(n)/O(1), n = number of JSX elements
50
- const makeBabelTagger = (relativeFilename) => ({
51
- name: "component-path-babel-tagger",
52
- visitor: {
53
- JSXOpeningElement(openPath) {
54
- const { node } = openPath;
55
- if (node.loc === null || node.loc === undefined) {
56
- return;
57
- }
58
- if (attrExists(node, componentPathAttributeName)) {
59
- return;
60
- }
61
- const { column, line } = node.loc.start;
62
- const value = formatComponentPathValue(relativeFilename, line, column);
63
- node.attributes.push(t.jsxAttribute(t.jsxIdentifier(componentPathAttributeName), t.stringLiteral(value)));
64
- }
65
- }
66
- });
28
+ const makeBabelTagger = (relativeFilename, attributeName) => {
29
+ const context = { relativeFilename, attributeName };
30
+ return {
31
+ name: "component-path-babel-tagger",
32
+ visitor: createJsxTaggerVisitor(() => context, t)
33
+ };
34
+ };
67
35
  /**
68
36
  * Builds a Vite transform result with a single component-path attribute per JSX element.
69
37
  *
@@ -78,7 +46,7 @@ const makeBabelTagger = (relativeFilename) => ({
78
46
  */
79
47
  // CHANGE: wrap Babel transform in Effect for typed errors and controlled effects.
80
48
  // WHY: satisfy the shell-only effect boundary while avoiding async/await.
81
- // 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"
49
+ // QUOTE(TZ): "Сам компонент должен быть в текущем app но вот что бы его протестировать надо создать ещё один проект который наш текущий апп будет подключать"
82
50
  // REF: user-2026-01-14-frontend-consumer
83
51
  // SOURCE: n/a
84
52
  // FORMAT THEOREM: forall c in Code: transform(c) = r -> r is tagged or null
@@ -86,7 +54,7 @@ const makeBabelTagger = (relativeFilename) => ({
86
54
  // EFFECT: Effect<ViteTransformResult | null, ComponentTaggerError, never>
87
55
  // INVARIANT: errors are surfaced as ComponentTaggerError only
88
56
  // COMPLEXITY: O(n)/O(1)
89
- const runTransform = (code, id, rootDir) => {
57
+ const runTransform = (code, id, rootDir, attributeName) => {
90
58
  const cleanId = stripQuery(id);
91
59
  return pipe(relativeFromRoot(rootDir, cleanId), Effect.flatMap((relative) => Effect.tryPromise({
92
60
  try: () => transformAsync(code, {
@@ -97,7 +65,7 @@ const runTransform = (code, id, rootDir) => {
97
65
  sourceType: "module",
98
66
  plugins: ["typescript", "jsx", "decorators-legacy"]
99
67
  },
100
- plugins: [makeBabelTagger(relative)],
68
+ plugins: [makeBabelTagger(relative, attributeName)],
101
69
  sourceMaps: true
102
70
  }),
103
71
  catch: (cause) => {
@@ -109,6 +77,7 @@ const runTransform = (code, id, rootDir) => {
109
77
  /**
110
78
  * Creates a Vite plugin that injects a single component-path data attribute.
111
79
  *
80
+ * @param options - Configuration options for the plugin.
112
81
  * @returns Vite PluginOption for pre-transform tagging.
113
82
  *
114
83
  * @pure false
@@ -117,17 +86,18 @@ const runTransform = (code, id, rootDir) => {
117
86
  * @complexity O(n) time / O(1) space per JSX module
118
87
  * @throws Never - errors are typed and surfaced by Effect
119
88
  */
120
- // CHANGE: expose a Vite plugin that tags JSX with only path.
121
- // WHY: reduce attribute noise while keeping full path metadata.
122
- // 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"
123
- // REF: user-2026-01-14-frontend-consumer
89
+ // CHANGE: add attributeName option with default "data-path".
90
+ // WHY: support customizable attribute names while maintaining backwards compatibility.
91
+ // QUOTE(issue-14): "Add option attributeName (default: data-path) for both plugins"
92
+ // REF: issue-14
124
93
  // SOURCE: n/a
125
- // FORMAT THEOREM: forall id: isJsxFile(id) -> transform(id) adds component-path
94
+ // FORMAT THEOREM: forall id: isJsxFile(id) -> transform(id) adds specified attribute
126
95
  // PURITY: SHELL
127
96
  // EFFECT: Effect<ViteTransformResult | null, ComponentTaggerError, never>
128
- // INVARIANT: no duplicate path attributes
97
+ // INVARIANT: no duplicate attributes with the same name
129
98
  // COMPLEXITY: O(n)/O(1)
130
- export const componentTagger = () => {
99
+ export const componentTagger = (options) => {
100
+ const attributeName = options?.attributeName ?? componentPathAttributeName;
131
101
  let resolvedRoot = process.cwd();
132
102
  return {
133
103
  name: "component-path-tagger",
@@ -140,7 +110,7 @@ export const componentTagger = () => {
140
110
  if (!isJsxFile(id)) {
141
111
  return null;
142
112
  }
143
- return Effect.runPromise(pipe(runTransform(code, id, resolvedRoot), Effect.provide(NodePathLayer)));
113
+ return Effect.runPromise(pipe(runTransform(code, id, resolvedRoot, attributeName), Effect.provide(NodePathLayer)));
144
114
  }
145
115
  };
146
116
  };
package/package.json CHANGED
@@ -1,11 +1,23 @@
1
1
  {
2
2
  "name": "@prover-coder-ai/component-tagger",
3
- "version": "1.0.22",
4
- "description": "Component tagger Vite plugin for JSX metadata",
3
+ "version": "1.0.25",
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",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./babel": {
13
+ "types": "./dist/shell/babel-plugin.d.ts",
14
+ "require": "./babel.cjs",
15
+ "import": "./dist/shell/babel-plugin.js"
16
+ }
17
+ },
7
18
  "files": [
8
- "dist"
19
+ "dist",
20
+ "babel.cjs"
9
21
  ],
10
22
  "directories": {
11
23
  "doc": "doc"
@@ -17,6 +29,8 @@
17
29
  "keywords": [
18
30
  "effect",
19
31
  "vite",
32
+ "babel",
33
+ "nextjs",
20
34
  "plugin",
21
35
  "tagger"
22
36
  ],