@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.
- package/README.md +185 -0
- package/lib/commonjs/actions/global.js +19 -0
- package/lib/commonjs/actions/global.js.map +1 -0
- package/lib/commonjs/actions/rum/components.js +81 -0
- package/lib/commonjs/actions/rum/components.js.map +1 -0
- package/lib/commonjs/actions/rum/content.js +168 -0
- package/lib/commonjs/actions/rum/content.js.map +1 -0
- package/lib/commonjs/actions/rum/index.js +68 -0
- package/lib/commonjs/actions/rum/index.js.map +1 -0
- package/lib/commonjs/actions/rum/memoization.js +88 -0
- package/lib/commonjs/actions/rum/memoization.js.map +1 -0
- package/lib/commonjs/actions/rum/metadata.js +63 -0
- package/lib/commonjs/actions/rum/metadata.js.map +1 -0
- package/lib/commonjs/actions/rum/tap.js +45 -0
- package/lib/commonjs/actions/rum/tap.js.map +1 -0
- package/lib/commonjs/constants.js +25 -0
- package/lib/commonjs/constants.js.map +1 -0
- package/lib/commonjs/index.js +48 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/options.js +23 -0
- package/lib/commonjs/options.js.map +1 -0
- package/lib/commonjs/state.js +32 -0
- package/lib/commonjs/state.js.map +1 -0
- package/lib/commonjs/types.js +6 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/utils/jsx.js +43 -0
- package/lib/commonjs/utils/jsx.js.map +1 -0
- package/lib/module/actions/global.js +13 -0
- package/lib/module/actions/global.js.map +1 -0
- package/lib/module/actions/rum/components.js +73 -0
- package/lib/module/actions/rum/components.js.map +1 -0
- package/lib/module/actions/rum/content.js +161 -0
- package/lib/module/actions/rum/content.js.map +1 -0
- package/lib/module/actions/rum/index.js +61 -0
- package/lib/module/actions/rum/index.js.map +1 -0
- package/lib/module/actions/rum/memoization.js +82 -0
- package/lib/module/actions/rum/memoization.js.map +1 -0
- package/lib/module/actions/rum/metadata.js +56 -0
- package/lib/module/actions/rum/metadata.js.map +1 -0
- package/lib/module/actions/rum/tap.js +38 -0
- package/lib/module/actions/rum/tap.js.map +1 -0
- package/lib/module/constants.js +19 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/index.js +42 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/options.js +17 -0
- package/lib/module/options.js.map +1 -0
- package/lib/module/state.js +25 -0
- package/lib/module/state.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/jsx.js +36 -0
- package/lib/module/utils/jsx.js.map +1 -0
- package/lib/typescript/actions/global.d.ts +2 -0
- package/lib/typescript/actions/rum/components.d.ts +5 -0
- package/lib/typescript/actions/rum/content.d.ts +6 -0
- package/lib/typescript/actions/rum/index.d.ts +4 -0
- package/lib/typescript/actions/rum/memoization.d.ts +4 -0
- package/lib/typescript/actions/rum/metadata.d.ts +4 -0
- package/lib/typescript/actions/rum/tap.d.ts +6 -0
- package/lib/typescript/constants.d.ts +4 -0
- package/lib/typescript/index.d.ts +5 -0
- package/lib/typescript/options.d.ts +2 -0
- package/lib/typescript/state.d.ts +5 -0
- package/lib/typescript/types.d.ts +56 -0
- package/lib/typescript/utils/jsx.d.ts +4 -0
- package/package.json +68 -0
- package/src/actions/global.ts +48 -0
- package/src/actions/rum/components.ts +128 -0
- package/src/actions/rum/content.ts +274 -0
- package/src/actions/rum/index.ts +154 -0
- package/src/actions/rum/memoization.ts +136 -0
- package/src/actions/rum/metadata.ts +147 -0
- package/src/actions/rum/tap.ts +148 -0
- package/src/constants.ts +21 -0
- package/src/index.ts +50 -0
- package/src/options.ts +26 -0
- package/src/state.ts +39 -0
- package/src/types.ts +72 -0
- 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
|
+
}
|