From 2ad5d197d275b94fb1483b0c870d39ae184a222f Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Mon, 11 Aug 2025 10:04:18 +0100 Subject: [PATCH] Improve Babel Plugin support for TextInput component Ensure `onFocus` attribute is added if not present already, so our wrapping logic can happen. --- .../react-native-babel-plugin/package.json | 4 +- .../src/actions/rum/index.ts | 66 +++++++++++++++++-- .../src/constants/rum.ts | 4 ++ .../test/plugin.test.ts | 61 +++++++++++++++++ 4 files changed, 126 insertions(+), 9 deletions(-) diff --git a/packages/react-native-babel-plugin/package.json b/packages/react-native-babel-plugin/package.json index ccbabd080..5cbda134e 100644 --- a/packages/react-native-babel-plugin/package.json +++ b/packages/react-native-babel-plugin/package.json @@ -39,14 +39,14 @@ }, "dependencies": { "@babel/core": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/types": "^7.27.1" }, "devDependencies": { "@babel/cli": "^7.27.2", "@babel/preset-env": "^7.27.2", "@babel/preset-react": "^7.27.1", "@babel/preset-typescript": "^7.27.1", - "@babel/types": "^7.27.1", "@swc/core": "^1.11.31", "@swc/jest": "^0.2.38", "@types/jest": "^29.5.14", diff --git a/packages/react-native-babel-plugin/src/actions/rum/index.ts b/packages/react-native-babel-plugin/src/actions/rum/index.ts index 7639be2da..a81d0d2dd 100644 --- a/packages/react-native-babel-plugin/src/actions/rum/index.ts +++ b/packages/react-native-babel-plugin/src/actions/rum/index.ts @@ -5,8 +5,19 @@ */ import type * as Babel from '@babel/core'; +import { + arrowFunctionExpression, + blockStatement, + jsxAttribute, + jsxExpressionContainer, + jsxIdentifier +} from '@babel/types'; -import { RumActionConstants, rumComponentAttributes } from '../../constants'; +import { + RumActionConstants, + rumComponentAttributes, + tapElementsRequiredAttributesMap +} from '../../constants'; import type { PluginPassState, PluginOptions, @@ -39,12 +50,17 @@ export function handleJSXElementActionPaths( state: PluginPassState, options: PluginOptions ) { - const { actionPathList, ddValues } = getJSXElementActionPaths( - componentName, - t, + const { + actionPathList, + actionPathNames, + ddValues + } = getJSXElementActionPaths(componentName, t, path, state, options); + + ensureMandatoryAttributes( path, - state, - options + componentName, + actionPathList, + actionPathNames ); for (const attrPath of actionPathList) { @@ -52,10 +68,44 @@ export function handleJSXElementActionPaths( ...attrPath.node.extra, ddValues }; + handleRumActions(t, attrPath, state); } } +export function ensureMandatoryAttributes( + path: Babel.NodePath, + componentName: string, + actionPathList: Babel.NodePath[], + actionPathNames: string[] +) { + // Check if we're missing some required attributes + const requiredAttributes = tapElementsRequiredAttributesMap[componentName]; + if (requiredAttributes) { + const attrToAdd = requiredAttributes.filter( + x => !actionPathNames.includes(x) + ); + + for (const attr of attrToAdd) { + const attribute = jsxAttribute( + jsxIdentifier(attr), + jsxExpressionContainer( + arrowFunctionExpression([], blockStatement([])) + ) + ); + path.node.openingElement.attributes.push(attribute); + + const attrPaths = path.get( + 'openingElement.attributes' + ) as Babel.NodePath[]; + + const lastPath = attrPaths[attrPaths.length - 1]; + + actionPathList.push(lastPath); + } + } +} + export function getJSXElementActionPaths( componentName: string, t: typeof Babel.types, @@ -71,6 +121,7 @@ export function getJSXElementActionPaths( const ddValues: Record = {}; const actionMapList = state.tapMappings?.[componentName] || []; const actionPathList: Babel.NodePath[] = []; + const actionPathNames: string[] = []; path.traverse({ JSXAttribute(subpath) { @@ -98,13 +149,14 @@ export function getJSXElementActionPaths( const isValidMapping = actionMapList.includes(attrName); if (isValidMapping) { + actionPathNames.push(attrName); actionPathList.push(subpath); return; } } }); - return { actionPathList, ddValues }; + return { actionPathList, actionPathNames, ddValues }; } export function handleRumActions( diff --git a/packages/react-native-babel-plugin/src/constants/rum.ts b/packages/react-native-babel-plugin/src/constants/rum.ts index 78bfa21f0..c29d32ffe 100644 --- a/packages/react-native-babel-plugin/src/constants/rum.ts +++ b/packages/react-native-babel-plugin/src/constants/rum.ts @@ -35,6 +35,10 @@ export const tapElementsMap: Record = { TextInput: ['onFocus'] }; +export const tapElementsRequiredAttributesMap: Record = { + TextInput: ['onFocus'] +}; + export const rumComponentAttributes = [ 'dd-action-name', 'accessibilityLabel' diff --git a/packages/react-native-babel-plugin/test/plugin.test.ts b/packages/react-native-babel-plugin/test/plugin.test.ts index 53beeefed..02ab66d9b 100644 --- a/packages/react-native-babel-plugin/test/plugin.test.ts +++ b/packages/react-native-babel-plugin/test/plugin.test.ts @@ -62,6 +62,67 @@ describe('Babel plugin: wrap interaction handlers for RUM', () => { `); }); + it('should wrap supported property (onFocus) on supported element (TextInput)', () => { + const input = ` + import { TextInput } from 'react-native'; + { console.log('test'); }} + /> + `; + const output = transformCode(input); + expect(output).toMatchInlineSnapshot(` + "import { DdBabelInteractionTracking } from "@datadog/mobile-react-native"; + import { TextInput } from 'react-native'; + /*#__PURE__*/React.createElement(TextInput, { + placeholder: "Enter username", + value: username, + onChangeText: setUsername, + style: styles.input, + onFocus: () => { + if (DdBabelInteractionTracking.getInstance()) return DdBabelInteractionTracking.getInstance().wrapRumAction(() => { + console.log('test'); + }, "TAP", { + "componentName": "TextInput" + })();else return (() => { + console.log('test'); + })(); + } + });" + `); + }); + + it('should add mandatory property (onFocus) on supported element (TextInput) when not present', () => { + const input = ` + import { TextInput } from 'react-native'; + + `; + const output = transformCode(input); + expect(output).toMatchInlineSnapshot(` + "import { DdBabelInteractionTracking } from "@datadog/mobile-react-native"; + import { TextInput } from 'react-native'; + /*#__PURE__*/React.createElement(TextInput, { + placeholder: "Enter username", + value: username, + onChangeText: setUsername, + style: styles.input, + onFocus: () => { + if (DdBabelInteractionTracking.getInstance()) return DdBabelInteractionTracking.getInstance().wrapRumAction(() => {}, "TAP", { + "componentName": "TextInput" + })();else return (() => {})(); + } + });" + `); + }); + it('should wrap arrow function with one argument', () => { const input = ` import { Pressable } from 'react-native';