UNPKG

@lucidlayer/babel-plugin-traceform

Version:

Babel plugin to inject data-traceform-id attributes into React components for Traceform

600 lines 34.9 kB
"use strict"; /* // SPDX-License-Identifier: MIT */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = injectComponentIdPlugin; const t = __importStar(require("@babel/types")); // Import babel types const path_1 = __importDefault(require("path")); // Import path library for normalization const fs_1 = __importDefault(require("fs")); // Import fs for checking root markers const traceformError_1 = require("./traceformError"); const shared_1 = require("@lucidlayer/shared"); const posthog_node_1 = require("posthog-node"); const dotenv = __importStar(require("dotenv")); dotenv.config(); const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY || ''; const POSTHOG_HOST = process.env.POSTHOG_HOST || 'https://app.posthog.com'; const posthog = new posthog_node_1.PostHog(POSTHOG_API_KEY, { host: POSTHOG_HOST }); const distinctId = process.env.USER || 'anonymous'; try { posthog.capture({ event: 'build_metrics', distinctId, properties: { project_type: 'babel-plugin-traceform', loaded: true } }); } catch (e) { // Do not block plugin on analytics errors console.error('PostHog analytics error:', e); } // Map to track already seen roots to avoid duplicate logs const seenRoots = new Map(); /** * Find the appropriate root directory based on the selected strategy */ function findWorkspaceRoot(startPath, options = {}) { const { rootMode = 'auto', customRoot, quiet = false } = options; // For custom root, directly use the provided path if (rootMode === 'custom' && customRoot) { const resolvedCustomRoot = path_1.default.resolve(customRoot); if (!quiet && !seenRoots.has(resolvedCustomRoot)) { console.log(`[Traceform Babel Plugin] Using custom root: ${resolvedCustomRoot}`); seenRoots.set(resolvedCustomRoot, true); } return resolvedCustomRoot; } // For 'none' mode, just return the startPath without normalization if (rootMode === 'none') { if (!quiet && !seenRoots.has(startPath)) { console.log(`[Traceform Babel Plugin] Using absolute paths (no normalization)`); seenRoots.set(startPath, true); } return startPath; } let currentPath = path_1.default.resolve(startPath); // In package mode or auto mode, try to find package.json first if (rootMode === 'package' || rootMode === 'auto') { try { const pkgPath = path_1.default.join(currentPath, 'package.json'); if (fs_1.default.existsSync(pkgPath)) { if (!quiet && !seenRoots.has(currentPath)) { // eslint-disable-next-line no-console console.log(`[Traceform Babel Plugin] Found package.json at: ${currentPath}, using as project root`); seenRoots.set(currentPath, true); } return currentPath; } } catch (e) { // Ignore errors and continue with other strategies } } // If we're in package mode and didn't find it, we should search upward if (rootMode === 'package') { for (let i = 0; i < 10; i++) { // Limit search depth const parentPath = path_1.default.dirname(currentPath); if (parentPath === currentPath) break; // Reached filesystem root currentPath = parentPath; try { const pkgPath = path_1.default.join(currentPath, 'package.json'); if (fs_1.default.existsSync(pkgPath)) { if (!quiet && !seenRoots.has(currentPath)) { console.log(`[Traceform Babel Plugin] Found package.json at: ${currentPath}, using as project root`); seenRoots.set(currentPath, true); } return currentPath; } } catch (e) { // Continue searching } } } // For git mode or continuing auto mode, look for .git directory if (rootMode === 'git' || rootMode === 'auto') { currentPath = path_1.default.resolve(startPath); // Reset path for (let i = 0; i < 10; i++) { try { if (fs_1.default.existsSync(path_1.default.join(currentPath, '.git'))) { if (!quiet && !seenRoots.has(currentPath)) { console.log(`[Traceform Babel Plugin] Found .git at: ${currentPath}, using as project root`); seenRoots.set(currentPath, true); } return currentPath; } } catch (e) { // Continue searching } const parentPath = path_1.default.dirname(currentPath); if (parentPath === currentPath) break; // Reached filesystem root currentPath = parentPath; } } // For monorepo mode or continuing auto mode, check for monorepo markers if (rootMode === 'monorepo' || rootMode === 'auto') { currentPath = path_1.default.resolve(startPath); // Reset path for (let i = 0; i < 10; i++) { try { if (fs_1.default.existsSync(path_1.default.join(currentPath, 'lerna.json')) || fs_1.default.existsSync(path_1.default.join(currentPath, 'pnpm-workspace.yaml')) || fs_1.default.existsSync(path_1.default.join(currentPath, 'nx.json'))) { if (!quiet && !seenRoots.has(currentPath)) { console.log(`[Traceform Babel Plugin] Found monorepo marker at: ${currentPath}, using as project root`); seenRoots.set(currentPath, true); } return currentPath; } // Check for package.json with workspaces field const pkgPath = path_1.default.join(currentPath, 'package.json'); if (fs_1.default.existsSync(pkgPath)) { try { const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf8')); if (pkg.workspaces) { if (!quiet && !seenRoots.has(currentPath)) { console.log(`[Traceform Babel Plugin] Found workspaces in package.json at: ${currentPath}, using as project root`); seenRoots.set(currentPath, true); } return currentPath; } } catch (e) { // Ignore JSON parse errors } } } catch (e) { // Continue searching } const parentPath = path_1.default.dirname(currentPath); if (parentPath === currentPath) break; // Reached filesystem root currentPath = parentPath; } } // Fallback to the starting path if no markers found within depth limit const fallbackRoot = path_1.default.resolve(startPath); if (!quiet && !seenRoots.has(fallbackRoot)) { // eslint-disable-next-line no-console console.log(`[Traceform Babel Plugin] No root markers found, falling back to start path: ${fallbackRoot}`); seenRoots.set(fallbackRoot, true); } return fallbackRoot; } /** * Helper function to normalize path and make it workspace-relative. * It tries to find the monorepo/project root and calculates the path relative to that. */ function normalizeAndMakeRelative(filePath, babelRoot, options = {}) { const absoluteFilePath = path_1.default.resolve(filePath); // Use babelRoot (state.file.opts.root or cwd) as the starting point for finding the *true* workspace root const startSearchPath = babelRoot || process.cwd(); const workspaceRoot = findWorkspaceRoot(startSearchPath, options); // eslint-disable-next-line no-console // console.log(`[Traceform Babel Plugin] File: ${absoluteFilePath}, Babel Root: ${babelRoot}, Found Workspace Root: ${workspaceRoot}`); let relativePath = path_1.default.relative(workspaceRoot, absoluteFilePath); // Ensure forward slashes relativePath = relativePath.replace(/\\/g, '/'); // Optional: Remove leading './' if present (path.relative usually doesn't add it unless same dir) if (relativePath.startsWith('./')) { relativePath = relativePath.substring(2); } // Ensure the path isn't empty if file is in root (e.g., workspaceRoot/file.tsx) if (!relativePath && absoluteFilePath.startsWith(workspaceRoot)) { relativePath = path_1.default.basename(absoluteFilePath); } // Remove any hardcoded prefix; use only the workspace-relative path // eslint-disable-next-line no-console // console.log('[Traceform Babel Plugin] Final traceformId path:', relativePath); return relativePath; } // Helper function to add the data-traceform-id attribute if it doesn't exist function addDataTraceformIdAttribute(path, componentName, filePath, state, options) { var _a, _b; const attributes = path.node.attributes; const hasAttribute = attributes.some((attr) => t.isJSXAttribute(attr) && attr.name.name === 'data-traceform-id'); if (!hasAttribute && typeof filePath === 'string' && filePath.length > 0) { const potentialRoot = ((_a = state.file) === null || _a === void 0 ? void 0 : _a.opts.root) || ((_b = state.file) === null || _b === void 0 ? void 0 : _b.opts.cwd); const babelRoot = potentialRoot !== null && potentialRoot !== void 0 ? potentialRoot : undefined; const relativePath = normalizeAndMakeRelative(filePath, babelRoot, { rootMode: options.rootMode, customRoot: options.customRoot, quiet: options.quiet }); // Use the shared utility to generate the traceformId const traceformId = (0, shared_1.createTraceformId)(relativePath, componentName, 0, babelPluginErrorHandler, 'BabelPlugin'); // If includeAbsPath is true, also add the absolute path ID for more resilient matching if (options.includeAbsPath) { const absFilePath = path_1.default.resolve(filePath).replace(/\\/g, '/'); const absTraceformId = `abs::${absFilePath}::${componentName}::0`; path.node.attributes.push(t.jsxAttribute(t.jsxIdentifier('data-traceform-abs-id'), t.stringLiteral(absTraceformId))); if (options.logIds) { // eslint-disable-next-line no-console console.log(`[Traceform Babel Plugin] Added absolute path ID: ${absTraceformId}`); } } // Log the actual ID when instrumentation occurs (helps with debugging) if (options.logIds) { // eslint-disable-next-line no-console console.log(`[Traceform Babel Plugin] Added data-traceform-id: ${traceformId}`); } path.node.attributes.push(t.jsxAttribute(t.jsxIdentifier('data-traceform-id'), t.stringLiteral(traceformId))); } } // Helper function to find the component name, handling HOCs function getComponentName(componentPath, state) { var _a, _b, _c; let currentPath = componentPath; let componentName = null; // Determine filename from Babel state (works across bundlers) const filename = (_b = (_a = state.filename) !== null && _a !== void 0 ? _a : state.file.opts.filename) !== null && _b !== void 0 ? _b : state.file.opts.filenameRelative; // Check for HOC wrappers like React.memo, React.forwardRef const parentPath = currentPath.parentPath; if ((parentPath === null || parentPath === void 0 ? void 0 : parentPath.isCallExpression()) && parentPath.node.arguments[0] === currentPath.node) { const callee = parentPath.get('callee'); let isWrapper = false; // Check for React.memo or memo() if (callee.isMemberExpression() && t.isIdentifier(callee.node.object, { name: 'React' }) && t.isIdentifier(callee.node.property, { name: 'memo' })) { isWrapper = true; } else if (callee.isIdentifier({ name: 'memo' })) { isWrapper = true; } // Check for React.forwardRef or forwardRef() else if (callee.isMemberExpression() && t.isIdentifier(callee.node.object, { name: 'React' }) && t.isIdentifier(callee.node.property, { name: 'forwardRef' })) { isWrapper = true; } else if (callee.isIdentifier({ name: 'forwardRef' })) { isWrapper = true; } if (isWrapper) { // If wrapped, the name is likely on the VariableDeclarator holding the CallExpression const varDeclarator = parentPath.parentPath; if ((varDeclarator === null || varDeclarator === void 0 ? void 0 : varDeclarator.isVariableDeclarator()) && t.isIdentifier(varDeclarator.node.id)) { return varDeclarator.node.id.name; } // If assigned directly, e.g. export default memo(...) if ((_c = parentPath.parentPath) === null || _c === void 0 ? void 0 : _c.isExportDefaultDeclaration()) { // Fallback to filename: leave componentName null to pick up below componentName = null; } } } // Get name based on the original component path type if (currentPath.isFunctionDeclaration() || currentPath.isFunctionExpression()) { componentName = currentPath.node.id ? currentPath.node.id.name : null; } else if (currentPath.isArrowFunctionExpression()) { const varDecl = currentPath.parentPath; if ((varDecl === null || varDecl === void 0 ? void 0 : varDecl.isVariableDeclarator()) && t.isIdentifier(varDecl.node.id)) { componentName = varDecl.node.id.name; } } else if (currentPath.isClassDeclaration() || currentPath.isClassExpression()) { componentName = currentPath.node.id ? currentPath.node.id.name : null; } // Basic check if it looks like a component name (starts with uppercase) if (componentName && /^[A-Z]/.test(componentName)) { return componentName; } // Fallback: use filename (without extension, UpperCamelCase) if (filename) { const path = require('path'); let base = path.basename(filename, path.extname(filename)); // Convert to UpperCamelCase if not already base = base.replace(/(^|[-_])(\w)/g, (_, __, c) => c.toUpperCase()); // Avoid generic names like 'index' if (base.toLowerCase() === 'index' && filename) { const dir = path.basename(path.dirname(filename)); base = dir.replace(/(^|[-_])(\w)/g, (_, __, c) => c.toUpperCase()); } return base; } return null; } // Helper function to find the first JSXElement within a node or its children function findFirstJSXElement(nodePath) { let targetElementPath = null; if (nodePath.isJSXElement()) { targetElementPath = nodePath.get('openingElement'); } else if (nodePath.isJSXFragment()) { const children = nodePath.get('children'); for (const child of children) { if (child.isJSXElement()) { targetElementPath = child.get('openingElement'); break; // Found the first one } // Potentially look deeper? For now, only direct children. } } else if (nodePath.isParenthesizedExpression()) { // Look inside parentheses, e.g., return (<div />) return findFirstJSXElement(nodePath.get('expression')); } // Add checks for other potential wrapper nodes if necessary // Check if the found element already has the attribute if (targetElementPath) { const hasAttribute = targetElementPath.node.attributes.some((attr) => t.isJSXAttribute(attr) && attr.name.name === 'data-traceform-id'); if (hasAttribute) { return null; // Don't target if attribute already exists } } return targetElementPath; } // Centralized error handler for traceformId errors in the Babel plugin const babelPluginErrorHandler = (context) => { const err = (0, traceformError_1.createTraceformError)('TF-BABEL-001', context.message, context.data, 'babelPlugin.createTraceformId.missingParam', true // telemetry ); (0, traceformError_1.handleTraceformError)(err, context.consumer || 'BabelPlugin'); }; // The main plugin function function injectComponentIdPlugin(options = {}) { // Force instrumentAllElements to true in non-production environments const isProduction = process.env.NODE_ENV === 'production'; const { instrumentAllElements = !isProduction, quiet = false, logIds = true, rootMode = 'auto', customRoot, includeAbsPath = true } = options; if (!quiet) { console.log(`[Traceform Babel Plugin] Running with instrumentAllElements: ${instrumentAllElements}, rootMode: ${rootMode}, includeAbsPath: ${includeAbsPath}`); } // Keep track of component names by JSX path in the file const componentNamesByPath = new Map(); // Track the number of instrumented components let instrumentedComponentCount = 0; return { name: 'inject-traceform-id', // Updated plugin name visitor: { // Process component definitions first to collect component names FunctionDeclaration(path, state) { var _a, _b; const id = path.node.id; if (!id || !/^[A-Z]/.test(id.name)) return; // Skip non-component functions const componentName = getComponentName(path, state); if (!componentName) { // Only error if filename fallback also failed const err = (0, traceformError_1.createTraceformError)('TF-BP-010', '[Babel Plugin] Could not determine component name in FunctionDeclaration (even after filename fallback)', { file: (_b = (_a = state.file) === null || _a === void 0 ? void 0 : _a.opts) === null || _b === void 0 ? void 0 : _b.filename }, 'babelPlugin.componentName.missing', false); (0, traceformError_1.handleTraceformError)(err, 'BabelPlugin'); // @ErrorFeedback return; } // Find the return statement and associate the JSX with this component path.get('body').traverse({ ReturnStatement(returnPath) { var _a, _b, _c, _d; const argumentPath = returnPath.get('argument'); if (!argumentPath.node) { // Use TraceformError for missing return argument const err = (0, traceformError_1.createTraceformError)('TF-BP-011', '[Babel Plugin] No return argument found in component', { componentName, file: (_b = (_a = state.file) === null || _a === void 0 ? void 0 : _a.opts) === null || _b === void 0 ? void 0 : _b.filename }, 'babelPlugin.returnArgument.missing', false); (0, traceformError_1.handleTraceformError)(err, 'BabelPlugin'); // @ErrorFeedback return; } const validArgumentPath = argumentPath; const targetElementPath = findFirstJSXElement(validArgumentPath); if (targetElementPath) { // Save mapping from the JSX element path to component name const elementPathStr = targetElementPath.toString(); componentNamesByPath.set(elementPathStr, componentName); // If not using instrumentAllElements, process direct component instrumentation if (!instrumentAllElements) { const filePath = state.file.opts.filename; if (typeof filePath !== 'string' || !filePath.length) { // Use TraceformError for missing/invalid file path const err = (0, traceformError_1.createTraceformError)('TF-BP-012', '[Babel Plugin] Invalid or missing file path for component', { componentName, filePath }, 'babelPlugin.filePath.invalid', true); (0, traceformError_1.handleTraceformError)(err, 'BabelPlugin'); // @ErrorFeedback return; } try { addDataTraceformIdAttribute(targetElementPath, componentName, filePath, state, { quiet, logIds, rootMode, customRoot, includeAbsPath }); returnPath.stop(); } catch (e) { // Use TraceformError for attribute injection failure const err = (0, traceformError_1.createTraceformError)('TF-BP-013', '[Babel Plugin] Failed to inject data-traceform-id attribute', { componentName, filePath, error: e }, 'babelPlugin.attributeInjection.failed', true); (0, traceformError_1.handleTraceformError)(err, 'BabelPlugin'); // @ErrorFeedback } } } else if (!instrumentAllElements) { // Use TraceformError for missing JSX element, but only if not in instrumentAllElements mode const err = (0, traceformError_1.createTraceformError)('TF-BP-014', '[Babel Plugin] No JSX element found in return statement', { componentName, file: (_d = (_c = state.file) === null || _c === void 0 ? void 0 : _c.opts) === null || _d === void 0 ? void 0 : _d.filename }, 'babelPlugin.jsxElement.missing', false); (0, traceformError_1.handleTraceformError)(err, 'BabelPlugin'); // @ErrorFeedback } } }); }, ArrowFunctionExpression(path, state) { var _a, _b, _c, _d, _e, _f, _g; const varDecl = path.parentPath; const isPascalVar = varDecl.isVariableDeclarator() && t.isIdentifier(varDecl.node.id) && /^[A-Z]/.test(varDecl.node.id.name); const isDefaultExport = (_a = varDecl.parentPath) === null || _a === void 0 ? void 0 : _a.isExportDefaultDeclaration(); if (!isPascalVar && !isDefaultExport) return; const componentName = getComponentName(path, state); if (!componentName) { // Only error if filename fallback also failed const err = (0, traceformError_1.createTraceformError)('TF-BP-020', '[Babel Plugin] Could not determine component name in ArrowFunctionExpression (even after filename fallback)', { file: (_c = (_b = state.file) === null || _b === void 0 ? void 0 : _b.opts) === null || _c === void 0 ? void 0 : _c.filename }, 'babelPlugin.componentName.missing', false); (0, traceformError_1.handleTraceformError)(err, 'BabelPlugin'); // @ErrorFeedback return; } // Find the JSX and associate it with this component const body = path.get('body'); if (!body.isBlockStatement()) { if (!body.node) { const err = (0, traceformError_1.createTraceformError)('TF-BP-021', '[Babel Plugin] No body node found in ArrowFunctionExpression', { componentName, file: (_e = (_d = state.file) === null || _d === void 0 ? void 0 : _d.opts) === null || _e === void 0 ? void 0 : _e.filename }, 'babelPlugin.bodyNode.missing', false); (0, traceformError_1.handleTraceformError)(err, 'BabelPlugin'); // @ErrorFeedback return; } const validBodyPath = body; const targetElementPath = findFirstJSXElement(validBodyPath); if (targetElementPath) { // Save mapping from the JSX element path to component name const elementPathStr = targetElementPath.toString(); componentNamesByPath.set(elementPathStr, componentName); // If not using instrumentAllElements, process direct component instrumentation if (!instrumentAllElements) { const filePath = state.file.opts.filename; if (typeof filePath !== 'string' || !filePath.length) { const err = (0, traceformError_1.createTraceformError)('TF-BP-022', '[Babel Plugin] Invalid or missing file path for ArrowFunctionExpression', { componentName, filePath }, 'babelPlugin.filePath.invalid', true); (0, traceformError_1.handleTraceformError)(err, 'BabelPlugin'); // @ErrorFeedback return; } try { addDataTraceformIdAttribute(targetElementPath, componentName, filePath, state, { quiet, logIds, rootMode, customRoot, includeAbsPath }); } catch (e) { const err = (0, traceformError_1.createTraceformError)('TF-BP-023', '[Babel Plugin] Failed to inject data-traceform-id attribute in ArrowFunctionExpression', { componentName, filePath, error: e }, 'babelPlugin.attributeInjection.failed', true); (0, traceformError_1.handleTraceformError)(err, 'BabelPlugin'); // @ErrorFeedback } } } else if (!instrumentAllElements) { const err = (0, traceformError_1.createTraceformError)('TF-BP-024', '[Babel Plugin] No JSX element found in ArrowFunctionExpression body', { componentName, file: (_g = (_f = state.file) === null || _f === void 0 ? void 0 : _f.opts) === null || _g === void 0 ? void 0 : _g.filename }, 'babelPlugin.jsxElement.missing', false); (0, traceformError_1.handleTraceformError)(err, 'BabelPlugin'); // @ErrorFeedback } } else { body.traverse({ ReturnStatement(returnPath) { var _a, _b, _c, _d; const argumentPath = returnPath.get('argument'); if (!argumentPath.node) { const err = (0, traceformError_1.createTraceformError)('TF-BP-025', '[Babel Plugin] No return argument found in ArrowFunctionExpression block', { componentName, file: (_b = (_a = state.file) === null || _a === void 0 ? void 0 : _a.opts) === null || _b === void 0 ? void 0 : _b.filename }, 'babelPlugin.returnArgument.missing', false); (0, traceformError_1.handleTraceformError)(err, 'BabelPlugin'); // @ErrorFeedback return; } const validArgumentPath = argumentPath; const targetElementPath = findFirstJSXElement(validArgumentPath); if (targetElementPath) { // Save mapping from the JSX element path to component name const elementPathStr = targetElementPath.toString(); componentNamesByPath.set(elementPathStr, componentName); // If not using instrumentAllElements, process direct component instrumentation if (!instrumentAllElements) { const filePath = state.file.opts.filename; if (typeof filePath !== 'string' || !filePath.length) { const err = (0, traceformError_1.createTraceformError)('TF-BP-026', '[Babel Plugin] Invalid or missing file path for ArrowFunctionExpression block', { componentName, filePath }, 'babelPlugin.filePath.invalid', true); (0, traceformError_1.handleTraceformError)(err, 'BabelPlugin'); // @ErrorFeedback return; } try { addDataTraceformIdAttribute(targetElementPath, componentName, filePath, state, { quiet, logIds, rootMode, customRoot, includeAbsPath }); returnPath.stop(); } catch (e) { const err = (0, traceformError_1.createTraceformError)('TF-BP-027', '[Babel Plugin] Failed to inject data-traceform-id attribute in ArrowFunctionExpression block', { componentName, filePath, error: e }, 'babelPlugin.attributeInjection.failed', true); (0, traceformError_1.handleTraceformError)(err, 'BabelPlugin'); // @ErrorFeedback } } } else if (!instrumentAllElements) { const err = (0, traceformError_1.createTraceformError)('TF-BP-028', '[Babel Plugin] No JSX element found in ArrowFunctionExpression block return', { componentName, file: (_d = (_c = state.file) === null || _c === void 0 ? void 0 : _c.opts) === null || _d === void 0 ? void 0 : _d.filename }, 'babelPlugin.jsxElement.missing', false); (0, traceformError_1.handleTraceformError)(err, 'BabelPlugin'); // @ErrorFeedback } } }); } }, // Full instrumentation: annotate every JSXOpeningElement, but try to use component names JSXOpeningElement(path, state) { var _a; if (!instrumentAllElements) return; const nameNode = path.node.name; if (!t.isJSXIdentifier(nameNode)) return; // Default to HTML tag name let elementName = nameNode.name; const filePath = (_a = state.filename) !== null && _a !== void 0 ? _a : state.file.opts.filename; if (typeof filePath !== 'string' || !filePath) return; // Check if this JSX element is a component's root element const pathStr = path.toString(); const parentComponentName = componentNamesByPath.get(pathStr); // If it's a component's root JSX element, use the component name // Otherwise if it's a capitalized name, it might be a custom component if (parentComponentName) { elementName = parentComponentName; } else if (/^[A-Z]/.test(elementName)) { // It's likely a custom component (React components start with uppercase) // Keep elementName as is } else { // Try a more robust way to find the parent component let ancestor = path.findParent(p => { // Find the closest parent function or class that looks like a component return (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isClassDeclaration()) && (p.node.id && /^[A-Z]/.test(p.node.id.name)); }); if (ancestor) { const ancestorComponentName = getComponentName(ancestor, state); if (ancestorComponentName) { elementName = ancestorComponentName; } } } // Now add the attribute with the best name we could find addDataTraceformIdAttribute(path, elementName, filePath, state, { quiet, logIds, rootMode, customRoot, includeAbsPath }); }, // ... keep other existing visitors ... }, }; } // At the end of the plugin function, after all JSX elements are processed, send build_metrics event // Track the number of instrumented components let instrumentedComponentCount = 0; try { posthog.capture({ event: 'build_metrics', distinctId, properties: { component_count: instrumentedComponentCount, success: instrumentedComponentCount > 0 } }); } catch (e) { posthog.capture({ event: 'error', distinctId, properties: { error_message: e instanceof Error ? e.message : String(e), component: 'babel-plugin-traceform', context: {} } }); } //# sourceMappingURL=index.js.map