Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Refactor the long parameter lists to use a context object
  • Loading branch information
antonis committed Aug 6, 2025
commit d728ba5e5e25bf6417eeff006568594542d436c0
186 changes: 77 additions & 109 deletions packages/babel-plugin-component-annotate/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,24 @@ interface AnnotationPluginPass extends PluginPass {

type AnnotationPlugin = PluginObj<AnnotationPluginPass>;

// Shared context object for all JSX processing functions
interface JSXProcessingContext {
/** Whether to annotate React fragments */
annotateFragments: boolean;
/** Babel types object */
t: typeof Babel.types;
/** Name of the React component */
componentName: string;
/** Source file name (optional) */
sourceFileName?: string;
/** Array of attribute names [component, element, sourceFile] */
attributeNames: string[];
/** Array of component names to ignore */
ignoredComponents: string[];
/** Fragment context for identifying React fragments */
fragmentContext?: FragmentContext;
}

// We must export the plugin as default, otherwise the Babel loader will not be able to resolve it when configured using its string identifier
export default function componentNameAnnotatePlugin({ types: t }: typeof Babel): AnnotationPlugin {
return {
Expand All @@ -81,16 +99,8 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel):
return;
}

functionBodyPushAttributes(
state.opts["annotate-fragments"] === true,
t,
path,
path.node.id.name,
sourceFileNameFromState(state),
attributeNamesFromState(state),
state.opts.ignoredComponents ?? [],
state.sentryFragmentContext
);
const context = createJSXProcessingContext(state, t, path.node.id.name);
functionBodyPushAttributes(context, path);
},
ArrowFunctionExpression(path, state) {
// We're expecting a `VariableDeclarator` like `const MyComponent =`
Expand All @@ -110,16 +120,8 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel):
return;
}

functionBodyPushAttributes(
state.opts["annotate-fragments"] === true,
t,
path,
parent.id.name,
sourceFileNameFromState(state),
attributeNamesFromState(state),
state.opts.ignoredComponents ?? [],
state.sentryFragmentContext
);
const context = createJSXProcessingContext(state, t, parent.id.name);
functionBodyPushAttributes(context, path);
},
ClassDeclaration(path, state) {
const name = path.get("id");
Expand All @@ -132,7 +134,7 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel):
return;
}

const ignoredComponents = state.opts.ignoredComponents ?? [];
const context = createJSXProcessingContext(state, t, name.node?.name || "");

render.traverse({
ReturnStatement(returnStatement) {
Expand All @@ -142,32 +144,41 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel):
return;
}

processJSX(
state.opts["annotate-fragments"] === true,
t,
arg,
name.node && name.node.name,
sourceFileNameFromState(state),
attributeNamesFromState(state),
ignoredComponents,
state.sentryFragmentContext
);
processJSX(context, arg);
},
});
},
},
};
}

