@userflow-tech/react-native 0.1.0 → 0.1.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 +24 -14
- package/app.plugin.js +3 -0
- package/babel-plugin/index.d.ts +43 -0
- package/babel-plugin/index.d.ts.map +1 -0
- package/babel-plugin/index.js +854 -0
- package/babel-plugin/index.js.map +1 -0
- package/docs/deep-linking.md +2 -2
- package/docs/troubleshooting.md +2 -2
- package/expo-plugin/withUserflow.d.ts +17 -0
- package/expo-plugin/withUserflow.d.ts.map +1 -0
- package/expo-plugin/withUserflow.js +99 -0
- package/expo-plugin/withUserflow.js.map +1 -0
- package/lib/commonjs/anchors/AutoAnchorRegistry.js +1 -1
- package/lib/commonjs/anchors/autoAnchor.js +1 -1
- package/lib/commonjs/anchors/hintsCodec.js +1 -1
- package/lib/commonjs/pairing/diagnostics.js +2 -2
- package/lib/commonjs/pairing/diagnostics.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/anchors/AutoAnchorRegistry.js +1 -1
- package/lib/module/anchors/autoAnchor.js +1 -1
- package/lib/module/anchors/hintsCodec.js +1 -1
- package/lib/module/pairing/diagnostics.js +2 -2
- package/lib/module/pairing/diagnostics.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/commonjs/src/anchors/AutoAnchorRegistry.d.ts +1 -1
- package/lib/typescript/commonjs/src/anchors/AutoAnchorRegistry.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/anchors/autoAnchor.d.ts +1 -1
- package/lib/typescript/commonjs/src/anchors/hintsCodec.d.ts +1 -1
- package/lib/typescript/commonjs/src/version.d.ts +1 -1
- package/lib/typescript/module/src/anchors/AutoAnchorRegistry.d.ts +1 -1
- package/lib/typescript/module/src/anchors/AutoAnchorRegistry.d.ts.map +1 -1
- package/lib/typescript/module/src/anchors/autoAnchor.d.ts +1 -1
- package/lib/typescript/module/src/anchors/hintsCodec.d.ts +1 -1
- package/lib/typescript/module/src/version.d.ts +1 -1
- package/package.json +15 -7
- package/src/anchors/AutoAnchorRegistry.ts +1 -1
- package/src/anchors/autoAnchor.ts +1 -1
- package/src/anchors/hintsCodec.ts +1 -1
- package/src/pairing/diagnostics.ts +2 -2
- package/src/version.ts +1 -1
- package/vendor/core/LICENSE +76 -0
- package/vendor/core/README.md +9 -0
- package/vendor/core/dist/attributes.d.ts +16 -0
- package/vendor/core/dist/attributes.d.ts.map +1 -0
- package/vendor/core/dist/attributes.js +55 -0
- package/vendor/core/dist/attributes.js.map +1 -0
- package/vendor/core/dist/constants.d.ts +21 -0
- package/vendor/core/dist/constants.d.ts.map +1 -0
- package/vendor/core/dist/constants.js +21 -0
- package/vendor/core/dist/constants.js.map +1 -0
- package/vendor/core/dist/engine/ClientConditionEngine.d.ts +68 -0
- package/vendor/core/dist/engine/ClientConditionEngine.d.ts.map +1 -0
- package/vendor/core/dist/engine/ClientConditionEngine.js +197 -0
- package/vendor/core/dist/engine/ClientConditionEngine.js.map +1 -0
- package/vendor/core/dist/engine/FlowSessionEngine.d.ts +92 -0
- package/vendor/core/dist/engine/FlowSessionEngine.d.ts.map +1 -0
- package/vendor/core/dist/engine/FlowSessionEngine.js +310 -0
- package/vendor/core/dist/engine/FlowSessionEngine.js.map +1 -0
- package/vendor/core/dist/engine/PairingEngine.d.ts +92 -0
- package/vendor/core/dist/engine/PairingEngine.d.ts.map +1 -0
- package/vendor/core/dist/engine/PairingEngine.js +302 -0
- package/vendor/core/dist/engine/PairingEngine.js.map +1 -0
- package/vendor/core/dist/engine/UserflowCore.d.ts +126 -0
- package/vendor/core/dist/engine/UserflowCore.d.ts.map +1 -0
- package/vendor/core/dist/engine/UserflowCore.js +266 -0
- package/vendor/core/dist/engine/UserflowCore.js.map +1 -0
- package/vendor/core/dist/engine/handleActions.d.ts +19 -0
- package/vendor/core/dist/engine/handleActions.d.ts.map +1 -0
- package/vendor/core/dist/engine/handleActions.js +71 -0
- package/vendor/core/dist/engine/handleActions.js.map +1 -0
- package/vendor/core/dist/engine/interpolateSessionData.d.ts +7 -0
- package/vendor/core/dist/engine/interpolateSessionData.d.ts.map +1 -0
- package/vendor/core/dist/engine/interpolateSessionData.js +29 -0
- package/vendor/core/dist/engine/interpolateSessionData.js.map +1 -0
- package/vendor/core/dist/errors.d.ts +9 -0
- package/vendor/core/dist/errors.d.ts.map +1 -0
- package/vendor/core/dist/errors.js +15 -0
- package/vendor/core/dist/errors.js.map +1 -0
- package/vendor/core/dist/eventAttributes.d.ts +7 -0
- package/vendor/core/dist/eventAttributes.d.ts.map +1 -0
- package/vendor/core/dist/eventAttributes.js +31 -0
- package/vendor/core/dist/eventAttributes.js.map +1 -0
- package/vendor/core/dist/index.d.ts +30 -0
- package/vendor/core/dist/index.d.ts.map +1 -0
- package/vendor/core/dist/index.js +21 -0
- package/vendor/core/dist/index.js.map +1 -0
- package/vendor/core/dist/transport/backoff.d.ts +11 -0
- package/vendor/core/dist/transport/backoff.d.ts.map +1 -0
- package/vendor/core/dist/transport/backoff.js +17 -0
- package/vendor/core/dist/transport/backoff.js.map +1 -0
- package/vendor/core/dist/transport/pairing.d.ts +121 -0
- package/vendor/core/dist/transport/pairing.d.ts.map +1 -0
- package/vendor/core/dist/transport/pairing.js +551 -0
- package/vendor/core/dist/transport/pairing.js.map +1 -0
- package/vendor/core/dist/transport/phoenix.d.ts +223 -0
- package/vendor/core/dist/transport/phoenix.d.ts.map +1 -0
- package/vendor/core/dist/transport/phoenix.js +757 -0
- package/vendor/core/dist/transport/phoenix.js.map +1 -0
- package/vendor/core/dist/transport/rateLimiter.d.ts +17 -0
- package/vendor/core/dist/transport/rateLimiter.d.ts.map +1 -0
- package/vendor/core/dist/transport/rateLimiter.js +30 -0
- package/vendor/core/dist/transport/rateLimiter.js.map +1 -0
- package/vendor/core/dist/transport/scheduler.d.ts +12 -0
- package/vendor/core/dist/transport/scheduler.d.ts.map +1 -0
- package/vendor/core/dist/transport/scheduler.js +11 -0
- package/vendor/core/dist/transport/scheduler.js.map +1 -0
- package/vendor/core/dist/types.d.ts +35 -0
- package/vendor/core/dist/types.d.ts.map +1 -0
- package/vendor/core/dist/types.js +2 -0
- package/vendor/core/dist/types.js.map +1 -0
- package/vendor/core/dist/version.d.ts +3 -0
- package/vendor/core/dist/version.d.ts.map +1 -0
- package/vendor/core/dist/version.js +3 -0
- package/vendor/core/dist/version.js.map +1 -0
- package/vendor/core/package.json +20 -0
- package/vendor/protocol/LICENSE +76 -0
- package/vendor/protocol/README.md +8 -0
- package/vendor/protocol/dist/autoData.d.ts +232 -0
- package/vendor/protocol/dist/autoData.d.ts.map +1 -0
- package/vendor/protocol/dist/autoData.js +78 -0
- package/vendor/protocol/dist/autoData.js.map +1 -0
- package/vendor/protocol/dist/events.d.ts +16 -0
- package/vendor/protocol/dist/events.d.ts.map +1 -0
- package/vendor/protocol/dist/events.js +8 -0
- package/vendor/protocol/dist/events.js.map +1 -0
- package/vendor/protocol/dist/flows.d.ts +298 -0
- package/vendor/protocol/dist/flows.d.ts.map +1 -0
- package/vendor/protocol/dist/flows.js +154 -0
- package/vendor/protocol/dist/flows.js.map +1 -0
- package/vendor/protocol/dist/index.d.ts +24 -0
- package/vendor/protocol/dist/index.d.ts.map +1 -0
- package/vendor/protocol/dist/index.js +16 -0
- package/vendor/protocol/dist/index.js.map +1 -0
- package/vendor/protocol/dist/messages.d.ts +166 -0
- package/vendor/protocol/dist/messages.d.ts.map +1 -0
- package/vendor/protocol/dist/messages.js +57 -0
- package/vendor/protocol/dist/messages.js.map +1 -0
- package/vendor/protocol/dist/pairing.d.ts +56 -0
- package/vendor/protocol/dist/pairing.d.ts.map +1 -0
- package/vendor/protocol/dist/pairing.js +146 -0
- package/vendor/protocol/dist/pairing.js.map +1 -0
- package/vendor/protocol/package.json +17 -0
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.default = userflowAutoStamp;
|
|
37
|
+
const nodePath = __importStar(require("node:path"));
|
|
38
|
+
/**
|
|
39
|
+
* @userflow-tech/react-native/babel-plugin — semantic auto-stamp + auto-register (rn-v2).
|
|
40
|
+
*
|
|
41
|
+
* Stamps allowlisted host elements with:
|
|
42
|
+
* 1. Canonical structural path where explicit IDs replace ordinals
|
|
43
|
+
* 2. Compact hints wire (v1: n/p/ct/…) expanded at runtime
|
|
44
|
+
* 3. Runtime bag (dynamic ids/labels + inherited `__ufScope` when captured)
|
|
45
|
+
* 4. Call-site `__ufScope` on first-party custom components
|
|
46
|
+
*
|
|
47
|
+
* When `stampOnly` is set, only `nativeID` and `collapsable` are injected —
|
|
48
|
+
* no SDK import, no `__ufRef` helper, no ref wrapping, and no scope capture.
|
|
49
|
+
* That mode is enough for the SDK's fiber scanner to resolve stamped hosts.
|
|
50
|
+
*
|
|
51
|
+
* Existing app props are composed, never replaced: `ref` (callback, object,
|
|
52
|
+
* legacy string, or single `{...props}.ref`), `nativeID`, and `collapsable`
|
|
53
|
+
* are preserved when the developer already set them.
|
|
54
|
+
*/
|
|
55
|
+
const STRUCTURAL_PATH_MAX_DEPTH = 12;
|
|
56
|
+
const DEFAULT_ALLOWLIST = [
|
|
57
|
+
'Pressable',
|
|
58
|
+
'TouchableOpacity',
|
|
59
|
+
'TouchableHighlight',
|
|
60
|
+
'TouchableWithoutFeedback',
|
|
61
|
+
'TouchableNativeFeedback',
|
|
62
|
+
'View',
|
|
63
|
+
'Text',
|
|
64
|
+
'Image',
|
|
65
|
+
];
|
|
66
|
+
const COLLAPSABLE_NAMES = new Set([
|
|
67
|
+
'Pressable',
|
|
68
|
+
'TouchableOpacity',
|
|
69
|
+
'TouchableHighlight',
|
|
70
|
+
'TouchableWithoutFeedback',
|
|
71
|
+
'TouchableNativeFeedback',
|
|
72
|
+
'View',
|
|
73
|
+
'Text',
|
|
74
|
+
]);
|
|
75
|
+
const MANUAL_ID_ATTRS = ['userflowId', 'testID', 'nativeID'];
|
|
76
|
+
const REF_HELPER = '__ufRef';
|
|
77
|
+
const UFID_PROP = '__ufid';
|
|
78
|
+
const UF_MANUAL_PROP = '__ufManual';
|
|
79
|
+
const UF_SCOPE_PROP = '__ufScope';
|
|
80
|
+
function hashId(input) {
|
|
81
|
+
let h = 5381;
|
|
82
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
83
|
+
h = (h * 33) ^ input.charCodeAt(i);
|
|
84
|
+
}
|
|
85
|
+
return (h >>> 0).toString(36);
|
|
86
|
+
}
|
|
87
|
+
function stringLiteralFromJsxAttr(t, attr) {
|
|
88
|
+
if (!attr?.value)
|
|
89
|
+
return undefined;
|
|
90
|
+
if (t.isStringLiteral(attr.value))
|
|
91
|
+
return attr.value.value;
|
|
92
|
+
if (t.isJSXExpressionContainer(attr.value) && t.isStringLiteral(attr.value.expression)) {
|
|
93
|
+
return attr.value.expression.value;
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
function expressionFromJsxAttr(t, attr) {
|
|
98
|
+
if (!attr?.value)
|
|
99
|
+
return undefined;
|
|
100
|
+
if (t.isStringLiteral(attr.value))
|
|
101
|
+
return t.stringLiteral(attr.value.value);
|
|
102
|
+
if (t.isJSXExpressionContainer(attr.value) && t.isExpression(attr.value.expression)) {
|
|
103
|
+
return t.cloneNode(attr.value.expression);
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
function firstStaticStringInJsx(t, node, depth = 0) {
|
|
108
|
+
if (!node || depth > 4)
|
|
109
|
+
return undefined;
|
|
110
|
+
if (t.isStringLiteral(node)) {
|
|
111
|
+
const s = node.value.trim();
|
|
112
|
+
return s || undefined;
|
|
113
|
+
}
|
|
114
|
+
if (t.isJSXText(node)) {
|
|
115
|
+
const s = node.value.replace(/\s+/g, ' ').trim();
|
|
116
|
+
return s || undefined;
|
|
117
|
+
}
|
|
118
|
+
if (t.isJSXExpressionContainer(node) && t.isStringLiteral(node.expression)) {
|
|
119
|
+
const s = node.expression.value.trim();
|
|
120
|
+
return s || undefined;
|
|
121
|
+
}
|
|
122
|
+
if (t.isJSXFragment(node)) {
|
|
123
|
+
for (const child of node.children) {
|
|
124
|
+
const found = firstStaticStringInJsx(t, child, depth + 1);
|
|
125
|
+
if (found)
|
|
126
|
+
return found;
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
if (t.isJSXElement(node)) {
|
|
131
|
+
for (const child of node.children) {
|
|
132
|
+
if (t.isJSXElement(child) || t.isJSXText(child) || t.isJSXExpressionContainer(child)) {
|
|
133
|
+
const found = firstStaticStringInJsx(t, child, depth + 1);
|
|
134
|
+
if (found)
|
|
135
|
+
return found;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
function jsxElementTypeName(t, el) {
|
|
142
|
+
const n = el.openingElement.name;
|
|
143
|
+
return t.isJSXIdentifier(n) ? n.name : undefined;
|
|
144
|
+
}
|
|
145
|
+
function directChildShape(t, el) {
|
|
146
|
+
const name = jsxElementTypeName(t, el);
|
|
147
|
+
if (!name)
|
|
148
|
+
return undefined;
|
|
149
|
+
const nested = el.children.filter((c) => t.isJSXElement(c));
|
|
150
|
+
const onlyChild = nested.length === 1 ? nested[0] : undefined;
|
|
151
|
+
if (onlyChild) {
|
|
152
|
+
const inner = jsxElementTypeName(t, onlyChild);
|
|
153
|
+
if (inner)
|
|
154
|
+
return `${name}>${inner}`;
|
|
155
|
+
}
|
|
156
|
+
return name;
|
|
157
|
+
}
|
|
158
|
+
function childShapeFromJsx(t, openingPath) {
|
|
159
|
+
const parent = openingPath.parentPath;
|
|
160
|
+
if (!parent?.isJSXElement())
|
|
161
|
+
return undefined;
|
|
162
|
+
const segments = [];
|
|
163
|
+
for (const child of parent.node.children) {
|
|
164
|
+
if (!t.isJSXElement(child))
|
|
165
|
+
continue;
|
|
166
|
+
const seg = directChildShape(t, child);
|
|
167
|
+
if (seg)
|
|
168
|
+
segments.push(seg);
|
|
169
|
+
}
|
|
170
|
+
return segments.length > 0 ? segments.join(',') : undefined;
|
|
171
|
+
}
|
|
172
|
+
function childTextFromJsx(t, openingPath) {
|
|
173
|
+
const parent = openingPath.parentPath;
|
|
174
|
+
if (!parent?.isJSXElement())
|
|
175
|
+
return undefined;
|
|
176
|
+
return firstStaticStringInJsx(t, parent.node);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Static intrinsic identity signals — position-independent and present in a
|
|
180
|
+
* release Hermes bundle (no fiber walk, no `toString`). Read from the stamped
|
|
181
|
+
* element *and its subtree* because the real icon-button pattern nests the
|
|
182
|
+
* signal: `<Pressable><Icon name="settings" /></Pressable>` /
|
|
183
|
+
* `<Pressable><Image source={{ uri: '…' }} /></Pressable>` — mirroring how
|
|
184
|
+
* `childText` descends to find a label.
|
|
185
|
+
*/
|
|
186
|
+
const INTRINSIC_SCAN_MAX_DEPTH = 4;
|
|
187
|
+
/** Read a string-literal `name` prop off an opening element. */
|
|
188
|
+
function stringNameAttr(t, opening) {
|
|
189
|
+
const attr = opening.attributes.find((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === 'name');
|
|
190
|
+
return attr ? stringLiteralFromJsxAttr(t, attr) : undefined;
|
|
191
|
+
}
|
|
192
|
+
/** Read a literal image `source` uri (string or `{ uri: '…' }`) off an opening element. */
|
|
193
|
+
function imageSourceUri(t, opening) {
|
|
194
|
+
const attr = opening.attributes.find((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === 'source');
|
|
195
|
+
if (!attr)
|
|
196
|
+
return undefined;
|
|
197
|
+
const literal = stringLiteralFromJsxAttr(t, attr);
|
|
198
|
+
if (literal)
|
|
199
|
+
return literal;
|
|
200
|
+
const value = attr.value;
|
|
201
|
+
if (!value || !t.isJSXExpressionContainer(value) || !t.isObjectExpression(value.expression)) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
for (const prop of value.expression.properties) {
|
|
205
|
+
if (t.isObjectProperty(prop) &&
|
|
206
|
+
((t.isIdentifier(prop.key) && prop.key.name === 'uri') ||
|
|
207
|
+
(t.isStringLiteral(prop.key) && prop.key.value === 'uri')) &&
|
|
208
|
+
t.isStringLiteral(prop.value)) {
|
|
209
|
+
return prop.value.value;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
/** Depth-first search over the stamped element + its subtree for the first hit of `read`. */
|
|
215
|
+
function scanSubtree(t, openingPath, read) {
|
|
216
|
+
const root = openingPath.parentPath;
|
|
217
|
+
if (!root?.isJSXElement())
|
|
218
|
+
return undefined;
|
|
219
|
+
const visit = (el, depth) => {
|
|
220
|
+
const hit = read(el.openingElement);
|
|
221
|
+
if (hit)
|
|
222
|
+
return hit;
|
|
223
|
+
if (depth >= INTRINSIC_SCAN_MAX_DEPTH)
|
|
224
|
+
return undefined;
|
|
225
|
+
for (const child of el.children) {
|
|
226
|
+
if (t.isJSXElement(child)) {
|
|
227
|
+
const r = visit(child, depth + 1);
|
|
228
|
+
if (r)
|
|
229
|
+
return r;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
};
|
|
234
|
+
return visit(root.node, 0);
|
|
235
|
+
}
|
|
236
|
+
function staticIconName(t, openingPath) {
|
|
237
|
+
return scanSubtree(t, openingPath, (o) => stringNameAttr(t, o));
|
|
238
|
+
}
|
|
239
|
+
function staticImageUri(t, openingPath) {
|
|
240
|
+
return scanSubtree(t, openingPath, (o) => imageSourceUri(t, o));
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Normalize static text for use inside a canonical `{text=…}` segment: strip the
|
|
244
|
+
* path delimiters (` > `, `{`, `}`) and control chars, collapse whitespace, cap
|
|
245
|
+
* length. Deterministic so record-time and resolve-time paths agree.
|
|
246
|
+
*/
|
|
247
|
+
function pathText(raw) {
|
|
248
|
+
return raw
|
|
249
|
+
.replace(/[\r\n\t]+/g, ' ')
|
|
250
|
+
.replace(/[>{}]/g, ' ')
|
|
251
|
+
.replace(/\s+/g, ' ')
|
|
252
|
+
.trim()
|
|
253
|
+
.slice(0, 40);
|
|
254
|
+
}
|
|
255
|
+
/** Leaf kinds whose static text *is* their identity — safe to anchor on `{text=…}`. */
|
|
256
|
+
const TEXT_ANCHOR_KINDS = new Set([
|
|
257
|
+
'Pressable',
|
|
258
|
+
'TouchableOpacity',
|
|
259
|
+
'TouchableHighlight',
|
|
260
|
+
'TouchableWithoutFeedback',
|
|
261
|
+
'TouchableNativeFeedback',
|
|
262
|
+
'Text',
|
|
263
|
+
]);
|
|
264
|
+
/** Canonical segment: ID replaces ordinal when present; static text anchors the leaf. */
|
|
265
|
+
function formatSegment(s) {
|
|
266
|
+
if (s.userflowId)
|
|
267
|
+
return `${s.name}{userflowId=${s.userflowId}}`;
|
|
268
|
+
if (s.testID)
|
|
269
|
+
return `${s.name}{testID=${s.testID}}`;
|
|
270
|
+
if (s.nativeID && !s.nativeID.startsWith('uf_'))
|
|
271
|
+
return `${s.name}{nativeID=${s.nativeID}}`;
|
|
272
|
+
if (s.key)
|
|
273
|
+
return `${s.name}{key=${s.key}}`;
|
|
274
|
+
if (s.childText)
|
|
275
|
+
return `${s.name}{text=${s.childText}}`;
|
|
276
|
+
return `${s.name}#${s.ordinal}`;
|
|
277
|
+
}
|
|
278
|
+
function idsFromOpening(t, opening) {
|
|
279
|
+
const findAttr = (attrName) => opening.attributes.find((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === attrName);
|
|
280
|
+
const userflowId = stringLiteralFromJsxAttr(t, findAttr('userflowId'));
|
|
281
|
+
const testID = stringLiteralFromJsxAttr(t, findAttr('testID'));
|
|
282
|
+
let nativeID = stringLiteralFromJsxAttr(t, findAttr('nativeID'));
|
|
283
|
+
if (nativeID?.startsWith('uf_'))
|
|
284
|
+
nativeID = undefined;
|
|
285
|
+
const key = stringLiteralFromJsxAttr(t, findAttr('key'));
|
|
286
|
+
return {
|
|
287
|
+
...(userflowId ? { userflowId } : {}),
|
|
288
|
+
...(testID ? { testID } : {}),
|
|
289
|
+
...(nativeID ? { nativeID } : {}),
|
|
290
|
+
...(key ? { key } : {}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Parent-scoped ordinal: count same-named siblings under the immediate JSX parent.
|
|
295
|
+
*/
|
|
296
|
+
function parentScopedOrdinal(t, openingPath, elementName) {
|
|
297
|
+
const jsxEl = openingPath.parentPath;
|
|
298
|
+
if (!jsxEl?.isJSXElement())
|
|
299
|
+
return { ordinal: 0, siblingIndex: 0 };
|
|
300
|
+
const container = jsxEl.parentPath;
|
|
301
|
+
let children = [];
|
|
302
|
+
if (container?.isJSXElement()) {
|
|
303
|
+
children = container.node.children;
|
|
304
|
+
}
|
|
305
|
+
else if (container?.isJSXFragment()) {
|
|
306
|
+
children = container.node.children;
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
return { ordinal: 0, siblingIndex: 0 };
|
|
310
|
+
}
|
|
311
|
+
let ordinal = 0;
|
|
312
|
+
let siblingIndex = 0;
|
|
313
|
+
let seen = 0;
|
|
314
|
+
for (const child of children) {
|
|
315
|
+
if (!t.isJSXElement(child))
|
|
316
|
+
continue;
|
|
317
|
+
if (child.openingElement === openingPath.node) {
|
|
318
|
+
siblingIndex = seen;
|
|
319
|
+
return { ordinal, siblingIndex };
|
|
320
|
+
}
|
|
321
|
+
const childName = jsxElementTypeName(t, child);
|
|
322
|
+
if (childName === elementName)
|
|
323
|
+
ordinal += 1;
|
|
324
|
+
seen += 1;
|
|
325
|
+
}
|
|
326
|
+
return { ordinal: 0, siblingIndex: 0 };
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Host allowlist elements always participate in the path. First-party custom
|
|
330
|
+
* components (PascalCase) also participate so siblings under different
|
|
331
|
+
* `<Section>` / layout wrappers do not collapse to the same `uf_<hash>`.
|
|
332
|
+
* Skipping them caused AnchorRegistry cache collisions: only one of several
|
|
333
|
+
* same-path Pressables stayed registered, and taps fell through to the parent View.
|
|
334
|
+
*/
|
|
335
|
+
function shouldIncludeInSemanticPath(name, allowlist) {
|
|
336
|
+
if (allowlist.includes(name))
|
|
337
|
+
return true;
|
|
338
|
+
// PascalCase / uppercase → custom component (same rule as call-site scope).
|
|
339
|
+
return /^[A-Z]/.test(name);
|
|
340
|
+
}
|
|
341
|
+
function buildSemanticPath(t, openingPath, moduleRel, component, allowlist, ordinals,
|
|
342
|
+
/**
|
|
343
|
+
* When false, leaf segments stay on ordinals even if static child text is
|
|
344
|
+
* present. stampOnly mode needs this: without a scored matcher, two "OK"
|
|
345
|
+
* buttons under the same parent would otherwise share one `uf_` hash and
|
|
346
|
+
* resolve as ambiguous.
|
|
347
|
+
*/
|
|
348
|
+
allowTextAnchor = true) {
|
|
349
|
+
const chain = [];
|
|
350
|
+
let cur = openingPath;
|
|
351
|
+
let depth = 0;
|
|
352
|
+
while (cur && depth < STRUCTURAL_PATH_MAX_DEPTH) {
|
|
353
|
+
depth += 1;
|
|
354
|
+
const nameNode = cur.node.name;
|
|
355
|
+
if (t.isJSXIdentifier(nameNode) && shouldIncludeInSemanticPath(nameNode.name, allowlist)) {
|
|
356
|
+
// Prefer the pristine-tree ordinal; fall back to a live scan when absent
|
|
357
|
+
// (e.g. a synthetic node the pre-pass never saw).
|
|
358
|
+
const { ordinal, siblingIndex } = ordinals?.get(cur.node) ?? parentScopedOrdinal(t, cur, nameNode.name);
|
|
359
|
+
const ids = idsFromOpening(t, cur.node);
|
|
360
|
+
// Anchor the leaf (first pushed = innermost) to its static text so its
|
|
361
|
+
// segment survives sibling inserts/reorders. Only for content leaves
|
|
362
|
+
// (pressables / Text) whose text *is* their identity — not containers,
|
|
363
|
+
// where `firstStaticStringInJsx` would greedily grab a descendant's label,
|
|
364
|
+
// nor ancestors (kept on ordinals; WS3 tolerates ancestor ordinal shifts).
|
|
365
|
+
const isLeaf = chain.length === 0;
|
|
366
|
+
let childText;
|
|
367
|
+
if (allowTextAnchor &&
|
|
368
|
+
isLeaf &&
|
|
369
|
+
TEXT_ANCHOR_KINDS.has(nameNode.name) &&
|
|
370
|
+
!ids.userflowId &&
|
|
371
|
+
!ids.testID &&
|
|
372
|
+
!ids.nativeID &&
|
|
373
|
+
!ids.key) {
|
|
374
|
+
const raw = childTextFromJsx(t, cur);
|
|
375
|
+
const cleaned = raw ? pathText(raw) : '';
|
|
376
|
+
if (cleaned)
|
|
377
|
+
childText = cleaned;
|
|
378
|
+
}
|
|
379
|
+
chain.push({
|
|
380
|
+
name: nameNode.name,
|
|
381
|
+
ordinal,
|
|
382
|
+
siblingIndex,
|
|
383
|
+
...ids,
|
|
384
|
+
...(childText ? { childText } : {}),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
const jsxEl = cur.parentPath;
|
|
388
|
+
if (!jsxEl?.isJSXElement())
|
|
389
|
+
break;
|
|
390
|
+
const parent = jsxEl.parentPath;
|
|
391
|
+
if (parent?.isJSXElement()) {
|
|
392
|
+
cur = parent.get('openingElement');
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
chain.reverse();
|
|
399
|
+
const self = chain[chain.length - 1];
|
|
400
|
+
const ancestors = chain.slice(0, -1);
|
|
401
|
+
const segments = chain.map(formatSegment);
|
|
402
|
+
const path = segments.length === 0
|
|
403
|
+
? `${moduleRel}::${component}`
|
|
404
|
+
: `${moduleRel}::${component} > ${segments.join(' > ')}`;
|
|
405
|
+
return { path, ancestors, self };
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Compact hints wire format (v1). Runtime expands via `expandBuildTimeHints`.
|
|
409
|
+
* Single path `p` replaces ufid/structuralPath/templatePath; role/ancestors
|
|
410
|
+
* are derived at expand time from `kind` + path.
|
|
411
|
+
*/
|
|
412
|
+
function buildCompactHintsObjectExpression(t, fields) {
|
|
413
|
+
const props = [
|
|
414
|
+
t.objectProperty(t.identifier('v'), t.numericLiteral(1)),
|
|
415
|
+
t.objectProperty(t.identifier('n'), t.stringLiteral(fields.nativeId)),
|
|
416
|
+
t.objectProperty(t.identifier('p'), t.stringLiteral(fields.path)),
|
|
417
|
+
];
|
|
418
|
+
if (fields.childText) {
|
|
419
|
+
props.push(t.objectProperty(t.identifier('ct'), t.stringLiteral(fields.childText)));
|
|
420
|
+
}
|
|
421
|
+
if (fields.childShape) {
|
|
422
|
+
props.push(t.objectProperty(t.identifier('cs'), t.stringLiteral(fields.childShape)));
|
|
423
|
+
}
|
|
424
|
+
if (fields.iconName) {
|
|
425
|
+
props.push(t.objectProperty(t.identifier('icn'), t.stringLiteral(fields.iconName)));
|
|
426
|
+
}
|
|
427
|
+
if (fields.imageSrc) {
|
|
428
|
+
props.push(t.objectProperty(t.identifier('img'), t.stringLiteral(fields.imageSrc)));
|
|
429
|
+
}
|
|
430
|
+
if (fields.ordinal !== undefined && fields.ordinal !== 0) {
|
|
431
|
+
props.push(t.objectProperty(t.identifier('o'), t.numericLiteral(fields.ordinal)));
|
|
432
|
+
}
|
|
433
|
+
if (fields.siblingIndex !== undefined && fields.siblingIndex !== 0) {
|
|
434
|
+
props.push(t.objectProperty(t.identifier('si'), t.numericLiteral(fields.siblingIndex)));
|
|
435
|
+
}
|
|
436
|
+
if (fields.literalTestId) {
|
|
437
|
+
props.push(t.objectProperty(t.identifier('tid'), t.stringLiteral(fields.literalTestId)));
|
|
438
|
+
}
|
|
439
|
+
if (fields.literalUserflowId) {
|
|
440
|
+
props.push(t.objectProperty(t.identifier('uid'), t.stringLiteral(fields.literalUserflowId)));
|
|
441
|
+
}
|
|
442
|
+
if (fields.literalLabel) {
|
|
443
|
+
props.push(t.objectProperty(t.identifier('lid'), t.stringLiteral(fields.literalLabel)));
|
|
444
|
+
}
|
|
445
|
+
return t.objectExpression(props);
|
|
446
|
+
}
|
|
447
|
+
function functionDeclaresIncomingScope(t, fnPath) {
|
|
448
|
+
if (!fnPath)
|
|
449
|
+
return false;
|
|
450
|
+
const body = fnPath.get('body');
|
|
451
|
+
if (!body.isBlockStatement())
|
|
452
|
+
return false;
|
|
453
|
+
return body.node.body.some((s) => t.isVariableDeclaration(s) &&
|
|
454
|
+
s.declarations.some((d) => t.isIdentifier(d.id) && d.id.name === '__ufIncomingScope'));
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Runtime bag passed to __ufRef. Only emits keys that exist; `scope` only when
|
|
458
|
+
* the enclosing function captured `__ufIncomingScope`.
|
|
459
|
+
*/
|
|
460
|
+
function buildRuntimeExpr(t, findAttr, includeScope) {
|
|
461
|
+
const props = [];
|
|
462
|
+
const pushAttr = (runtimeKey, attrName) => {
|
|
463
|
+
const expr = expressionFromJsxAttr(t, findAttr(attrName));
|
|
464
|
+
if (!expr)
|
|
465
|
+
return;
|
|
466
|
+
if (attrName === 'nativeID' && t.isStringLiteral(expr) && expr.value.startsWith('uf_'))
|
|
467
|
+
return;
|
|
468
|
+
props.push(t.objectProperty(t.identifier(runtimeKey), expr));
|
|
469
|
+
};
|
|
470
|
+
pushAttr('userflowId', 'userflowId');
|
|
471
|
+
pushAttr('testID', 'testID');
|
|
472
|
+
pushAttr('nativeID', 'nativeID');
|
|
473
|
+
pushAttr('accessibilityLabel', 'accessibilityLabel');
|
|
474
|
+
// Dynamic intrinsic identity: icon name (`<Icon name={x}>`) survives reorders.
|
|
475
|
+
pushAttr('iconName', 'name');
|
|
476
|
+
if (includeScope) {
|
|
477
|
+
props.push(t.objectProperty(t.identifier('scope'), t.conditionalExpression(t.binaryExpression('!==', t.unaryExpression('typeof', t.identifier('__ufIncomingScope')), t.stringLiteral('undefined')), t.identifier('__ufIncomingScope'), t.identifier('undefined'))));
|
|
478
|
+
}
|
|
479
|
+
return t.objectExpression(props);
|
|
480
|
+
}
|
|
481
|
+
function enclosingComponentName(t, openingPath, filename) {
|
|
482
|
+
let fn = openingPath.getFunctionParent();
|
|
483
|
+
let sawDefaultExport = false;
|
|
484
|
+
while (fn) {
|
|
485
|
+
const node = fn.node;
|
|
486
|
+
if ((t.isFunctionDeclaration(node) || t.isFunctionExpression(node)) && node.id?.name) {
|
|
487
|
+
return node.id.name;
|
|
488
|
+
}
|
|
489
|
+
if (t.isClassMethod(node) || t.isClassPrivateMethod(node)) {
|
|
490
|
+
const cls = fn.findParent((p) => t.isClassDeclaration(p.node) || t.isClassExpression(p.node));
|
|
491
|
+
const clsNode = cls?.node;
|
|
492
|
+
if (clsNode?.id?.name)
|
|
493
|
+
return clsNode.id.name;
|
|
494
|
+
}
|
|
495
|
+
if (t.isObjectMethod(node) && t.isIdentifier(node.key)) {
|
|
496
|
+
return node.key.name;
|
|
497
|
+
}
|
|
498
|
+
const declarator = fn.findParent((p) => t.isVariableDeclarator(p.node));
|
|
499
|
+
const declId = declarator?.node?.id;
|
|
500
|
+
if (declId && t.isIdentifier(declId)) {
|
|
501
|
+
return declId.name;
|
|
502
|
+
}
|
|
503
|
+
if (t.isExportDefaultDeclaration(fn.parentPath?.node)) {
|
|
504
|
+
sawDefaultExport = true;
|
|
505
|
+
}
|
|
506
|
+
fn = fn.getFunctionParent();
|
|
507
|
+
}
|
|
508
|
+
if (sawDefaultExport)
|
|
509
|
+
return 'default';
|
|
510
|
+
const base = nodePath.basename(filename);
|
|
511
|
+
return base.replace(/\.[^.]+$/, '') || 'Module';
|
|
512
|
+
}
|
|
513
|
+
function isCustomComponentName(name, allowlist) {
|
|
514
|
+
if (allowlist.includes(name))
|
|
515
|
+
return false;
|
|
516
|
+
// PascalCase / uppercase start → custom component
|
|
517
|
+
return /^[A-Z]/.test(name);
|
|
518
|
+
}
|
|
519
|
+
function formatCallSiteScope(moduleRel, component, childName, ordinal, ids) {
|
|
520
|
+
let seg = childName;
|
|
521
|
+
if (ids.userflowId)
|
|
522
|
+
seg = `${childName}{userflowId=${ids.userflowId}}`;
|
|
523
|
+
else if (ids.testID)
|
|
524
|
+
seg = `${childName}{testID=${ids.testID}}`;
|
|
525
|
+
else if (ids.key)
|
|
526
|
+
seg = `${childName}{key=${ids.key}}`;
|
|
527
|
+
else
|
|
528
|
+
seg = `${childName}#${ordinal}`;
|
|
529
|
+
return `${moduleRel}::${component} > ${seg}`;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Inject `const __ufIncomingScope = props?.__ufScope` at the start of a
|
|
533
|
+
* function component that has a `props` param (or first param).
|
|
534
|
+
*/
|
|
535
|
+
function ensureScopeCapture(t, fnPath) {
|
|
536
|
+
const body = fnPath.get('body');
|
|
537
|
+
if (!body.isBlockStatement())
|
|
538
|
+
return;
|
|
539
|
+
const already = body.node.body.some((s) => t.isVariableDeclaration(s) &&
|
|
540
|
+
s.declarations.some((d) => t.isIdentifier(d.id) && d.id.name === '__ufIncomingScope'));
|
|
541
|
+
if (already)
|
|
542
|
+
return;
|
|
543
|
+
const params = fnPath.node.params;
|
|
544
|
+
if (params.length === 0)
|
|
545
|
+
return;
|
|
546
|
+
const first = params[0];
|
|
547
|
+
let propsAccess;
|
|
548
|
+
if (t.isIdentifier(first)) {
|
|
549
|
+
propsAccess = t.memberExpression(first, t.identifier(UF_SCOPE_PROP));
|
|
550
|
+
}
|
|
551
|
+
else if (t.isObjectPattern(first)) {
|
|
552
|
+
// Look for __ufScope in pattern or add member via rest — skip if complex
|
|
553
|
+
const hasScope = first.properties.some((p) => t.isObjectProperty(p) &&
|
|
554
|
+
((t.isIdentifier(p.key) && p.key.name === UF_SCOPE_PROP) ||
|
|
555
|
+
(t.isStringLiteral(p.key) && p.key.value === UF_SCOPE_PROP)));
|
|
556
|
+
if (hasScope) {
|
|
557
|
+
propsAccess = t.identifier(UF_SCOPE_PROP);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
// Inject __ufScope into the pattern
|
|
561
|
+
first.properties.unshift(t.objectProperty(t.identifier(UF_SCOPE_PROP), t.identifier('__ufScopeProp'), false, true));
|
|
562
|
+
propsAccess = t.identifier('__ufScopeProp');
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
body.unshiftContainer('body', t.variableDeclaration('var', [
|
|
569
|
+
t.variableDeclarator(t.identifier('__ufIncomingScope'), propsAccess),
|
|
570
|
+
]));
|
|
571
|
+
}
|
|
572
|
+
function userflowAutoStamp(api) {
|
|
573
|
+
const t = api.types;
|
|
574
|
+
const isProduction = (typeof api.env === 'function' && api.env('production')) ||
|
|
575
|
+
process.env['NODE_ENV'] === 'production';
|
|
576
|
+
return {
|
|
577
|
+
name: '@userflow-tech/react-native/babel-plugin',
|
|
578
|
+
pre() {
|
|
579
|
+
this.ufCallSiteCounters = new Map();
|
|
580
|
+
},
|
|
581
|
+
visitor: {
|
|
582
|
+
Program: {
|
|
583
|
+
enter(programPath, state) {
|
|
584
|
+
// Precompute parent-scoped ordinals on the still-pristine JSX tree.
|
|
585
|
+
// Running here (plugin Program.enter fires before preset-react) means
|
|
586
|
+
// no `<El>` has been rewritten to `_jsx(El, …)` yet, so sibling counts
|
|
587
|
+
// are accurate. Reading them lazily in the main visitor would instead
|
|
588
|
+
// race the JSX transform and mis-count every element as `#0`.
|
|
589
|
+
const ordinals = (state.ufOrdinals = new WeakMap());
|
|
590
|
+
programPath.traverse({
|
|
591
|
+
JSXOpeningElement(p) {
|
|
592
|
+
const nameNode = p.node.name;
|
|
593
|
+
if (!t.isJSXIdentifier(nameNode))
|
|
594
|
+
return;
|
|
595
|
+
ordinals.set(p.node, parentScopedOrdinal(t, p, nameNode.name));
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
},
|
|
599
|
+
exit(programPath, state) {
|
|
600
|
+
if (!state.ufStamped)
|
|
601
|
+
return;
|
|
602
|
+
const opts = (state.opts ?? {});
|
|
603
|
+
// stampOnly (default) never wraps refs, so there is nothing to import or helper.
|
|
604
|
+
if (opts.stampOnly !== false)
|
|
605
|
+
return;
|
|
606
|
+
// Bind the runtime installer into every stamped module so production
|
|
607
|
+
// tree-shaking cannot drop `globalThis.__userflowAutoAnchor` when the
|
|
608
|
+
// app never names the export. Falls back to globalThis for tests /
|
|
609
|
+
// older SDK versions.
|
|
610
|
+
const importName = '__userflowAutoAnchor';
|
|
611
|
+
const packageName = '@userflow-tech/react-native';
|
|
612
|
+
let hasImport = false;
|
|
613
|
+
for (const stmt of programPath.node.body) {
|
|
614
|
+
if (!t.isImportDeclaration(stmt))
|
|
615
|
+
continue;
|
|
616
|
+
if (stmt.source.value !== packageName)
|
|
617
|
+
continue;
|
|
618
|
+
const already = stmt.specifiers.some((s) => t.isImportSpecifier(s) &&
|
|
619
|
+
t.isIdentifier(s.imported) &&
|
|
620
|
+
s.imported.name === importName);
|
|
621
|
+
if (!already) {
|
|
622
|
+
stmt.specifiers.push(t.importSpecifier(t.identifier(importName), t.identifier(importName)));
|
|
623
|
+
}
|
|
624
|
+
hasImport = true;
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
if (!hasImport) {
|
|
628
|
+
programPath.unshiftContainer('body', t.importDeclaration([t.importSpecifier(t.identifier(importName), t.identifier(importName))], t.stringLiteral(packageName)));
|
|
629
|
+
}
|
|
630
|
+
const helper = api.template.statement(`
|
|
631
|
+
function ${REF_HELPER}(id, hints, userRef, kind, runtime) {
|
|
632
|
+
var f = typeof ${importName} === "function"
|
|
633
|
+
? ${importName}
|
|
634
|
+
: globalThis.__userflowAutoAnchor;
|
|
635
|
+
var resolved = (id != null && id !== "") ? id : (hints && (hints.n || hints.nativeId));
|
|
636
|
+
return f ? f(resolved, hints, userRef, kind, runtime) : userRef;
|
|
637
|
+
}
|
|
638
|
+
`)();
|
|
639
|
+
// Place helper after imports (unshift would put it above the import).
|
|
640
|
+
const body = programPath.get('body');
|
|
641
|
+
let insertAt = 0;
|
|
642
|
+
for (let i = 0; i < body.length; i += 1) {
|
|
643
|
+
if (body[i].isImportDeclaration())
|
|
644
|
+
insertAt = i + 1;
|
|
645
|
+
else
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
if (insertAt < body.length) {
|
|
649
|
+
body[insertAt].insertBefore(helper);
|
|
650
|
+
}
|
|
651
|
+
else if (insertAt > 0) {
|
|
652
|
+
body[insertAt - 1].insertAfter(helper);
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
programPath.unshiftContainer('body', helper);
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
Function: {
|
|
660
|
+
enter(fnPath, state) {
|
|
661
|
+
const opts = (state.opts ?? {});
|
|
662
|
+
if (opts.stampOnly !== false)
|
|
663
|
+
return;
|
|
664
|
+
const filename = state.filename ?? state.file.opts.filename ?? '';
|
|
665
|
+
if (!filename || filename.includes('node_modules'))
|
|
666
|
+
return;
|
|
667
|
+
if (opts.exclude?.some((frag) => filename.includes(frag)))
|
|
668
|
+
return;
|
|
669
|
+
// Do not pollute the injected `__ufRef` helper (Program.exit re-visits it).
|
|
670
|
+
if ((fnPath.isFunctionDeclaration() || fnPath.isFunctionExpression()) &&
|
|
671
|
+
t.isIdentifier(fnPath.node.id) &&
|
|
672
|
+
fnPath.node.id.name === REF_HELPER) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
// Capture incoming scope for any function that may stamp hosts.
|
|
676
|
+
ensureScopeCapture(t, fnPath);
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
JSXOpeningElement(openingPath, state) {
|
|
680
|
+
const opts = (state.opts ?? {});
|
|
681
|
+
const nameNode = openingPath.node.name;
|
|
682
|
+
if (!t.isJSXIdentifier(nameNode))
|
|
683
|
+
return;
|
|
684
|
+
const name = nameNode.name;
|
|
685
|
+
const allowlist = opts.allowlist ?? DEFAULT_ALLOWLIST;
|
|
686
|
+
// Default true — full mode is unsafe until __userflowAutoAnchor ships.
|
|
687
|
+
const stampOnly = opts.stampOnly !== false;
|
|
688
|
+
const filename = state.filename ?? state.file.opts.filename ?? '';
|
|
689
|
+
if (!filename || filename.includes('node_modules'))
|
|
690
|
+
return;
|
|
691
|
+
if (opts.exclude?.some((frag) => filename.includes(frag)))
|
|
692
|
+
return;
|
|
693
|
+
const rootDir = opts.rootDir ?? state.cwd ?? process.cwd();
|
|
694
|
+
const rel = nodePath.relative(rootDir, filename).split(nodePath.sep).join('/') || filename;
|
|
695
|
+
const component = enclosingComponentName(t, openingPath, filename);
|
|
696
|
+
const attrs = openingPath.node.attributes;
|
|
697
|
+
const findAttr = (attrName) => attrs.find((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === attrName);
|
|
698
|
+
// --- Custom component call-site scope ---
|
|
699
|
+
// stampOnly has no runtime scope consumer, so skip this entirely.
|
|
700
|
+
if (isCustomComponentName(name, allowlist)) {
|
|
701
|
+
if (stampOnly)
|
|
702
|
+
return;
|
|
703
|
+
if (findAttr(UF_SCOPE_PROP))
|
|
704
|
+
return;
|
|
705
|
+
const counters = (state.ufCallSiteCounters ?? (state.ufCallSiteCounters = new Map()));
|
|
706
|
+
let byComp = counters.get(component);
|
|
707
|
+
if (!byComp) {
|
|
708
|
+
byComp = new Map();
|
|
709
|
+
counters.set(component, byComp);
|
|
710
|
+
}
|
|
711
|
+
const ordinal = byComp.get(name) ?? 0;
|
|
712
|
+
byComp.set(name, ordinal + 1);
|
|
713
|
+
const ufId = stringLiteralFromJsxAttr(t, findAttr('userflowId'));
|
|
714
|
+
const tId = stringLiteralFromJsxAttr(t, findAttr('testID'));
|
|
715
|
+
const kId = stringLiteralFromJsxAttr(t, findAttr('key'));
|
|
716
|
+
const ids = {
|
|
717
|
+
...(ufId ? { userflowId: ufId } : {}),
|
|
718
|
+
...(tId ? { testID: tId } : {}),
|
|
719
|
+
...(kId ? { key: kId } : {}),
|
|
720
|
+
};
|
|
721
|
+
const scope = formatCallSiteScope(rel, component, name, ordinal, ids);
|
|
722
|
+
attrs.push(t.jsxAttribute(t.jsxIdentifier(UF_SCOPE_PROP), t.stringLiteral(scope)));
|
|
723
|
+
state.ufStamped = true;
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (!allowlist.includes(name))
|
|
727
|
+
return;
|
|
728
|
+
// Idempotency
|
|
729
|
+
if (findAttr(UFID_PROP))
|
|
730
|
+
return;
|
|
731
|
+
if (stampOnly && findAttr('nativeID')) {
|
|
732
|
+
const existing = stringLiteralFromJsxAttr(t, findAttr('nativeID'));
|
|
733
|
+
if (existing?.startsWith('uf_'))
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const { path: structuralPath, self } = buildSemanticPath(t, openingPath, rel, component, allowlist, state.ufOrdinals,
|
|
737
|
+
// stampOnly has no scored matcher — keep leaf ordinals unique even when
|
|
738
|
+
// sibling hosts share the same static label.
|
|
739
|
+
!stampOnly);
|
|
740
|
+
const hashedKey = `uf_${hashId(structuralPath)}`;
|
|
741
|
+
let manualIdExpr = null;
|
|
742
|
+
for (const attrName of MANUAL_ID_ATTRS) {
|
|
743
|
+
const attr = findAttr(attrName);
|
|
744
|
+
if (!attr)
|
|
745
|
+
continue;
|
|
746
|
+
const value = attr.value;
|
|
747
|
+
if (t.isStringLiteral(value)) {
|
|
748
|
+
if (attrName === 'nativeID' && value.value.startsWith('uf_'))
|
|
749
|
+
continue;
|
|
750
|
+
manualIdExpr = t.stringLiteral(value.value);
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
if (t.isJSXExpressionContainer(value) && t.isExpression(value.expression)) {
|
|
754
|
+
manualIdExpr = t.cloneNode(value.expression);
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (COLLAPSABLE_NAMES.has(name) && !findAttr('collapsable')) {
|
|
759
|
+
attrs.push(t.jsxAttribute(t.jsxIdentifier('collapsable'), t.jsxExpressionContainer(t.booleanLiteral(false))));
|
|
760
|
+
}
|
|
761
|
+
if (stampOnly) {
|
|
762
|
+
if (!manualIdExpr && !findAttr('nativeID')) {
|
|
763
|
+
attrs.push(t.jsxAttribute(t.jsxIdentifier('nativeID'), t.stringLiteral(hashedKey)));
|
|
764
|
+
}
|
|
765
|
+
state.ufStamped = true;
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const literalTestId = stringLiteralFromJsxAttr(t, findAttr('testID'));
|
|
769
|
+
const literalLabel = stringLiteralFromJsxAttr(t, findAttr('accessibilityLabel'));
|
|
770
|
+
const literalUserflowId = stringLiteralFromJsxAttr(t, findAttr('userflowId'));
|
|
771
|
+
const childText = childTextFromJsx(t, openingPath);
|
|
772
|
+
const childShape = childShapeFromJsx(t, openingPath);
|
|
773
|
+
const iconName = staticIconName(t, openingPath);
|
|
774
|
+
const imageSrc = staticImageUri(t, openingPath);
|
|
775
|
+
const hintsExpr = buildCompactHintsObjectExpression(t, {
|
|
776
|
+
nativeId: hashedKey,
|
|
777
|
+
path: structuralPath,
|
|
778
|
+
...(literalTestId ? { literalTestId } : {}),
|
|
779
|
+
...(literalLabel ? { literalLabel } : {}),
|
|
780
|
+
...(literalUserflowId ? { literalUserflowId } : {}),
|
|
781
|
+
...(childText ? { childText } : {}),
|
|
782
|
+
...(childShape ? { childShape } : {}),
|
|
783
|
+
...(iconName ? { iconName } : {}),
|
|
784
|
+
...(imageSrc ? { imageSrc } : {}),
|
|
785
|
+
ordinal: self.ordinal,
|
|
786
|
+
siblingIndex: self.siblingIndex,
|
|
787
|
+
});
|
|
788
|
+
// Dev: inject __ufid for fiber/debug. Production default: omit (bundle size).
|
|
789
|
+
const injectUfid = opts.injectUfidProp !== undefined ? opts.injectUfidProp : !isProduction;
|
|
790
|
+
if (injectUfid) {
|
|
791
|
+
attrs.push(t.jsxAttribute(t.jsxIdentifier(UFID_PROP), t.stringLiteral(structuralPath)));
|
|
792
|
+
}
|
|
793
|
+
if (manualIdExpr) {
|
|
794
|
+
attrs.push(t.jsxAttribute(t.jsxIdentifier(UF_MANUAL_PROP), t.stringLiteral('true')));
|
|
795
|
+
}
|
|
796
|
+
// Compose with whatever ref the app already has — never drop it.
|
|
797
|
+
// Supports: ref={expr}, ref="legacyString", and a single {...props}.ref.
|
|
798
|
+
const existingRefIdx = attrs.findIndex((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === 'ref');
|
|
799
|
+
let userRefExpr = t.nullLiteral();
|
|
800
|
+
if (existingRefIdx >= 0) {
|
|
801
|
+
const existing = attrs[existingRefIdx];
|
|
802
|
+
if (t.isStringLiteral(existing.value)) {
|
|
803
|
+
// Legacy string refs: pass through so runtime can assign owner.refs[name].
|
|
804
|
+
userRefExpr = t.stringLiteral(existing.value.value);
|
|
805
|
+
}
|
|
806
|
+
else if (t.isJSXExpressionContainer(existing.value)) {
|
|
807
|
+
const inner = existing.value.expression;
|
|
808
|
+
if (t.isExpression(inner)) {
|
|
809
|
+
if (t.isCallExpression(inner) &&
|
|
810
|
+
t.isIdentifier(inner.callee) &&
|
|
811
|
+
inner.callee.name === REF_HELPER) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
userRefExpr = inner;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
// No explicit ref attribute — if the element only has one object spread
|
|
820
|
+
// (e.g. {...props}), compose with that object's `.ref` so we don't
|
|
821
|
+
// clobber a ref that arrives via spread (JSX later-attrs-win).
|
|
822
|
+
const spreads = attrs.filter((a) => t.isJSXSpreadAttribute(a));
|
|
823
|
+
if (spreads.length === 1 && t.isIdentifier(spreads[0].argument)) {
|
|
824
|
+
userRefExpr = t.memberExpression(spreads[0].argument, t.identifier('ref'));
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const fnParent = openingPath.getFunctionParent();
|
|
828
|
+
const includeScope = functionDeclaresIncomingScope(t, fnParent);
|
|
829
|
+
const runtimeExpr = buildRuntimeExpr(t, findAttr, includeScope);
|
|
830
|
+
const refCall = t.callExpression(t.identifier(REF_HELPER), [
|
|
831
|
+
manualIdExpr ?? t.stringLiteral(hashedKey),
|
|
832
|
+
hintsExpr,
|
|
833
|
+
userRefExpr,
|
|
834
|
+
t.stringLiteral(name),
|
|
835
|
+
runtimeExpr,
|
|
836
|
+
]);
|
|
837
|
+
const refAttr = t.jsxAttribute(t.jsxIdentifier('ref'), t.jsxExpressionContainer(refCall));
|
|
838
|
+
if (existingRefIdx >= 0) {
|
|
839
|
+
attrs[existingRefIdx] = refAttr;
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
attrs.push(refAttr);
|
|
843
|
+
}
|
|
844
|
+
if (!manualIdExpr && !findAttr('nativeID')) {
|
|
845
|
+
attrs.push(t.jsxAttribute(t.jsxIdentifier('nativeID'), t.stringLiteral(hashedKey)));
|
|
846
|
+
}
|
|
847
|
+
state.ufStamped = true;
|
|
848
|
+
},
|
|
849
|
+
},
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
module.exports = userflowAutoStamp;
|
|
853
|
+
module.exports.default = userflowAutoStamp;
|
|
854
|
+
//# sourceMappingURL=index.js.map
|