@truewatchtech/react-native-mobile-babel-plugin 0.4.3-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +185 -0
  2. package/lib/commonjs/actions/global.js +19 -0
  3. package/lib/commonjs/actions/global.js.map +1 -0
  4. package/lib/commonjs/actions/rum/components.js +81 -0
  5. package/lib/commonjs/actions/rum/components.js.map +1 -0
  6. package/lib/commonjs/actions/rum/content.js +168 -0
  7. package/lib/commonjs/actions/rum/content.js.map +1 -0
  8. package/lib/commonjs/actions/rum/index.js +68 -0
  9. package/lib/commonjs/actions/rum/index.js.map +1 -0
  10. package/lib/commonjs/actions/rum/memoization.js +88 -0
  11. package/lib/commonjs/actions/rum/memoization.js.map +1 -0
  12. package/lib/commonjs/actions/rum/metadata.js +63 -0
  13. package/lib/commonjs/actions/rum/metadata.js.map +1 -0
  14. package/lib/commonjs/actions/rum/tap.js +45 -0
  15. package/lib/commonjs/actions/rum/tap.js.map +1 -0
  16. package/lib/commonjs/constants.js +25 -0
  17. package/lib/commonjs/constants.js.map +1 -0
  18. package/lib/commonjs/index.js +48 -0
  19. package/lib/commonjs/index.js.map +1 -0
  20. package/lib/commonjs/options.js +23 -0
  21. package/lib/commonjs/options.js.map +1 -0
  22. package/lib/commonjs/state.js +32 -0
  23. package/lib/commonjs/state.js.map +1 -0
  24. package/lib/commonjs/types.js +6 -0
  25. package/lib/commonjs/types.js.map +1 -0
  26. package/lib/commonjs/utils/jsx.js +43 -0
  27. package/lib/commonjs/utils/jsx.js.map +1 -0
  28. package/lib/module/actions/global.js +13 -0
  29. package/lib/module/actions/global.js.map +1 -0
  30. package/lib/module/actions/rum/components.js +73 -0
  31. package/lib/module/actions/rum/components.js.map +1 -0
  32. package/lib/module/actions/rum/content.js +161 -0
  33. package/lib/module/actions/rum/content.js.map +1 -0
  34. package/lib/module/actions/rum/index.js +61 -0
  35. package/lib/module/actions/rum/index.js.map +1 -0
  36. package/lib/module/actions/rum/memoization.js +82 -0
  37. package/lib/module/actions/rum/memoization.js.map +1 -0
  38. package/lib/module/actions/rum/metadata.js +56 -0
  39. package/lib/module/actions/rum/metadata.js.map +1 -0
  40. package/lib/module/actions/rum/tap.js +38 -0
  41. package/lib/module/actions/rum/tap.js.map +1 -0
  42. package/lib/module/constants.js +19 -0
  43. package/lib/module/constants.js.map +1 -0
  44. package/lib/module/index.js +42 -0
  45. package/lib/module/index.js.map +1 -0
  46. package/lib/module/options.js +17 -0
  47. package/lib/module/options.js.map +1 -0
  48. package/lib/module/state.js +25 -0
  49. package/lib/module/state.js.map +1 -0
  50. package/lib/module/types.js +2 -0
  51. package/lib/module/types.js.map +1 -0
  52. package/lib/module/utils/jsx.js +36 -0
  53. package/lib/module/utils/jsx.js.map +1 -0
  54. package/lib/typescript/actions/global.d.ts +2 -0
  55. package/lib/typescript/actions/rum/components.d.ts +5 -0
  56. package/lib/typescript/actions/rum/content.d.ts +6 -0
  57. package/lib/typescript/actions/rum/index.d.ts +4 -0
  58. package/lib/typescript/actions/rum/memoization.d.ts +4 -0
  59. package/lib/typescript/actions/rum/metadata.d.ts +4 -0
  60. package/lib/typescript/actions/rum/tap.d.ts +6 -0
  61. package/lib/typescript/constants.d.ts +4 -0
  62. package/lib/typescript/index.d.ts +5 -0
  63. package/lib/typescript/options.d.ts +2 -0
  64. package/lib/typescript/state.d.ts +5 -0
  65. package/lib/typescript/types.d.ts +56 -0
  66. package/lib/typescript/utils/jsx.d.ts +4 -0
  67. package/package.json +68 -0
  68. package/src/actions/global.ts +48 -0
  69. package/src/actions/rum/components.ts +128 -0
  70. package/src/actions/rum/content.ts +274 -0
  71. package/src/actions/rum/index.ts +154 -0
  72. package/src/actions/rum/memoization.ts +136 -0
  73. package/src/actions/rum/metadata.ts +147 -0
  74. package/src/actions/rum/tap.ts +148 -0
  75. package/src/constants.ts +21 -0
  76. package/src/index.ts +50 -0
  77. package/src/options.ts +26 -0
  78. package/src/state.ts +39 -0
  79. package/src/types.ts +72 -0
  80. package/src/utils/jsx.ts +50 -0
