@lucidlayer/babel-plugin-traceform
Version:
Babel plugin to inject data-traceform-id attributes into React components for Traceform
600 lines • 34.9 kB
JavaScript
"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