function functionBodyPushAttributes(
annotateFragments: boolean,
/**
* Creates a JSX processing context from the plugin state
*/
function createJSXProcessingContext(
state: AnnotationPluginPass,
t: typeof Babel.types,
path: Babel.NodePath<Babel.types.Function>,
componentName: string,
sourceFileName: string | undefined,
attributeNames: string[],
ignoredComponents: string[],
fragmentContext?: FragmentContext
componentName: string
): JSXProcessingContext {
return {
annotateFragments: state.opts["annotate-fragments"] === true,
t,
componentName,
sourceFileName: sourceFileNameFromState(state),
attributeNames: attributeNamesFromState(state),
ignoredComponents: state.opts.ignoredComponents ?? [],
fragmentContext: state.sentryFragmentContext,
};
}

/**
* Processes the body of a function to add Sentry tracking attributes to JSX elements.
* Handles various function body structures including direct JSX returns, conditional expressions,
* and nested JSX elements.
*/
function functionBodyPushAttributes(
context: JSXProcessingContext,
path: Babel.NodePath<Babel.types.Function>
): void {
let jsxNode: Babel.NodePath;

Expand Down Expand Up @@ -209,29 +220,11 @@ function functionBodyPushAttributes(
if (arg.isConditionalExpression()) {
const consequent = arg.get("consequent");
if (consequent.isJSXFragment() || consequent.isJSXElement()) {
processJSX(
annotateFragments,
t,
consequent,
componentName,
sourceFileName,
attributeNames,
ignoredComponents,
fragmentContext
);
processJSX(context, consequent);
}
const alternate = arg.get("alternate");
if (alternate.isJSXFragment() || alternate.isJSXElement()) {
processJSX(
annotateFragments,
t,
alternate,
componentName,
sourceFileName,
attributeNames,
ignoredComponents,
fragmentContext
);
processJSX(context, alternate);
}
return;
}
Expand All @@ -247,45 +240,36 @@ function functionBodyPushAttributes(
return;
}

processJSX(
annotateFragments,
t,
jsxNode,
componentName,
sourceFileName,
attributeNames,
ignoredComponents,
fragmentContext
);
processJSX(context, jsxNode);
}

/**
* Recursively processes JSX elements to add Sentry tracking attributes.
* Handles both JSX elements and fragments, applying appropriate attributes
* based on configuration and component context.
*/
function processJSX(
annotateFragments: boolean,
t: typeof Babel.types,
context: JSXProcessingContext,
jsxNode: Babel.NodePath,
componentName: string | null,
sourceFileName: string | undefined,
attributeNames: string[],
ignoredComponents: string[],
fragmentContext?: FragmentContext
componentName?: string | null
Copy link

@alwx alwx Aug 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel slightly confused that componentName could be a string, undefined and null now — in which situations it could be null and undefined and what's the difference between these two cases?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point @alwx 👍
I tried to simplify this with ea68863

): void {
if (!jsxNode) {
return;
}

// Use provided componentName or fall back to context componentName
const currentComponentName = componentName !== undefined ? componentName : context.componentName;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will there be any cases where componentName is null? If so, do we want to fall back to the context data instead of setting null?
If true, we can use the following suggestion:

Suggested change
const currentComponentName = componentName !== undefined ? componentName : context.componentName;
const currentComponentName = componentName ?? context.componentName;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually from the code below you do set componentName as null in some function calls, on that context, I believe we should give it a chance for the context.componentName to be set

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point @lucas-zimerman 👍
Updated with ea68863


// NOTE: I don't know of a case where `openingElement` would have more than one item,
// but it's safer to always iterate
const paths = jsxNode.get("openingElement");
const openingElements = Array.isArray(paths) ? paths : [paths];

openingElements.forEach((openingElement) => {
applyAttributes(
t,
context,
openingElement as Babel.NodePath<Babel.types.JSXOpeningElement>,
componentName,
sourceFileName,
attributeNames,
ignoredComponents,
fragmentContext
currentComponentName
);
});

Expand All @@ -296,7 +280,7 @@ function processJSX(
children = [children];
}

let shouldSetComponentName = annotateFragments;
let shouldSetComponentName = context.annotateFragments;

children.forEach((child) => {
// Happens for some node types like plain text
Expand All @@ -314,40 +298,24 @@ function processJSX(

if (shouldSetComponentName && openingElement && openingElement.node) {
shouldSetComponentName = false;
processJSX(
annotateFragments,
t,
child,
componentName,
sourceFileName,
attributeNames,
ignoredComponents,
fragmentContext
);
processJSX(context, child, currentComponentName);
} else {
processJSX(
annotateFragments,
t,
child,
null,
sourceFileName,
attributeNames,
ignoredComponents,
fragmentContext
);
processJSX(context, child, null);
}
});
}

/**
* Applies Sentry tracking attributes to a JSX opening element.
* Adds component name, element name, and source file attributes while
* respecting ignore lists and fragment detection.
*/
function applyAttributes(
t: typeof Babel.types,
context: JSXProcessingContext,
openingElement: Babel.NodePath<Babel.types.JSXOpeningElement>,
componentName: string | null,
sourceFileName: string | undefined,
attributeNames: string[],
ignoredComponents: string[],
fragmentContext?: FragmentContext
componentName: string | null
): void {
const { t, attributeNames, ignoredComponents, fragmentContext, sourceFileName } = context;
const [componentAttributeName, elementAttributeName, sourceFileAttributeName] = attributeNames;

// e.g., Raw JSX text like the `A` in `<h1>a</h1>`
Expand Down
Loading