From 1196ca552348c8bf62ecda67402603778a4da0dc Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 28 Feb 2025 10:14:05 +0100 Subject: [PATCH] handle jsx member expressions --- .../src/index.ts | 23 +++++ .../__snapshots__/test-plugin.test.ts.snap | 38 ++++++++ .../test/test-plugin.test.ts | 93 +++++++++++++++++++ 3 files changed, 154 insertions(+) diff --git a/packages/babel-plugin-component-annotate/src/index.ts b/packages/babel-plugin-component-annotate/src/index.ts index 0cd087d9..b1f0af1f 100644 --- a/packages/babel-plugin-component-annotate/src/index.ts +++ b/packages/babel-plugin-component-annotate/src/index.ts @@ -533,6 +533,29 @@ function getPathName(t: typeof Babel.types, path: Babel.NodePath): string { return name.name.name; } + // Handle JSX member expressions like Tab.Group + if (t.isJSXMemberExpression(name)) { + const objectName = getJSXMemberExpressionObjectName(t, name.object); + const propertyName = name.property.name; + return `${objectName}.${propertyName}`; + } + + return UNKNOWN_ELEMENT_NAME; +} + +// Recursively handle nested member expressions (e.g. Components.UI.Header) +function getJSXMemberExpressionObjectName( + t: typeof Babel.types, + object: Babel.types.JSXMemberExpression | Babel.types.JSXIdentifier +): string { + if (t.isJSXIdentifier(object)) { + return object.name; + } + if (t.isJSXMemberExpression(object)) { + const objectName = getJSXMemberExpressionObjectName(t, object.object); + return `${objectName}.${object.property.name}`; + } + return UNKNOWN_ELEMENT_NAME; } diff --git a/packages/babel-plugin-component-annotate/test/__snapshots__/test-plugin.test.ts.snap b/packages/babel-plugin-component-annotate/test/__snapshots__/test-plugin.test.ts.snap index adb23ee5..af43b6c0 100644 --- a/packages/babel-plugin-component-annotate/test/__snapshots__/test-plugin.test.ts.snap +++ b/packages/babel-plugin-component-annotate/test/__snapshots__/test-plugin.test.ts.snap @@ -223,6 +223,20 @@ class componentName extends Component { export default componentName;" `; +exports[`handles nested member expressions in component names 1`] = ` +"import React from 'react'; +import { Components } from 'my-ui-library'; +export default function TestComponent() { + return /*#__PURE__*/React.createElement(\\"div\\", { + \\"data-sentry-component\\": \\"TestComponent\\", + \\"data-sentry-source-file\\": \\"filename-test.js\\" + }, /*#__PURE__*/React.createElement(Components.UI.Button, null, \\"Click me\\"), /*#__PURE__*/React.createElement(Components.UI.Card.Header, { + \\"data-sentry-element\\": \\"Components.UI.Card.Header\\", + \\"data-sentry-source-file\\": \\"filename-test.js\\" + }, \\"Title\\")); +}" +`; + exports[`handles ternary operation returned by function body 1`] = ` "const maybeTrue = Math.random() > 0.5; export default function componentName() { @@ -233,6 +247,30 @@ export default function componentName() { }" `; +exports[`ignores React.Fragment with member expression handling 1`] = ` +"import React from 'react'; +export default function TestComponent() { + return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(\\"div\\", null, \\"Content\\")); +}" +`; + +exports[`ignores components with member expressions when in ignoredComponents 1`] = ` +"import React from 'react'; +import { Tab } from '@headlessui/react'; +export default function TestComponent() { + return /*#__PURE__*/React.createElement(\\"div\\", { + \\"data-sentry-component\\": \\"TestComponent\\", + \\"data-sentry-source-file\\": \\"filename-test.js\\" + }, /*#__PURE__*/React.createElement(Tab.Group, null, /*#__PURE__*/React.createElement(Tab.List, null, /*#__PURE__*/React.createElement(Tab, { + \\"data-sentry-element\\": \\"Tab\\", + \\"data-sentry-source-file\\": \\"filename-test.js\\" + }, \\"Tab 1\\"), /*#__PURE__*/React.createElement(Tab, { + \\"data-sentry-element\\": \\"Tab\\", + \\"data-sentry-source-file\\": \\"filename-test.js\\" + }, \\"Tab 2\\")), /*#__PURE__*/React.createElement(Tab.Panels, null, /*#__PURE__*/React.createElement(Tab.Panel, null, \\"Content 1\\"), /*#__PURE__*/React.createElement(Tab.Panel, null, \\"Content 2\\")))); +}" +`; + exports[`nonJSX snapshot matches 1`] = ` "import React, { Component } from 'react'; class TestClass extends Component { diff --git a/packages/babel-plugin-component-annotate/test/test-plugin.test.ts b/packages/babel-plugin-component-annotate/test/test-plugin.test.ts index 7dce87ad..7d6a73e8 100644 --- a/packages/babel-plugin-component-annotate/test/test-plugin.test.ts +++ b/packages/babel-plugin-component-annotate/test/test-plugin.test.ts @@ -1187,3 +1187,96 @@ export default function componentName() { ); expect(result?.code).toMatchSnapshot(); }); + +it("ignores components with member expressions when in ignoredComponents", () => { + const result = transform( + `import React from 'react'; +import { Tab } from '@headlessui/react'; + +export default function TestComponent() { + return ( +
+ + + Tab 1 + Tab 2 + + + Content 1 + Content 2 + + +
+ ); +}`, + { + filename: "/filename-test.js", + configFile: false, + presets: ["@babel/preset-react"], + plugins: [ + [plugin, { ignoredComponents: ["Tab.Group", "Tab.List", "Tab.Panels", "Tab.Panel"] }], + ], + } + ); + + // The component should be transformed but Tab.* components should not have annotations + expect(result?.code).toContain("React.createElement(Tab.Group"); + expect(result?.code).not.toContain('"data-sentry-element": "Tab.Group"'); + expect(result?.code).toContain("React.createElement(Tab.List"); + expect(result?.code).not.toContain('"data-sentry-element": "Tab.List"'); + expect(result?.code).toMatchSnapshot(); +}); + +it("handles nested member expressions in component names", () => { + const result = transform( + `import React from 'react'; +import { Components } from 'my-ui-library'; + +export default function TestComponent() { + return ( +
+ Click me + Title +
+ ); +}`, + { + filename: "/filename-test.js", + configFile: false, + presets: ["@babel/preset-react"], + plugins: [[plugin, { ignoredComponents: ["Components.UI.Button"] }]], + } + ); + + // Components.UI.Button should be ignored but Components.UI.Card.Header should be annotated + expect(result?.code).toContain("React.createElement(Components.UI.Button"); + expect(result?.code).not.toContain('"data-sentry-element": "Components.UI.Button"'); + expect(result?.code).toContain("React.createElement(Components.UI.Card.Header"); + expect(result?.code).toContain('"data-sentry-element": "Components.UI.Card.Header"'); + expect(result?.code).toMatchSnapshot(); +}); + +it("ignores React.Fragment with member expression handling", () => { + const result = transform( + `import React from 'react'; + +export default function TestComponent() { + return ( + +
Content
+
+ ); +}`, + { + filename: "/filename-test.js", + configFile: false, + presets: ["@babel/preset-react"], + plugins: [plugin], + } + ); + + // React.Fragment should be ignored by default + expect(result?.code).toContain("React.createElement(React.Fragment"); + expect(result?.code).not.toContain('"data-sentry-element": "React.Fragment"'); + expect(result?.code).toMatchSnapshot(); +});