@@ -0,0 +1,274 @@
1
+ /*
2
+ * Portions of this file are adapted from Datadog's dd-sdk-reactnative Babel
3
+ * interaction tracking implementation, licensed under Apache-2.0.
4
+ */
5
+
6
+ import type * as Babel from '@babel/core';
7
+ import type { PluginState, TrackedComponentData } from '../../types';
8
+ import { getDirectAttributePaths, getNodeName } from '../../utils/jsx';
9
+
10
+ const CONTENT_PROPS = ['trackingLabel', 'title', 'label', 'text'];
11
+
12
+ function contentCandidateNames(component: TrackedComponentData): string[] {
13
+ return component.contentProp && !CONTENT_PROPS.includes(component.contentProp)
14
+ ? [...CONTENT_PROPS, component.contentProp]
15
+ : CONTENT_PROPS;
16
+ }
17
+
18
+ function elementType(
19
+ t: typeof Babel.types,
20
+ name: Babel.types.JSXOpeningElement['name']
21
+ ): Babel.types.Expression {
22
+ if (t.isJSXMemberExpression(name)) {
23
+ // Lowercase member receivers (e.g. ui.Text) are variables, not host tags.
24
+ const receiver = t.isJSXIdentifier(name.object)
25
+ ? t.identifier(name.object.name)
26
+ : elementType(t, name.object);
27
+ return t.memberExpression(receiver, t.identifier(name.property.name));
28
+ }
29
+ const value = getNodeName(t, name) || '';
30
+ return t.isJSXIdentifier(name) && !/^[a-z]/.test(value)
31
+ ? t.identifier(value)
32
+ : t.stringLiteral(value);
33
+ }
34
+
35
+ function effectiveAttributes(
36
+ t: typeof Babel.types,
37
+ attributes: Babel.types.JSXOpeningElement['attributes']
38
+ ): Map<string, Babel.types.JSXAttribute> {
39
+ const result = new Map<string, Babel.types.JSXAttribute>();
40
+ for (const attribute of attributes) {
41
+ if (t.isJSXSpreadAttribute(attribute)) {
42
+ // Do not replay spreads or trust props they may have overridden.
43
+ result.clear();
44
+ } else {
45
+ result.set(getNodeName(t, attribute.name) || '', attribute);
46
+ }
47
+ }
48
+ return result;
49
+ }
50
+
51
+ function contentSourcePaths(
52
+ t: typeof Babel.types,
53
+ path: Babel.NodePath<Babel.types.JSXElement>,
54
+ component: TrackedComponentData
55
+ ): Babel.NodePath[] {
56
+ const candidateNames = contentCandidateNames(component);
57
+ const attributes = new Map<
58
+ string,
59
+ Babel.NodePath<Babel.types.JSXAttribute>
60
+ >();
61
+ for (const attributePath of getDirectAttributePaths(path)) {
62
+ if (!attributePath.isJSXAttribute()) {
63
+ attributes.clear();
64
+ continue;
65
+ }
66
+ attributes.set(
67
+ getNodeName(t, attributePath.node.name) || '',
68
+ attributePath
69
+ );
70
+ }
71
+ const sources: Babel.NodePath[] = [];
72
+ for (const name of candidateNames) {
73
+ const attributePath = attributes.get(name);
74
+ const valuePath = attributePath?.get('value');
75
+ if (valuePath && !Array.isArray(valuePath) && valuePath.node) {
76
+ sources.push(valuePath as Babel.NodePath<Babel.types.Node>);
77
+ }
78
+ }
79
+ return sources.concat(path.get('children') as Babel.NodePath[]);
80
+ }
81
+
82
+ function containsPath(
83
+ root: Babel.NodePath,
84
+ candidate: Babel.NodePath
85
+ ): boolean {
86
+ let current: Babel.NodePath | null = candidate;
87
+ while (current) {
88
+ if (current === root || current.node === root.node) {
89
+ return true;
90
+ }
91
+ current = current.parentPath;
92
+ }
93
+ return false;
94
+ }
95
+
96
+ /** Checks whether moving content into a memo callback preserves every binding. */
97
+ export function isContentVisibleFromScope(
98
+ t: typeof Babel.types,
99
+ path: Babel.NodePath<Babel.types.JSXElement>,
100
+ component: TrackedComponentData,
101
+ destinationScope: Babel.NodePath['scope']
102
+ ): boolean {
103
+ const sources = contentSourcePaths(t, path, component);
104
+ let visible = true;
105
+ const checkReference = (referencePath: Babel.NodePath, name: string) => {
106
+ const sourceBinding = referencePath.scope.getBinding(name);
107
+ if (
108
+ sourceBinding &&
109
+ sources.some((source) => containsPath(source, sourceBinding.path))
110
+ ) {
111
+ return;
112
+ }
113
+ const destinationBinding = destinationScope.getBinding(name);
114
+ if (sourceBinding?.identifier !== destinationBinding?.identifier) {
115
+ visible = false;
116
+ }
117
+ };
118
+
119
+ for (const source of sources) {
120
+ source.traverse({
121
+ ReferencedIdentifier(referencePath) {
122
+ checkReference(referencePath, referencePath.node.name);
123
+ if (!visible) {
124
+ referencePath.stop();
125
+ }
126
+ },
127
+ JSXOpeningElement(openingPath) {
128
+ let name = openingPath.node.name;
129
+ let isMember = false;
130
+ while (t.isJSXMemberExpression(name)) {
131
+ isMember = true;
132
+ name = name.object;
133
+ }
134
+ if (
135
+ t.isJSXIdentifier(name) &&
136
+ (isMember || !/^[a-z]/.test(name.name))
137
+ ) {
138
+ checkReference(openingPath, name.name);
139
+ }
140
+ if (!visible) {
141
+ openingPath.stop();
142
+ }
143
+ },
144
+ JSXSpreadAttribute(spreadPath) {
145
+ // Spread attributes are not copied into the content getter.
146
+ spreadPath.skip();
147
+ },
148
+ });
149
+ if (!visible) {
150
+ return false;
151
+ }
152
+ }
153
+ return true;
154
+ }
155
+
156
+ /** Converts JSX anywhere in an expression, including render functions and maps. */
157
+ function toRuntimeNode(
158
+ t: typeof Babel.types,
159
+ node: Babel.types.Node,
160
+ react: Babel.types.Identifier
161
+ ): Babel.types.Node {
162
+ if (t.isJSXElement(node) || t.isJSXFragment(node)) {
163
+ const properties: Babel.types.ObjectProperty[] = [];
164
+ if (t.isJSXElement(node)) {
165
+ for (const [name, attribute] of effectiveAttributes(
166
+ t,
167
+ node.openingElement.attributes
168
+ )) {
169
+ properties.push(
170
+ t.objectProperty(
171
+ t.stringLiteral(name),
172
+ attribute.value
173
+ ? (toRuntimeNode(
174
+ t,
175
+ attribute.value,
176
+ react
177
+ ) as Babel.types.Expression)
178
+ : t.booleanLiteral(true)
179
+ )
180
+ );
181
+ }
182
+ }
183
+ const children = node.children
184
+ .filter(
185
+ (child) =>
186
+ !(t.isJSXText(child) && !child.value.trim()) &&
187
+ !(
188
+ t.isJSXExpressionContainer(child) &&
189
+ t.isJSXEmptyExpression(child.expression)
190
+ )
191
+ )
192
+ .map((child) => toRuntimeNode(t, child, react) as Babel.types.Expression);
193
+ return t.callExpression(
194
+ t.memberExpression(t.cloneNode(react), t.identifier('createElement')),
195
+ [
196
+ t.isJSXElement(node)
197
+ ? elementType(t, node.openingElement.name)
198
+ : t.memberExpression(t.cloneNode(react), t.identifier('Fragment')),
199
+ properties.length ? t.objectExpression(properties) : t.nullLiteral(),
200
+ ...children,
201
+ ]
202
+ );
203
+ }
204
+ if (t.isJSXText(node)) {
205
+ return t.stringLiteral(node.value.replace(/\s+/g, ' ').trim());
206
+ }
207
+ if (t.isJSXExpressionContainer(node)) {
208
+ return toRuntimeNode(t, node.expression, react);
209
+ }
210
+ // Recurse through all expression shapes so cloned JSX cannot survive Babel's
211
+ // JSX visitor or be instrumented a second time inside the content getter.
212
+ const cloned = t.cloneNode(node, false);
213
+ const fields = cloned as unknown as Record<
214
+ string,
215
+ Babel.types.Node | Babel.types.Node[] | null
216
+ >;
217
+ for (const key of t.VISITOR_KEYS[node.type] || []) {
218
+ const value = fields[key];
219
+ if (Array.isArray(value)) {
220
+ fields[key] = value.map((child) =>
221
+ child ? toRuntimeNode(t, child, react) : child
222
+ );
223
+ } else if (value) {
224
+ fields[key] = toRuntimeNode(t, value, react);
225
+ }
226
+ }
227
+ return cloned;
228
+ }
229
+
230
+ /** Matches Datadog's click-time evaluation of content props and JSX children. */
231
+ export function buildContentGetter(
232
+ t: typeof Babel.types,
233
+ path: Babel.NodePath<Babel.types.JSXElement>,
234
+ component: TrackedComponentData,
235
+ state: PluginState
236
+ ): Babel.types.ArrowFunctionExpression | null {
237
+ if (!component.useContent) {
238
+ return null;
239
+ }
240
+ const candidateNames = contentCandidateNames(component);
241
+ const attributes = effectiveAttributes(
242
+ t,
243
+ path.node.openingElement.attributes
244
+ );
245
+ const candidates: Babel.types.Expression[] = [];
246
+ for (const name of candidateNames) {
247
+ const attribute = attributes.get(name);
248
+ if (attribute?.value) {
249
+ candidates.push(
250
+ toRuntimeNode(
251
+ t,
252
+ attribute.value,
253
+ state._ftReactIdentifier
254
+ ) as Babel.types.Expression
255
+ );
256
+ }
257
+ }
258
+ const fragment = t.jsxFragment(
259
+ t.jsxOpeningFragment(),
260
+ t.jsxClosingFragment(),
261
+ path.node.children
262
+ );
263
+ return t.arrowFunctionExpression(
264
+ [],
265
+ t.callExpression(t.cloneNode(state._ftExtractTextIdentifier), [
266
+ toRuntimeNode(
267
+ t,
268
+ fragment,
269
+ state._ftReactIdentifier
270
+ ) as Babel.types.Expression,
271
+ t.arrayExpression(candidates),
272
+ ])
273
+ );
274
+ }
@@ -0,0 +1,154 @@
1
+ /*
2
+ * Portions of this file are adapted from Datadog's dd-sdk-reactnative Babel
3
+ * interaction tracking implementation, licensed under Apache-2.0.
4
+ */
5
+
6
+ import type * as Babel from '@babel/core';
7
+ import { RUNTIME_PACKAGE } from '../../constants';
8
+ import type {
9
+ ActionMetadata,
10
+ NormalizedPluginOptions,
11
+ PluginState,
12
+ } from '../../types';
13
+ import { getDirectAttributePaths, getNodeName } from '../../utils/jsx';
14
+ import {
15
+ addRequiredTextInputHandler,
16
+ isInsideConfiguredComponent,
17
+ } from './components';
18
+ import { buildMetadata } from './metadata';
19
+ import { wrapMemoizedHandler } from './memoization';
20
+ import { buildGuardedHandlerWrapper, buildHandlerWrapper } from './tap';
21
+
22
+ export function handleJSXElement(
23
+ t: typeof Babel.types,
24
+ path: Babel.NodePath<Babel.types.JSXElement>,
25
+ state: PluginState,
26
+ options: NormalizedPluginOptions
27
+ ): void {
28
+ const componentName = getNodeName(t, path.node.openingElement.name);
29
+ const component = componentName
30
+ ? state._ftTrackedComponents[componentName]
31
+ : undefined;
32
+ if (
33
+ !componentName ||
34
+ !component ||
35
+ isInsideConfiguredComponent(t, path, state._ftCustomNames)
36
+ ) {
37
+ return;
38
+ }
39
+
40
+ addRequiredTextInputHandler(t, path, component, componentName);
41
+ let metadata: ActionMetadata | undefined;
42
+
43
+ for (const attributePath of getDirectAttributePaths(path)) {
44
+ if (
45
+ !attributePath.isJSXAttribute() ||
46
+ attributePath.node.extra?.__ftWrappedAction
47
+ ) {
48
+ continue;
49
+ }
50
+ const attributeName = getNodeName(t, attributePath.node.name);
51
+ const handler = component.handlers.find(
52
+ (item) => item.event === attributeName
53
+ );
54
+ if (!handler) {
55
+ continue;
56
+ }
57
+ const value = attributePath.node.value;
58
+ if (!t.isJSXExpressionContainer(value)) {
59
+ continue;
60
+ }
61
+ const expression = value.expression;
62
+ if (
63
+ !t.isArrowFunctionExpression(expression) &&
64
+ !t.isFunctionExpression(expression) &&
65
+ !t.isIdentifier(expression) &&
66
+ !t.isMemberExpression(expression) &&
67
+ !t.isConditionalExpression(expression)
68
+ ) {
69
+ continue;
70
+ }
71
+
72
+ metadata ||= buildMetadata(
73
+ t,
74
+ path,
75
+ componentName,
76
+ component,
77
+ options,
78
+ state
79
+ );
80
+
81
+ if (
82
+ !wrapMemoizedHandler(
83
+ t,
84
+ attributePath,
85
+ expression,
86
+ metadata,
87
+ state,
88
+ handler.mode,
89
+ handler.actionType
90
+ )
91
+ ) {
92
+ const wrappedHandler = t.isConditionalExpression(expression)
93
+ ? buildGuardedHandlerWrapper(
94
+ t,
95
+ expression,
96
+ metadata,
97
+ state,
98
+ attributePath.scope,
99
+ handler.mode,
100
+ handler.actionType
101
+ )
102
+ : buildHandlerWrapper(
103
+ t,
104
+ expression,
105
+ metadata,
106
+ state,
107
+ attributePath.scope,
108
+ handler.mode,
109
+ handler.actionType
110
+ );
111
+ attributePath.node.value = t.jsxExpressionContainer(wrappedHandler);
112
+ state._ftHasWrappedAction = true;
113
+ }
114
+ attributePath.node.extra = {
115
+ ...attributePath.node.extra,
116
+ __ftWrappedAction: true,
117
+ };
118
+ }
119
+ }
120
+
121
+ export function insertRuntimeImports(
122
+ t: typeof Babel.types,
123
+ programPath: Babel.NodePath<Babel.types.Program>,
124
+ state: PluginState
125
+ ): void {
126
+ if (!state._ftHasWrappedAction) {
127
+ return;
128
+ }
129
+ const imports = [
130
+ t.importSpecifier(
131
+ t.cloneNode(state._ftTrackingIdentifier),
132
+ t.identifier('FTBabelInteractionTracking')
133
+ ),
134
+ ];
135
+ if (state._ftNeedsContentRuntime) {
136
+ imports.push(
137
+ t.importSpecifier(
138
+ t.cloneNode(state._ftExtractTextIdentifier),
139
+ t.identifier('__ftExtractText')
140
+ )
141
+ );
142
+ programPath.unshiftContainer(
143
+ 'body',
144
+ t.importDeclaration(
145
+ [t.importNamespaceSpecifier(t.cloneNode(state._ftReactIdentifier))],
146
+ t.stringLiteral('react')
147
+ )
148
+ );
149
+ }
150
+ programPath.unshiftContainer(
151
+ 'body',
152
+ t.importDeclaration(imports, t.stringLiteral(RUNTIME_PACKAGE))
153
+ );
154
+ }
@@ -0,0 +1,136 @@
1
+ /*
2
+ * Portions of this file are adapted from Datadog's dd-sdk-reactnative Babel
3
+ * interaction tracking implementation, licensed under Apache-2.0.
4
+ */
5
+
6
+ import type * as Babel from '@babel/core';
7
+ import type { ActionMetadata, PluginState, TrackedHandler } from '../../types';
8
+ import { isContentVisibleFromScope } from './content';
9
+ import { buildHandlerWrapper } from './tap';
10
+
11
+ function getMemoizationName(
12
+ t: typeof Babel.types,
13
+ callee: Babel.types.Expression | Babel.types.V8IntrinsicIdentifier
14
+ ): string | null {
15
+ if (t.isIdentifier(callee)) {
16
+ return callee.name;
17
+ }
18
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
19
+ return callee.property.name;
20
+ }
21
+ return null;
22
+ }
23
+
24
+ function getVariableDeclaratorPath(
25
+ bindingPath: Babel.NodePath
26
+ ): Babel.NodePath<Babel.types.VariableDeclarator> | null {
27
+ if (bindingPath.isVariableDeclarator()) {
28
+ return bindingPath;
29
+ }
30
+ return (
31
+ (bindingPath.findParent((parent) =>
32
+ parent.isVariableDeclarator()
33
+ ) as Babel.NodePath<Babel.types.VariableDeclarator> | null) || null
34
+ );
35
+ }
36
+
37
+ function isLocalFunctionReference(
38
+ path: Babel.NodePath,
39
+ expression: Babel.types.Expression
40
+ ): boolean {
41
+ if (!expression || expression.type !== 'Identifier') {
42
+ return true;
43
+ }
44
+ const binding = path.scope.getBinding(expression.name);
45
+ if (!binding) {
46
+ return false;
47
+ }
48
+ return !(
49
+ binding.path.isImportSpecifier() ||
50
+ binding.path.isImportDefaultSpecifier() ||
51
+ binding.path.isImportNamespaceSpecifier()
52
+ );
53
+ }
54
+
55
+ /** Wraps the hook callback once so JSX references retain their memoized identity. */
56
+ export function wrapMemoizedHandler(
57
+ t: typeof Babel.types,
58
+ path: Babel.NodePath<Babel.types.JSXAttribute>,
59
+ expression: Babel.types.Expression,
60
+ metadata: ActionMetadata,
61
+ state: PluginState,
62
+ mode: TrackedHandler['mode'],
63
+ actionType: TrackedHandler['actionType']
64
+ ): boolean {
65
+ if (!t.isIdentifier(expression)) {
66
+ return false;
67
+ }
68
+ const binding = path.scope.getBinding(expression.name);
69
+ if (!binding) {
70
+ return false;
71
+ }
72
+ const declaratorPath = getVariableDeclaratorPath(binding.path);
73
+ if (!declaratorPath) {
74
+ return false;
75
+ }
76
+ const initPath = declaratorPath.get('init');
77
+ if (!initPath.isCallExpression()) {
78
+ return false;
79
+ }
80
+ const memoizationName = getMemoizationName(t, initPath.node.callee);
81
+ if (
82
+ !memoizationName ||
83
+ !['useCallback', 'useMemo'].includes(memoizationName)
84
+ ) {
85
+ return false;
86
+ }
87
+ if (state._ftMemoizedHandlers.has(declaratorPath.node)) {
88
+ return true;
89
+ }
90
+
91
+ const argumentPaths = initPath.get('arguments');
92
+ const callbackPath = argumentPaths[0];
93
+ if (!callbackPath || callbackPath.isSpreadElement()) {
94
+ return false;
95
+ }
96
+ const callback = callbackPath.node;
97
+ if (
98
+ !t.isArrowFunctionExpression(callback) &&
99
+ !t.isFunctionExpression(callback) &&
100
+ !t.isIdentifier(callback)
101
+ ) {
102
+ return false;
103
+ }
104
+ if (!isLocalFunctionReference(path, callback)) {
105
+ state._ftMemoizedHandlers.add(declaratorPath.node);
106
+ return true;
107
+ }
108
+
109
+ const elementPath = path.parentPath?.parentPath;
110
+ const metadataAtMemoDefinition =
111
+ metadata.getContent &&
112
+ elementPath?.isJSXElement() &&
113
+ !isContentVisibleFromScope(
114
+ t,
115
+ elementPath,
116
+ metadata.component,
117
+ initPath.scope
118
+ )
119
+ ? { ...metadata, getContent: null }
120
+ : metadata;
121
+
122
+ callbackPath.replaceWith(
123
+ buildHandlerWrapper(
124
+ t,
125
+ callback,
126
+ metadataAtMemoDefinition,
127
+ state,
128
+ callbackPath.scope,
129
+ memoizationName === 'useMemo' ? 'delayed' : mode,
130
+ actionType
131
+ )
132
+ );
133
+ state._ftMemoizedHandlers.add(declaratorPath.node);
134
+ state._ftHasWrappedAction = true;
135
+ return true;
136
+ }
@@ -0,0 +1,147 @@
1
+ /*
2
+ * Portions of this file are adapted from Datadog's dd-sdk-reactnative Babel
3
+ * interaction tracking implementation, licensed under Apache-2.0.
4
+ */
5
+
6
+ import type * as Babel from '@babel/core';
7
+ import { ACTION_NAME_ATTRIBUTE } from '../../constants';
8
+ import type {
9
+ ActionMetadata,
10
+ NormalizedPluginOptions,
11
+ PluginState,
12
+ TrackedComponentData,
13
+ } from '../../types';
14
+ import { getNodeName } from '../../utils/jsx';
15
+ import { buildContentGetter } from './content';
16
+
17
+ function collectStaticActionNames(
18
+ t: typeof Babel.types,
19
+ path: Babel.NodePath<Babel.types.JSXElement>,
20
+ customAttribute: string | undefined
21
+ ): Pick<
22
+ ActionMetadata,
23
+ 'actionNames' | 'customActionNames' | 'accessibilityLabels'
24
+ > {
25
+ const values: Pick<
26
+ ActionMetadata,
27
+ 'actionNames' | 'customActionNames' | 'accessibilityLabels'
28
+ > = {
29
+ actionNames: [],
30
+ customActionNames: [],
31
+ accessibilityLabels: [],
32
+ };
33
+ path.traverse({
34
+ JSXAttribute(attributePath) {
35
+ if (!t.isStringLiteral(attributePath.node.value)) {
36
+ return;
37
+ }
38
+ const name = getNodeName(t, attributePath.node.name);
39
+ const field =
40
+ name === ACTION_NAME_ATTRIBUTE
41
+ ? values.actionNames
42
+ : name === 'accessibilityLabel'
43
+ ? values.accessibilityLabels
44
+ : name === customAttribute
45
+ ? values.customActionNames
46
+ : null;
47
+ if (field) {
48
+ field.push(t.cloneNode(attributePath.node.value, true));
49
+ }
50
+ },
51
+ });
52
+ return values;
53
+ }
54
+
55
+ export function buildMetadata(
56
+ t: typeof Babel.types,
57
+ path: Babel.NodePath<Babel.types.JSXElement>,
58
+ componentName: string,
59
+ component: TrackedComponentData,
60
+ options: NormalizedPluginOptions,
61
+ state: PluginState
62
+ ): ActionMetadata {
63
+ const names = collectStaticActionNames(t, path, options.actionNameAttribute);
64
+ const hasStaticName = Object.values(names).some((values) =>
65
+ values.some((value) => value.value.trim().length > 0)
66
+ );
67
+ return {
68
+ ...names,
69
+ component,
70
+ componentName,
71
+ getContent: hasStaticName
72
+ ? null
73
+ : buildContentGetter(t, path, component, state),
74
+ };
75
+ }
76
+
77
+ export function buildTargetObject(
78
+ t: typeof Babel.types,
79
+ metadata: ActionMetadata,
80
+ handlerArgsIdentifier: Babel.types.Identifier,
81
+ state: PluginState
82
+ ): Babel.types.ObjectExpression {
83
+ const properties: Babel.types.ObjectProperty[] = [
84
+ t.objectProperty(
85
+ t.identifier('options'),
86
+ t.objectExpression([
87
+ t.objectProperty(
88
+ t.identifier('useContent'),
89
+ t.booleanLiteral(metadata.component.useContent)
90
+ ),
91
+ t.objectProperty(
92
+ t.identifier('useNamePrefix'),
93
+ t.booleanLiteral(metadata.component.useNamePrefix)
94
+ ),
95
+ ])
96
+ ),
97
+ t.objectProperty(
98
+ t.identifier('handlerArgs'),
99
+ t.cloneNode(handlerArgsIdentifier)
100
+ ),
101
+ t.objectProperty(
102
+ t.identifier('componentName'),
103
+ t.stringLiteral(metadata.componentName)
104
+ ),
105
+ ];
106
+
107
+ if (metadata.actionNames.length > 0) {
108
+ properties.push(
109
+ t.objectProperty(
110
+ t.stringLiteral(ACTION_NAME_ATTRIBUTE),
111
+ t.arrayExpression(
112
+ metadata.actionNames.map((value) => t.cloneNode(value, true))
113
+ )
114
+ )
115
+ );
116
+ }
117
+ if (metadata.customActionNames.length > 0) {
118
+ properties.push(
119
+ t.objectProperty(
120
+ t.identifier('customActionName'),
121
+ t.arrayExpression(
122
+ metadata.customActionNames.map((value) => t.cloneNode(value, true))
123
+ )
124
+ )
125
+ );
126
+ }
127
+ if (metadata.accessibilityLabels.length > 0) {
128
+ properties.push(
129
+ t.objectProperty(
130
+ t.identifier('accessibilityLabel'),
131
+ t.arrayExpression(
132
+ metadata.accessibilityLabels.map((value) => t.cloneNode(value, true))
133
+ )
134
+ )
135
+ );
136
+ }
137
+ if (metadata.getContent) {
138
+ state._ftNeedsContentRuntime = true;
139
+ properties.push(
140
+ t.objectProperty(
141
+ t.identifier('getContent'),
142
+ t.cloneNode(metadata.getContent, true)
143
+ )
144
+ );
145
+ }
146
+ return t.objectExpression(properties);
147
+ }