|
| 1 | +import type { types as t, Visitor } from "@babel/core" |
| 2 | + |
| 3 | +import { componentPathAttributeName, formatComponentPathValue } from "./component-path.js" |
| 4 | + |
| 5 | +/** |
| 6 | + * Context required for JSX tagging. |
| 7 | + * |
| 8 | + * @pure true |
| 9 | + */ |
| 10 | +export type JsxTaggerContext = { |
| 11 | + /** |
| 12 | + * Relative file path from the project root. |
| 13 | + */ |
| 14 | + readonly relativeFilename: string |
| 15 | +} |
| 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 | +// CHANGE: extract attribute existence check as a pure utility. |
| 29 | +// WHY: enable reuse across Vite and Babel plugin implementations. |
| 30 | +// REF: issue-12 (unified interface request) |
| 31 | +// FORMAT THEOREM: ∀ node, name: attrExists(node, name) ↔ ∃ attr ∈ node.attributes: attr.name = name |
| 32 | +// PURITY: CORE |
| 33 | +// EFFECT: n/a |
| 34 | +// INVARIANT: predicate is deterministic for fixed inputs |
| 35 | +// COMPLEXITY: O(n)/O(1) |
| 36 | +export const attrExists = (node: t.JSXOpeningElement, attrName: string, types: typeof t): boolean => |
| 37 | + node.attributes.some( |
| 38 | + (attr) => types.isJSXAttribute(attr) && types.isJSXIdentifier(attr.name, { name: attrName }) |
| 39 | + ) |
| 40 | + |
| 41 | +/** |
| 42 | + * Creates a JSX attribute with the component path value. |
| 43 | + * |
| 44 | + * @param relativeFilename - Relative path to the file. |
| 45 | + * @param line - 1-based line number. |
| 46 | + * @param column - 0-based column number. |
| 47 | + * @param types - Babel types module. |
| 48 | + * @returns JSX attribute node with the path value. |
| 49 | + * |
| 50 | + * @pure true |
| 51 | + * @invariant attribute name is always componentPathAttributeName |
| 52 | + * @complexity O(1) |
| 53 | + */ |
| 54 | +// CHANGE: extract attribute creation as a pure factory. |
| 55 | +// WHY: single point for attribute creation ensures consistency. |
| 56 | +// REF: issue-12 (unified interface request) |
| 57 | +// FORMAT THEOREM: ∀ f, l, c: createPathAttribute(f, l, c) = JSXAttribute(path, f:l:c) |
| 58 | +// PURITY: CORE |
| 59 | +// EFFECT: n/a |
| 60 | +// INVARIANT: output format is always path:line:column |
| 61 | +// COMPLEXITY: O(1)/O(1) |
| 62 | +export const createPathAttribute = ( |
| 63 | + relativeFilename: string, |
| 64 | + line: number, |
| 65 | + column: number, |
| 66 | + types: typeof t |
| 67 | +): t.JSXAttribute => { |
| 68 | + const value = formatComponentPathValue(relativeFilename, line, column) |
| 69 | + return types.jsxAttribute(types.jsxIdentifier(componentPathAttributeName), types.stringLiteral(value)) |
| 70 | +} |
| 71 | + |
| 72 | +/** |
| 73 | + * Processes a single JSX opening element and adds path attribute if needed. |
| 74 | + * |
| 75 | + * This is the unified business logic for tagging JSX elements with source location. |
| 76 | + * Both the Vite plugin and standalone Babel plugin use this function. |
| 77 | + * |
| 78 | + * @param node - JSX opening element to process. |
| 79 | + * @param context - Tagging context with relative filename. |
| 80 | + * @param types - Babel types module. |
| 81 | + * @returns true if attribute was added, false if skipped. |
| 82 | + * |
| 83 | + * @pure false (mutates node) |
| 84 | + * @invariant each JSX element has at most one path attribute after processing |
| 85 | + * @complexity O(n) where n = number of existing attributes |
| 86 | + */ |
| 87 | +// CHANGE: extract unified JSX element processing logic. |
| 88 | +// WHY: satisfy user request for single business logic shared by Vite and Babel. |
| 89 | +// QUOTE(TZ): "А ты можешь сделать что бы бизнес логика оставалось одной? Ну типо переиспользуй код с vite версии на babel" |
| 90 | +// REF: issue-12-comment (unified interface request) |
| 91 | +// FORMAT THEOREM: ∀ jsx ∈ JSXOpeningElement: processElement(jsx) → tagged(jsx) ∨ skipped(jsx) |
| 92 | +// PURITY: SHELL (mutates AST) |
| 93 | +// EFFECT: AST mutation |
| 94 | +// INVARIANT: idempotent - processing same element twice produces same result |
| 95 | +// COMPLEXITY: O(n)/O(1) |
| 96 | +export const processJsxElement = ( |
| 97 | + node: t.JSXOpeningElement, |
| 98 | + context: JsxTaggerContext, |
| 99 | + types: typeof t |
| 100 | +): boolean => { |
| 101 | + // Skip if no location info |
| 102 | + if (node.loc === null || node.loc === undefined) { |
| 103 | + return false |
| 104 | + } |
| 105 | + |
| 106 | + // Skip if already has path attribute (idempotency) |
| 107 | + if (attrExists(node, componentPathAttributeName, types)) { |
| 108 | + return false |
| 109 | + } |
| 110 | + |
| 111 | + const { column, line } = node.loc.start |
| 112 | + const attr = createPathAttribute(context.relativeFilename, line, column, types) |
| 113 | + |
| 114 | + node.attributes.push(attr) |
| 115 | + return true |
| 116 | +} |
| 117 | + |
| 118 | +/** |
| 119 | + * Creates a Babel visitor for JSX elements that uses the unified tagging logic. |
| 120 | + * |
| 121 | + * This is the shared visitor factory used by both: |
| 122 | + * - Vite plugin (componentTagger) - passes relative filename directly |
| 123 | + * - Standalone Babel plugin - computes relative filename from state |
| 124 | + * |
| 125 | + * @param getContext - Function to extract context from Babel state. |
| 126 | + * @param types - Babel types module. |
| 127 | + * @returns Babel visitor object for JSXOpeningElement. |
| 128 | + * |
| 129 | + * @pure true (returns immutable visitor object) |
| 130 | + * @invariant visitor applies processJsxElement to all JSX opening elements |
| 131 | + * @complexity O(1) for visitor creation |
| 132 | + */ |
| 133 | +// CHANGE: create shared visitor factory for both plugin types. |
| 134 | +// WHY: single unified interface as requested by user. |
| 135 | +// QUOTE(TZ): "Сделай единный интерфейс для этого" |
| 136 | +// REF: issue-12-comment (unified interface request) |
| 137 | +// FORMAT THEOREM: ∀ visitor = createVisitor(ctx): visitor processes all JSX elements uniformly |
| 138 | +// PURITY: CORE |
| 139 | +// EFFECT: n/a (visitor application has effects) |
| 140 | +// INVARIANT: visitor behavior is consistent across plugin implementations |
| 141 | +// COMPLEXITY: O(1)/O(1) |
| 142 | +export const createJsxTaggerVisitor = <TState>( |
| 143 | + getContext: (state: TState) => JsxTaggerContext | null, |
| 144 | + types: typeof t |
| 145 | +): Visitor<TState> => ({ |
| 146 | + JSXOpeningElement(nodePath, state) { |
| 147 | + const context = getContext(state) |
| 148 | + if (context === null) { |
| 149 | + return |
| 150 | + } |
| 151 | + processJsxElement(nodePath.node, context, types) |
| 152 | + } |
| 153 | +}) |
0 commit comments