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

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/babel.cjs ADDED
@@ -0,0 +1,80 @@
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.
15
+ // WHY: Babel configuration often requires CommonJS modules.
16
+ // REF: issue-12
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 = "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
+ // Skip if already has path attribute
62
+ if (attrExists(node, componentPathAttributeName, t)) {
63
+ return
64
+ }
65
+
66
+ // Compute relative path from root
67
+ const opts = state.opts || {}
68
+ const rootDir = opts.rootDir || state.cwd || process.cwd()
69
+ const relativeFilename = path.relative(rootDir, filename)
70
+
71
+ const { column, line } = node.loc.start
72
+ const value = formatComponentPathValue(relativeFilename, line, column)
73
+
74
+ node.attributes.push(
75
+ t.jsxAttribute(t.jsxIdentifier(componentPathAttributeName), t.stringLiteral(value))
76
+ )
77
+ }
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,70 @@
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
+ /**
14
+ * Checks if a JSX attribute with the given name already exists on the element.
15
+ *
16
+ * @param node - JSX opening element to check.
17
+ * @param attrName - Name of the attribute to look for.
18
+ * @returns true if attribute exists, false otherwise.
19
+ *
20
+ * @pure true
21
+ * @invariant returns true iff attribute with exact name exists
22
+ * @complexity O(n) where n = number of attributes
23
+ */
24
+ export declare const attrExists: (node: t.JSXOpeningElement, attrName: string, types: typeof t) => boolean;
25
+ /**
26
+ * Creates a JSX attribute with the component path value.
27
+ *
28
+ * @param relativeFilename - Relative path to the file.
29
+ * @param line - 1-based line number.
30
+ * @param column - 0-based column number.
31
+ * @param types - Babel types module.
32
+ * @returns JSX attribute node with the path value.
33
+ *
34
+ * @pure true
35
+ * @invariant attribute name is always componentPathAttributeName
36
+ * @complexity O(1)
37
+ */
38
+ export declare const createPathAttribute: (relativeFilename: string, line: number, column: number, types: typeof t) => t.JSXAttribute;
39
+ /**
40
+ * Processes a single JSX opening element and adds path attribute if needed.
41
+ *
42
+ * This is the unified business logic for tagging JSX elements with source location.
43
+ * Both the Vite plugin and standalone Babel plugin use this function.
44
+ *
45
+ * @param node - JSX opening element to process.
46
+ * @param context - Tagging context with relative filename.
47
+ * @param types - Babel types module.
48
+ * @returns true if attribute was added, false if skipped.
49
+ *
50
+ * @pure false (mutates node)
51
+ * @invariant each JSX element has at most one path attribute after processing
52
+ * @complexity O(n) where n = number of existing attributes
53
+ */
54
+ export declare const processJsxElement: (node: t.JSXOpeningElement, context: JsxTaggerContext, types: typeof t) => boolean;
55
+ /**
56
+ * Creates a Babel visitor for JSX elements that uses the unified tagging logic.
57
+ *
58
+ * This is the shared visitor factory used by both:
59
+ * - Vite plugin (componentTagger) - passes relative filename directly
60
+ * - Standalone Babel plugin - computes relative filename from state
61
+ *
62
+ * @param getContext - Function to extract context from Babel state.
63
+ * @param types - Babel types module.
64
+ * @returns Babel visitor object for JSXOpeningElement.
65
+ *
66
+ * @pure true (returns immutable visitor object)
67
+ * @invariant visitor applies processJsxElement to all JSX opening elements
68
+ * @complexity O(1) for visitor creation
69
+ */
70
+ export declare const createJsxTaggerVisitor: <TState>(getContext: (state: TState) => JsxTaggerContext | null, types: typeof t) => Visitor<TState>;
@@ -0,0 +1,117 @@
1
+ import { componentPathAttributeName, 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 relativeFilename - Relative path to the file.
26
+ * @param line - 1-based line number.
27
+ * @param column - 0-based column number.
28
+ * @param types - Babel types module.
29
+ * @returns JSX attribute node with the path value.
30
+ *
31
+ * @pure true
32
+ * @invariant attribute name is always componentPathAttributeName
33
+ * @complexity O(1)
34
+ */
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)
39
+ // PURITY: CORE
40
+ // EFFECT: n/a
41
+ // INVARIANT: output format is always path:line:column
42
+ // COMPLEXITY: O(1)/O(1)
43
+ export const createPathAttribute = (relativeFilename, line, column, types) => {
44
+ const value = formatComponentPathValue(relativeFilename, line, column);
45
+ return types.jsxAttribute(types.jsxIdentifier(componentPathAttributeName), types.stringLiteral(value));
46
+ };
47
+ /**
48
+ * Processes a single JSX opening element and adds path attribute if needed.
49
+ *
50
+ * This is the unified business logic for tagging JSX elements with source location.
51
+ * Both the Vite plugin and standalone Babel plugin use this function.
52
+ *
53
+ * @param node - JSX opening element to process.
54
+ * @param context - Tagging context with relative filename.
55
+ * @param types - Babel types module.
56
+ * @returns true if attribute was added, false if skipped.
57
+ *
58
+ * @pure false (mutates node)
59
+ * @invariant each JSX element has at most one path attribute after processing
60
+ * @complexity O(n) where n = number of existing attributes
61
+ */
62
+ // CHANGE: extract unified JSX element processing logic.
63
+ // WHY: satisfy user request for single business logic shared by Vite and Babel.
64
+ // QUOTE(TZ): "А ты можешь сделать что бы бизнес логика оставалось одной? Ну типо переиспользуй код с vite версии на babel"
65
+ // REF: issue-12-comment (unified interface request)
66
+ // FORMAT THEOREM: ∀ jsx ∈ JSXOpeningElement: processElement(jsx) → tagged(jsx) ∨ skipped(jsx)
67
+ // PURITY: SHELL (mutates AST)
68
+ // EFFECT: AST mutation
69
+ // INVARIANT: idempotent - processing same element twice produces same result
70
+ // COMPLEXITY: O(n)/O(1)
71
+ export const processJsxElement = (node, context, types) => {
72
+ // Skip if no location info
73
+ if (node.loc === null || node.loc === undefined) {
74
+ return false;
75
+ }
76
+ // Skip if already has path attribute (idempotency)
77
+ if (attrExists(node, componentPathAttributeName, types)) {
78
+ return false;
79
+ }
80
+ const { column, line } = node.loc.start;
81
+ const attr = createPathAttribute(context.relativeFilename, line, column, types);
82
+ node.attributes.push(attr);
83
+ return true;
84
+ };
85
+ /**
86
+ * Creates a Babel visitor for JSX elements that uses the unified tagging logic.
87
+ *
88
+ * This is the shared visitor factory used by both:
89
+ * - Vite plugin (componentTagger) - passes relative filename directly
90
+ * - Standalone Babel plugin - computes relative filename from state
91
+ *
92
+ * @param getContext - Function to extract context from Babel state.
93
+ * @param types - Babel types module.
94
+ * @returns Babel visitor object for JSXOpeningElement.
95
+ *
96
+ * @pure true (returns immutable visitor object)
97
+ * @invariant visitor applies processJsxElement to all JSX opening elements
98
+ * @complexity O(1) for visitor creation
99
+ */
100
+ // CHANGE: create shared visitor factory for both plugin types.
101
+ // WHY: single unified interface as requested by user.
102
+ // QUOTE(TZ): "Сделай единный интерфейс для этого"
103
+ // REF: issue-12-comment (unified interface request)
104
+ // FORMAT THEOREM: ∀ visitor = createVisitor(ctx): visitor processes all JSX elements uniformly
105
+ // PURITY: CORE
106
+ // EFFECT: n/a (visitor application has effects)
107
+ // INVARIANT: visitor behavior is consistent across plugin implementations
108
+ // COMPLEXITY: O(1)/O(1)
109
+ export const createJsxTaggerVisitor = (getContext, types) => ({
110
+ JSXOpeningElement(nodePath, state) {
111
+ const context = getContext(state);
112
+ if (context === null) {
113
+ return;
114
+ }
115
+ processJsxElement(nodePath.node, context, types);
116
+ }
117
+ });
@@ -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 { attrExists, createJsxTaggerVisitor, createPathAttribute, type JsxTaggerContext, processJsxElement } from "./core/jsx-tagger.js";
3
+ export { componentTaggerBabelPlugin, type ComponentTaggerBabelPluginOptions } from "./shell/babel-plugin.js";
2
4
  export { componentTagger } 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,59 @@
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
+ type BabelState = {
13
+ readonly filename?: string;
14
+ readonly cwd?: string;
15
+ readonly opts?: ComponentTaggerBabelPluginOptions;
16
+ };
17
+ /**
18
+ * Creates a Babel plugin that injects component path attributes into JSX elements.
19
+ *
20
+ * This plugin is designed to be used standalone with build tools that support
21
+ * Babel plugins directly (e.g., Next.js via .babelrc).
22
+ *
23
+ * Uses the unified JSX tagger core that is shared with the Vite plugin,
24
+ * ensuring consistent behavior across both build tools.
25
+ *
26
+ * @returns Babel plugin object
27
+ *
28
+ * @pure false
29
+ * @effect Babel AST transformation
30
+ * @invariant JSX elements are tagged with path attribute containing file:line:column
31
+ * @complexity O(n) where n = number of JSX elements
32
+ *
33
+ * @example
34
+ * // .babelrc or babel.config.js
35
+ * {
36
+ * "plugins": ["@prover-coder-ai/component-tagger/babel"]
37
+ * }
38
+ *
39
+ * @example
40
+ * // Next.js .babelrc with options
41
+ * {
42
+ * "presets": ["next/babel"],
43
+ * "env": {
44
+ * "development": {
45
+ * "plugins": [
46
+ * ["@prover-coder-ai/component-tagger/babel", { "rootDir": "/custom/root" }]
47
+ * ]
48
+ * }
49
+ * }
50
+ * }
51
+ */
52
+ export declare const componentTaggerBabelPlugin: () => PluginObj<BabelState>;
53
+ /**
54
+ * Default export for Babel plugin resolution.
55
+ *
56
+ * Babel resolves plugins by looking for a default export function that
57
+ * returns a plugin object when called.
58
+ */
59
+ export default componentTaggerBabelPlugin;
@@ -0,0 +1,102 @@
1
+ import { types as t } from "@babel/core";
2
+ import { 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: 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)
19
+ // FORMAT THEOREM: ∀ state: getContext(state) = context ↔ isValidState(state)
20
+ // PURITY: CORE
21
+ // EFFECT: n/a
22
+ // INVARIANT: context contains valid relative path
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 ?? "";
36
+ const relativeFilename = computeRelativePath(rootDir, filename);
37
+ return { relativeFilename };
38
+ };
39
+ /**
40
+ * Creates a Babel plugin that injects component path attributes into JSX elements.
41
+ *
42
+ * This plugin is designed to be used standalone with build tools that support
43
+ * Babel plugins directly (e.g., Next.js via .babelrc).
44
+ *
45
+ * Uses the unified JSX tagger core that is shared with the Vite plugin,
46
+ * ensuring consistent behavior across both build tools.
47
+ *
48
+ * @returns Babel plugin object
49
+ *
50
+ * @pure false
51
+ * @effect Babel AST transformation
52
+ * @invariant JSX elements are tagged with path attribute containing file:line:column
53
+ * @complexity O(n) where n = number of JSX elements
54
+ *
55
+ * @example
56
+ * // .babelrc or babel.config.js
57
+ * {
58
+ * "plugins": ["@prover-coder-ai/component-tagger/babel"]
59
+ * }
60
+ *
61
+ * @example
62
+ * // Next.js .babelrc with options
63
+ * {
64
+ * "presets": ["next/babel"],
65
+ * "env": {
66
+ * "development": {
67
+ * "plugins": [
68
+ * ["@prover-coder-ai/component-tagger/babel", { "rootDir": "/custom/root" }]
69
+ * ]
70
+ * }
71
+ * }
72
+ * }
73
+ */
74
+ // CHANGE: use unified JSX tagger visitor from core module.
75
+ // WHY: share business logic between Vite and Babel plugins as requested.
76
+ // QUOTE(TZ): "А ты можешь сделать что бы бизнес логика оставалось одной? Ну типо переиспользуй код с vite версии на babel. Сделай единный интерфейс для этого"
77
+ // REF: issue-12-comment (unified interface request)
78
+ // SOURCE: https://babeljs.io/docs/plugins
79
+ // FORMAT THEOREM: forall jsx in JSXOpeningElement: transform(jsx) -> tagged(jsx, path)
80
+ // PURITY: SHELL
81
+ // EFFECT: Babel AST mutation
82
+ // INVARIANT: each JSX opening element has at most one path attribute
83
+ // COMPLEXITY: O(n)/O(1)
84
+ export const componentTaggerBabelPlugin = () => ({
85
+ name: "component-path-babel-tagger",
86
+ visitor: createJsxTaggerVisitor(getContextFromState, t)
87
+ });
88
+ /**
89
+ * Default export for Babel plugin resolution.
90
+ *
91
+ * Babel resolves plugins by looking for a default export function that
92
+ * returns a plugin object when called.
93
+ */
94
+ // CHANGE: provide default export for standard Babel plugin resolution.
95
+ // WHY: Babel expects plugins to be functions that return plugin objects.
96
+ // REF: issue-12
97
+ // FORMAT THEOREM: forall babel: require(plugin) -> callable -> PluginObj
98
+ // PURITY: SHELL
99
+ // EFFECT: n/a
100
+ // INVARIANT: default export matches Babel plugin signature
101
+ // COMPLEXITY: O(1)/O(1)
102
+ export default componentTaggerBabelPlugin;
@@ -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 { 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) => {
29
+ const context = { relativeFilename };
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
@@ -119,7 +87,7 @@ const runTransform = (code, id, rootDir) => {
119
87
  */
120
88
  // CHANGE: expose a Vite plugin that tags JSX with only path.
121
89
  // 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"
90
+ // QUOTE(TZ): "Сам компонент должен быть в текущем app но вот что бы его протестировать надо создать ещё один проект который наш текущий апп будет подключать"
123
91
  // REF: user-2026-01-14-frontend-consumer
124
92
  // SOURCE: n/a
125
93
  // FORMAT THEOREM: forall id: isJsxFile(id) -> transform(id) adds component-path
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.24",
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
  ],