@appilots/cli 0.11.0 → 0.13.0
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 +23 -11
- package/dist/cli/index.js +2495 -534
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +318 -5
- package/dist/index.d.ts +318 -5
- package/dist/index.js +1950 -247
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1943 -240
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var fs = require('fs/promises');
|
|
4
|
-
var
|
|
5
|
-
var
|
|
4
|
+
var crypto = require('crypto');
|
|
5
|
+
var traverse5 = require('@babel/traverse');
|
|
6
6
|
var BabelTypes = require('@babel/types');
|
|
7
|
-
var
|
|
7
|
+
var path2 = require('path');
|
|
8
|
+
var fastGlob = require('fast-glob');
|
|
8
9
|
var parser = require('@babel/parser');
|
|
9
10
|
var fs$1 = require('fs');
|
|
10
|
-
var crypto = require('crypto');
|
|
11
11
|
|
|
12
12
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
13
|
|
|
@@ -30,10 +30,10 @@ function _interopNamespace(e) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
33
|
-
var
|
|
34
|
-
var traverse4__default = /*#__PURE__*/_interopDefault(traverse4);
|
|
33
|
+
var traverse5__default = /*#__PURE__*/_interopDefault(traverse5);
|
|
35
34
|
var BabelTypes__namespace = /*#__PURE__*/_interopNamespace(BabelTypes);
|
|
36
|
-
var
|
|
35
|
+
var path2__namespace = /*#__PURE__*/_interopNamespace(path2);
|
|
36
|
+
var fastGlob__default = /*#__PURE__*/_interopDefault(fastGlob);
|
|
37
37
|
var parser__namespace = /*#__PURE__*/_interopNamespace(parser);
|
|
38
38
|
|
|
39
39
|
var __defProp = Object.defineProperty;
|
|
@@ -41,6 +41,350 @@ var __export = (target, all) => {
|
|
|
41
41
|
for (var name in all)
|
|
42
42
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
43
43
|
};
|
|
44
|
+
var MAX_HOPS = 8;
|
|
45
|
+
function unwrap(path11) {
|
|
46
|
+
while (path11.isTSAsExpression() || path11.isTSTypeAssertion() || path11.isTSNonNullExpression() || path11.isTSSatisfiesExpression() || path11.isParenthesizedExpression())
|
|
47
|
+
path11 = path11.get("expression");
|
|
48
|
+
return path11;
|
|
49
|
+
}
|
|
50
|
+
function constantValue(path11, depth = 0) {
|
|
51
|
+
if (depth > MAX_HOPS) return void 0;
|
|
52
|
+
path11 = unwrap(path11);
|
|
53
|
+
if (!path11.isIdentifier()) return path11;
|
|
54
|
+
const binding = path11.scope.getBinding(path11.node.name);
|
|
55
|
+
if (!binding?.constant || !binding.path.isVariableDeclarator()) return path11;
|
|
56
|
+
const init = binding.path.get("init");
|
|
57
|
+
const resolved = init.node ? constantValue(init, depth + 1) : void 0;
|
|
58
|
+
if (resolved?.isObjectExpression() && binding.referencePaths.some(
|
|
59
|
+
(reference) => !reference.parentPath?.isJSXSpreadAttribute() && !reference.parentPath?.isSpreadElement()
|
|
60
|
+
))
|
|
61
|
+
return void 0;
|
|
62
|
+
return resolved;
|
|
63
|
+
}
|
|
64
|
+
function propValue(path11, name) {
|
|
65
|
+
const readObject = (path12) => {
|
|
66
|
+
const resolved = constantValue(path12);
|
|
67
|
+
if (!resolved?.isObjectExpression()) return { blocked: true };
|
|
68
|
+
for (const property of [...resolved.get("properties")].reverse()) {
|
|
69
|
+
if (property.isSpreadElement()) {
|
|
70
|
+
const found = readObjectBounded(property.get("argument"));
|
|
71
|
+
if (found.value || found.blocked) return found;
|
|
72
|
+
} else if (property.isObjectProperty() || property.isObjectMethod()) {
|
|
73
|
+
if (property.node.computed) return { blocked: true };
|
|
74
|
+
const key = property.node.key;
|
|
75
|
+
if ((BabelTypes__namespace.isIdentifier(key) ? key.name : BabelTypes__namespace.isStringLiteral(key) ? key.value : "") === name)
|
|
76
|
+
return property.isObjectProperty() ? { value: property.get("value") } : { value: property };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {};
|
|
80
|
+
};
|
|
81
|
+
let objectBudget = MAX_HOPS;
|
|
82
|
+
const readObjectBounded = (path12) => objectBudget-- > 0 ? readObject(path12) : { blocked: true };
|
|
83
|
+
for (const attr of [...path11.get("attributes")].reverse()) {
|
|
84
|
+
if (attr.isJSXAttribute() && BabelTypes__namespace.isJSXIdentifier(attr.node.name, { name })) {
|
|
85
|
+
const value = attr.get("value");
|
|
86
|
+
return value.isJSXExpressionContainer() ? unwrap(value.get("expression")) : value.node ? value : void 0;
|
|
87
|
+
}
|
|
88
|
+
if (attr.isJSXSpreadAttribute()) {
|
|
89
|
+
const found = readObjectBounded(attr.get("argument"));
|
|
90
|
+
if (found.value || found.blocked) return found.value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
function importIdentity(path11, depth = 0) {
|
|
96
|
+
if (depth > MAX_HOPS) return void 0;
|
|
97
|
+
path11 = unwrap(path11);
|
|
98
|
+
if (path11.isIdentifier() || path11.isJSXIdentifier()) {
|
|
99
|
+
const binding = path11.scope.getBinding(path11.node.name);
|
|
100
|
+
if (!binding?.constant) return void 0;
|
|
101
|
+
const declaration = binding.path;
|
|
102
|
+
if (declaration.parentPath?.isImportDeclaration()) {
|
|
103
|
+
const module = declaration.parentPath.node.source.value;
|
|
104
|
+
if (declaration.isImportSpecifier()) {
|
|
105
|
+
const imported = declaration.node.imported;
|
|
106
|
+
return { module, imported: BabelTypes__namespace.isIdentifier(imported) ? imported.name : imported.value };
|
|
107
|
+
}
|
|
108
|
+
if (declaration.isImportDefaultSpecifier()) return { module, imported: "default" };
|
|
109
|
+
if (declaration.isImportNamespaceSpecifier()) return { module, imported: "*" };
|
|
110
|
+
}
|
|
111
|
+
if (declaration.isVariableDeclarator() && declaration.get("init").node)
|
|
112
|
+
return importIdentity(declaration.get("init"), depth + 1);
|
|
113
|
+
}
|
|
114
|
+
if (path11.isJSXMemberExpression() || path11.isMemberExpression() && !path11.node.computed) {
|
|
115
|
+
const origin = importIdentity(path11.get("object"), depth + 1);
|
|
116
|
+
const key = path11.node.property;
|
|
117
|
+
if (origin && (BabelTypes__namespace.isIdentifier(key) || BabelTypes__namespace.isJSXIdentifier(key)) && (origin.imported === "*" || origin.module === "react" && origin.imported === "default"))
|
|
118
|
+
return { module: origin.module, imported: key.name };
|
|
119
|
+
}
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
function handlerFunction(path11, depth = 0) {
|
|
123
|
+
if (depth > MAX_HOPS) return void 0;
|
|
124
|
+
path11 = unwrap(path11);
|
|
125
|
+
if (path11.isFunction()) return path11;
|
|
126
|
+
if (path11.isIdentifier()) {
|
|
127
|
+
const binding = path11.scope.getBinding(path11.node.name);
|
|
128
|
+
if (!binding?.constant) return void 0;
|
|
129
|
+
if (binding.path.isFunctionDeclaration()) return binding.path;
|
|
130
|
+
if (binding.path.isVariableDeclarator() && binding.path.get("init").node)
|
|
131
|
+
return handlerFunction(binding.path.get("init"), depth + 1);
|
|
132
|
+
}
|
|
133
|
+
if (path11.isCallExpression()) {
|
|
134
|
+
const origin = importIdentity(path11.get("callee"));
|
|
135
|
+
if (origin?.module === "react" && origin.imported === "useCallback") {
|
|
136
|
+
const callback = path11.get("arguments")[0];
|
|
137
|
+
if (callback) return handlerFunction(callback, depth + 1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (path11.isMemberExpression() && !path11.node.computed && BabelTypes__namespace.isThisExpression(path11.node.object)) {
|
|
141
|
+
const key = path11.node.property;
|
|
142
|
+
if (!BabelTypes__namespace.isIdentifier(key)) return void 0;
|
|
143
|
+
const owner = path11.findParent((p) => p.isClassDeclaration() || p.isClassExpression());
|
|
144
|
+
if (!owner || !(owner.isClassDeclaration() || owner.isClassExpression())) return void 0;
|
|
145
|
+
for (const member of owner.get("body").get("body")) {
|
|
146
|
+
if (!(member.isClassMethod() || member.isClassProperty()) || member.node.computed || member.node.static)
|
|
147
|
+
continue;
|
|
148
|
+
if (!BabelTypes__namespace.isIdentifier(member.node.key, { name: key.name })) continue;
|
|
149
|
+
if (member.isClassMethod() && member.node.kind === "method") return member;
|
|
150
|
+
if (member.isClassProperty() && member.get("value").node)
|
|
151
|
+
return handlerFunction(member.get("value"), depth + 1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return void 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/extractors/control-evidence.ts
|
|
158
|
+
function symbol(node) {
|
|
159
|
+
if (BabelTypes__namespace.isTSAsExpression(node) || BabelTypes__namespace.isTSTypeAssertion(node) || BabelTypes__namespace.isTSNonNullExpression(node) || BabelTypes__namespace.isTSSatisfiesExpression(node))
|
|
160
|
+
return symbol(node.expression);
|
|
161
|
+
if (BabelTypes__namespace.isIdentifier(node) || BabelTypes__namespace.isJSXIdentifier(node)) return node.name;
|
|
162
|
+
if (BabelTypes__namespace.isThisExpression(node)) return "this";
|
|
163
|
+
if (BabelTypes__namespace.isMemberExpression(node) && !node.computed || BabelTypes__namespace.isJSXMemberExpression(node)) {
|
|
164
|
+
const object = symbol(node.object), property = symbol(node.property);
|
|
165
|
+
return object && property ? `${object}.${property}` : void 0;
|
|
166
|
+
}
|
|
167
|
+
if (BabelTypes__namespace.isUnaryExpression(node) && node.operator === "!") {
|
|
168
|
+
const value = symbol(node.argument);
|
|
169
|
+
return value ? `!${value}` : void 0;
|
|
170
|
+
}
|
|
171
|
+
return void 0;
|
|
172
|
+
}
|
|
173
|
+
function attribute(node, name) {
|
|
174
|
+
return node.attributes.find(
|
|
175
|
+
(a) => BabelTypes__namespace.isJSXAttribute(a) && BabelTypes__namespace.isJSXIdentifier(a.name, { name })
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
var ICON_LIBRARIES = [
|
|
179
|
+
"lucide-react-native",
|
|
180
|
+
"lucide-react",
|
|
181
|
+
"@tamagui/lucide-icons",
|
|
182
|
+
"@expo/vector-icons",
|
|
183
|
+
"@react-native-vector-icons/",
|
|
184
|
+
"react-native-vector-icons/"
|
|
185
|
+
];
|
|
186
|
+
function iconLibrary(module) {
|
|
187
|
+
return ICON_LIBRARIES.some(
|
|
188
|
+
(name) => name.endsWith("/") ? module.startsWith(name) : module === name || module.startsWith(name + "/")
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
function add(values, value, max = 12) {
|
|
192
|
+
if (value && value.length <= 240 && values.length < max && !values.includes(value))
|
|
193
|
+
values.push(value);
|
|
194
|
+
}
|
|
195
|
+
function iconName(path11, explicitIcon = false) {
|
|
196
|
+
const origin = importIdentity(path11);
|
|
197
|
+
if (origin && iconLibrary(origin.module) && origin.imported !== "*")
|
|
198
|
+
return origin.imported === "default" ? origin.module : `${origin.module}:${origin.imported}`;
|
|
199
|
+
const name = symbol(path11.node);
|
|
200
|
+
return name && (explicitIcon || /icon/i.test(name)) ? name : void 0;
|
|
201
|
+
}
|
|
202
|
+
function iconsInElement(path11, explicitIcon = false) {
|
|
203
|
+
const name = iconName(path11.get("name"), explicitIcon);
|
|
204
|
+
if (!name) return void 0;
|
|
205
|
+
const glyph = propValue(path11, "name");
|
|
206
|
+
const value = glyph && constantValue(glyph);
|
|
207
|
+
return value?.isStringLiteral() ? `${name}:${value.node.value}` : name;
|
|
208
|
+
}
|
|
209
|
+
function hasInteraction(path11) {
|
|
210
|
+
return ["onPress", "onLongPress", "onClick"].some(
|
|
211
|
+
(name) => attribute(path11.node, name) || propValue(path11, name)
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
function collectPresentationIcons(path11, icons) {
|
|
215
|
+
const namedIconProps = [
|
|
216
|
+
"icon",
|
|
217
|
+
"prefix",
|
|
218
|
+
"suffix",
|
|
219
|
+
"left",
|
|
220
|
+
"right",
|
|
221
|
+
"leadingIcon",
|
|
222
|
+
"trailingIcon",
|
|
223
|
+
"startIcon",
|
|
224
|
+
"endIcon",
|
|
225
|
+
"renderIcon"
|
|
226
|
+
];
|
|
227
|
+
const props = new Set(namedIconProps);
|
|
228
|
+
for (const attr of path11.node.attributes)
|
|
229
|
+
if (BabelTypes__namespace.isJSXAttribute(attr) && BabelTypes__namespace.isJSXIdentifier(attr.name) && !/^on[A-Z]/.test(attr.name.name))
|
|
230
|
+
props.add(attr.name.name);
|
|
231
|
+
for (const prop of props) {
|
|
232
|
+
const icon = propValue(path11, prop);
|
|
233
|
+
if (icon) {
|
|
234
|
+
const value = constantValue(icon);
|
|
235
|
+
if (value?.isStringLiteral()) {
|
|
236
|
+
if (prop.toLowerCase().includes("icon")) add(icons, value.node.value, 8);
|
|
237
|
+
} else {
|
|
238
|
+
const explicitIcon = prop.toLowerCase() === "icon";
|
|
239
|
+
if (icon.isJSXElement()) {
|
|
240
|
+
if (hasInteraction(icon.get("openingElement"))) continue;
|
|
241
|
+
add(icons, iconsInElement(icon.get("openingElement"), explicitIcon), 8);
|
|
242
|
+
}
|
|
243
|
+
icon.traverse({
|
|
244
|
+
JSXAttribute(attr) {
|
|
245
|
+
if (BabelTypes__namespace.isJSXIdentifier(attr.node.name) && /^on[A-Z]/.test(attr.node.name.name))
|
|
246
|
+
attr.skip();
|
|
247
|
+
},
|
|
248
|
+
JSXElement(child) {
|
|
249
|
+
const opening = child.get("openingElement");
|
|
250
|
+
if (hasInteraction(opening)) {
|
|
251
|
+
child.skip();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
add(icons, iconsInElement(opening, explicitIcon), 8);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
if (!icons.length && (namedIconProps.includes(prop) || explicitIcon))
|
|
258
|
+
add(icons, iconName(icon, explicitIcon), 8);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function collectIcons(path11) {
|
|
264
|
+
const icons = [];
|
|
265
|
+
collectPresentationIcons(path11, icons);
|
|
266
|
+
const jsx = path11.parentPath;
|
|
267
|
+
if (jsx.isJSXElement())
|
|
268
|
+
jsx.traverse({
|
|
269
|
+
// JSX mentioned inside an event callback is not a rendered child icon.
|
|
270
|
+
JSXAttribute(attr) {
|
|
271
|
+
attr.skip();
|
|
272
|
+
},
|
|
273
|
+
JSXElement(child) {
|
|
274
|
+
const opening = child.get("openingElement");
|
|
275
|
+
if (hasInteraction(opening)) {
|
|
276
|
+
child.skip();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
add(icons, iconsInElement(opening), 8);
|
|
280
|
+
collectPresentationIcons(opening, icons);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
return icons;
|
|
284
|
+
}
|
|
285
|
+
function conditionsAt(path11) {
|
|
286
|
+
const conditions = [];
|
|
287
|
+
let child = path11;
|
|
288
|
+
for (let parent = child.parentPath; parent && !parent.isFunction(); child = parent, parent = parent.parentPath) {
|
|
289
|
+
let test;
|
|
290
|
+
let negated = false;
|
|
291
|
+
if (parent.isLogicalExpression() && parent.node.right === child.node) {
|
|
292
|
+
if (parent.node.operator === "&&") test = parent.node.left;
|
|
293
|
+
if (parent.node.operator === "||") {
|
|
294
|
+
test = parent.node.left;
|
|
295
|
+
negated = true;
|
|
296
|
+
}
|
|
297
|
+
} else if (parent.isConditionalExpression() && parent.node.test !== child.node) {
|
|
298
|
+
test = parent.node.test;
|
|
299
|
+
negated = parent.node.alternate === child.node;
|
|
300
|
+
} else if (parent.isIfStatement() && parent.node.test !== child.node) {
|
|
301
|
+
test = parent.node.test;
|
|
302
|
+
negated = parent.node.alternate === child.node;
|
|
303
|
+
}
|
|
304
|
+
const name = symbol(test);
|
|
305
|
+
if (name) add(conditions, negated ? name.startsWith("!") ? name.slice(1) : `!${name}` : name);
|
|
306
|
+
}
|
|
307
|
+
return conditions;
|
|
308
|
+
}
|
|
309
|
+
function collectHandlerEvidence(expression, evidence) {
|
|
310
|
+
const seen = /* @__PURE__ */ new Set();
|
|
311
|
+
let budget = 100;
|
|
312
|
+
const visit = (path11, depth) => {
|
|
313
|
+
if (depth > 4 || budget-- <= 0) return;
|
|
314
|
+
const fn = handlerFunction(path11);
|
|
315
|
+
if (!fn || seen.has(fn.node)) return;
|
|
316
|
+
seen.add(fn.node);
|
|
317
|
+
fn.traverse({
|
|
318
|
+
// Ignore uncalled helper definitions; inline callbacks remain source evidence.
|
|
319
|
+
Function(nested) {
|
|
320
|
+
if (!nested.parentPath.isCallExpression() && !nested.parentPath.isObjectProperty())
|
|
321
|
+
nested.skip();
|
|
322
|
+
},
|
|
323
|
+
CallExpression(call) {
|
|
324
|
+
if (budget-- <= 0) {
|
|
325
|
+
call.skip();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
add(evidence.calls, symbol(call.node.callee));
|
|
329
|
+
for (const arg of call.node.arguments) add(evidence.argumentBindings, symbol(arg));
|
|
330
|
+
visit(call.get("callee"), depth + 1);
|
|
331
|
+
if (!BabelTypes__namespace.isMemberExpression(call.node.callee) || call.node.callee.computed || !BabelTypes__namespace.isIdentifier(call.node.callee.property, { name: "alert" }))
|
|
332
|
+
return;
|
|
333
|
+
const callee = call.get("callee");
|
|
334
|
+
if (!callee.isMemberExpression()) return;
|
|
335
|
+
const origin = importIdentity(callee.get("object"));
|
|
336
|
+
if (origin?.module !== "react-native" || origin.imported !== "Alert") return;
|
|
337
|
+
const options = call.node.arguments[2];
|
|
338
|
+
const destructiveOption = BabelTypes__namespace.isArrayExpression(options) && options.elements.some(
|
|
339
|
+
(option) => BabelTypes__namespace.isObjectExpression(option) && option.properties.some(
|
|
340
|
+
(p) => BabelTypes__namespace.isObjectProperty(p) && !p.computed && symbol(p.key) === "style" && BabelTypes__namespace.isStringLiteral(p.value, { value: "destructive" })
|
|
341
|
+
)
|
|
342
|
+
);
|
|
343
|
+
if (destructiveOption)
|
|
344
|
+
evidence.nativeConfirmation = {
|
|
345
|
+
title: symbol(call.node.arguments[0]),
|
|
346
|
+
destructiveOption
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
visit(unwrap(expression), 0);
|
|
352
|
+
}
|
|
353
|
+
function extractControlEvidence(ast, source, file) {
|
|
354
|
+
const candidates = [];
|
|
355
|
+
const sourceHash = crypto.createHash("sha256").update(source).digest("hex");
|
|
356
|
+
traverse5__default.default(ast, {
|
|
357
|
+
JSXOpeningElement(path11) {
|
|
358
|
+
const node = path11.node;
|
|
359
|
+
if (attribute(node, "__appilotsControl")) return;
|
|
360
|
+
const value = propValue(path11, "onPress");
|
|
361
|
+
if (!value && !attribute(node, "onPress") || value?.isNullLiteral() || value?.isJSXEmptyExpression())
|
|
362
|
+
return;
|
|
363
|
+
const icons = collectIcons(path11);
|
|
364
|
+
if (!icons.length || node.end == null) return;
|
|
365
|
+
const evidence = {
|
|
366
|
+
version: 1,
|
|
367
|
+
siteId: crypto.createHash("sha256").update(`${file}:${sourceHash}:${node.start}`).digest("hex").slice(0, 20),
|
|
368
|
+
component: symbol(node.name) ?? "unknown",
|
|
369
|
+
icons,
|
|
370
|
+
...value && symbol(value.node) ? { handler: symbol(value.node) } : {},
|
|
371
|
+
calls: [],
|
|
372
|
+
argumentBindings: [],
|
|
373
|
+
conditions: conditionsAt(path11)
|
|
374
|
+
};
|
|
375
|
+
if (value) collectHandlerEvidence(value, evidence);
|
|
376
|
+
candidates.push({ evidence, sourceHash, offset: node.end - (node.selfClosing ? 2 : 1) });
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
return candidates;
|
|
380
|
+
}
|
|
381
|
+
function byCodeUnit(a, b) {
|
|
382
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
383
|
+
}
|
|
384
|
+
async function globSorted(patterns, options) {
|
|
385
|
+
const files = await fastGlob__default.default(patterns, options);
|
|
386
|
+
return files.sort(byCodeUnit);
|
|
387
|
+
}
|
|
44
388
|
var DEFAULT_PARSER_PLUGINS = [
|
|
45
389
|
"jsx",
|
|
46
390
|
"typescript",
|
|
@@ -155,7 +499,7 @@ function classifyJsxComponent(name, element) {
|
|
|
155
499
|
}
|
|
156
500
|
function collectFunctions(ast) {
|
|
157
501
|
const handlers = /* @__PURE__ */ new Map();
|
|
158
|
-
|
|
502
|
+
traverse5__default.default(ast, {
|
|
159
503
|
FunctionDeclaration: (nodePath) => {
|
|
160
504
|
if (nodePath.node.id?.name) handlers.set(nodePath.node.id.name, nodePath.node);
|
|
161
505
|
},
|
|
@@ -258,7 +602,7 @@ function analyzeFunctionBehavior(name, fn, handlers, seen = /* @__PURE__ */ new
|
|
|
258
602
|
}
|
|
259
603
|
};
|
|
260
604
|
if (fn.body) {
|
|
261
|
-
|
|
605
|
+
traverse5__default.default(fn.body, {
|
|
262
606
|
noScope: true,
|
|
263
607
|
enter: (nodePath) => inspectNode(nodePath.node)
|
|
264
608
|
});
|
|
@@ -286,7 +630,7 @@ function setterToStateName(setterName) {
|
|
|
286
630
|
}
|
|
287
631
|
function extractNavigationCalls(ast) {
|
|
288
632
|
const calls = [];
|
|
289
|
-
|
|
633
|
+
traverse5__default.default(ast, {
|
|
290
634
|
noScope: !BabelTypes__namespace.isFile(ast),
|
|
291
635
|
CallExpression: (nodePath) => {
|
|
292
636
|
const node = nodePath.node;
|
|
@@ -425,14 +769,26 @@ var ScreenAnalyzer = class {
|
|
|
425
769
|
strictScreens;
|
|
426
770
|
/** Glob patterns that identify screen files in strict mode */
|
|
427
771
|
screenPatterns;
|
|
772
|
+
/**
|
|
773
|
+
* Arquivos que uma ROTA monta (`component={…}` resolvido pelo
|
|
774
|
+
* `NavigationAnalyzer`). Passam pelo filtro estrito sem depender de
|
|
775
|
+
* convenção de nome, porque um componente que uma rota monta É uma tela por
|
|
776
|
+
* definição — não é heurística, é o que o app declarou.
|
|
777
|
+
*
|
|
778
|
+
* É isto que resolve o caso `rocketchat`, cujas telas se chamam `*View.tsx`
|
|
779
|
+
* em `app/views/`, e o `coopcycle`, que não tem diretório `screens/` nenhum.
|
|
780
|
+
*/
|
|
781
|
+
routeTargetFiles;
|
|
428
782
|
/** §D: Count of screens filtered out in strict mode (available after analyze()) */
|
|
429
783
|
screensFilteredOut = 0;
|
|
784
|
+
controlEvidenceFiles = {};
|
|
430
785
|
constructor(config, options) {
|
|
431
786
|
this.config = config;
|
|
787
|
+
this.routeTargetFiles = options?.routeTargetFiles ?? /* @__PURE__ */ new Set();
|
|
432
788
|
this.strictScreens = options?.strictScreens ?? false;
|
|
433
789
|
this.screenPatterns = options?.screenPatterns ?? [
|
|
434
|
-
"**/*Screen.{ts,tsx}",
|
|
435
|
-
"**/screens/**/*.{ts,tsx}"
|
|
790
|
+
"**/*Screen.{ts,tsx,js,jsx}",
|
|
791
|
+
"**/screens/**/*.{ts,tsx,js,jsx}"
|
|
436
792
|
];
|
|
437
793
|
}
|
|
438
794
|
/** Analyze all screens in the project */
|
|
@@ -444,7 +800,7 @@ var ScreenAnalyzer = class {
|
|
|
444
800
|
console.log(`[ScreenAnalyzer] Strict mode ON \u2014 screen patterns:`, this.screenPatterns);
|
|
445
801
|
}
|
|
446
802
|
}
|
|
447
|
-
const files = await
|
|
803
|
+
const files = await globSorted(include, {
|
|
448
804
|
cwd: this.config.rootDir,
|
|
449
805
|
ignore: exclude
|
|
450
806
|
});
|
|
@@ -453,23 +809,24 @@ var ScreenAnalyzer = class {
|
|
|
453
809
|
}
|
|
454
810
|
let screenPatternFiles = null;
|
|
455
811
|
if (this.strictScreens) {
|
|
456
|
-
const matched = await
|
|
812
|
+
const matched = await globSorted(this.screenPatterns, {
|
|
457
813
|
cwd: this.config.rootDir,
|
|
458
814
|
ignore: exclude
|
|
459
815
|
});
|
|
460
|
-
screenPatternFiles = new Set(matched.map((f) =>
|
|
816
|
+
screenPatternFiles = new Set(matched.map((f) => path2__namespace.default.resolve(this.config.rootDir, f)));
|
|
461
817
|
}
|
|
462
818
|
const screens = [];
|
|
463
819
|
this.screensFilteredOut = 0;
|
|
464
820
|
for (const file of files) {
|
|
465
|
-
const filePath =
|
|
821
|
+
const filePath = path2__namespace.default.resolve(this.config.rootDir, file);
|
|
466
822
|
try {
|
|
467
823
|
const descriptor = await this.analyzeFile(filePath);
|
|
468
824
|
if (!descriptor) continue;
|
|
469
825
|
if (this.strictScreens) {
|
|
470
826
|
const hasRegisterScreen = descriptor.__hasRegisterScreen === true;
|
|
471
827
|
const matchesPattern = screenPatternFiles?.has(filePath) ?? false;
|
|
472
|
-
|
|
828
|
+
const isRouteTarget = this.routeTargetFiles.has(filePath);
|
|
829
|
+
if (!hasRegisterScreen && !matchesPattern && !isRouteTarget) {
|
|
473
830
|
this.screensFilteredOut++;
|
|
474
831
|
if (this.verbose) {
|
|
475
832
|
console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file}`);
|
|
@@ -525,6 +882,11 @@ var ScreenAnalyzer = class {
|
|
|
525
882
|
title: registerScreenMeta?.title,
|
|
526
883
|
description: registerScreenMeta?.description,
|
|
527
884
|
components,
|
|
885
|
+
controlCandidates: extractControlEvidence(
|
|
886
|
+
ast,
|
|
887
|
+
source,
|
|
888
|
+
path2__namespace.default.relative(this.config.rootDir, filePath)
|
|
889
|
+
),
|
|
528
890
|
forms,
|
|
529
891
|
actions,
|
|
530
892
|
navigationTargets,
|
|
@@ -535,6 +897,8 @@ var ScreenAnalyzer = class {
|
|
|
535
897
|
...permissionsFromJsDoc.isPii ? { isPii: true } : {}
|
|
536
898
|
} : {}
|
|
537
899
|
};
|
|
900
|
+
if (descriptor.controlCandidates?.length)
|
|
901
|
+
this.controlEvidenceFiles[path2__namespace.default.relative(this.config.rootDir, filePath).split(path2__namespace.default.sep).join("/")] = descriptor.controlCandidates;
|
|
538
902
|
descriptor.__hasRegisterScreen = hasRegisterScreenCall;
|
|
539
903
|
return descriptor;
|
|
540
904
|
}
|
|
@@ -544,7 +908,7 @@ var ScreenAnalyzer = class {
|
|
|
544
908
|
*/
|
|
545
909
|
detectRegisterScreenCall(ast) {
|
|
546
910
|
let found = false;
|
|
547
|
-
|
|
911
|
+
traverse5__default.default(ast, {
|
|
548
912
|
CallExpression: (nodePath) => {
|
|
549
913
|
if (found) return;
|
|
550
914
|
const callee = nodePath.node.callee;
|
|
@@ -561,7 +925,7 @@ var ScreenAnalyzer = class {
|
|
|
561
925
|
*/
|
|
562
926
|
extractRegisterScreenMetadata(ast) {
|
|
563
927
|
let metadata = null;
|
|
564
|
-
|
|
928
|
+
traverse5__default.default(ast, {
|
|
565
929
|
CallExpression: (nodePath) => {
|
|
566
930
|
const callee = nodePath.node.callee;
|
|
567
931
|
if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "registerScreen" || BabelTypes__namespace.isMemberExpression(callee) && BabelTypes__namespace.isIdentifier(callee.property) && callee.property.name === "registerScreen") {
|
|
@@ -677,6 +1041,8 @@ var ScreenAnalyzer = class {
|
|
|
677
1041
|
action.riskLevel = value.value;
|
|
678
1042
|
} else if (key === "nativeConfirmationExpected" && BabelTypes__namespace.isBooleanLiteral(value)) {
|
|
679
1043
|
action.nativeConfirmationExpected = value.value;
|
|
1044
|
+
} else if (key === "asyncBudgetMs" && BabelTypes__namespace.isNumericLiteral(value) && Number.isInteger(value.value) && value.value > 0) {
|
|
1045
|
+
action.asyncBudgetMs = value.value;
|
|
680
1046
|
} else if (key === "appilotsInferred" && BabelTypes__namespace.isObjectExpression(value)) {
|
|
681
1047
|
action.appilotsInferred = this.parseAppilotsInferredObject(value);
|
|
682
1048
|
}
|
|
@@ -856,7 +1222,7 @@ var ScreenAnalyzer = class {
|
|
|
856
1222
|
*/
|
|
857
1223
|
extractDefaultComponentName(ast) {
|
|
858
1224
|
let componentName = "";
|
|
859
|
-
|
|
1225
|
+
traverse5__default.default(ast, {
|
|
860
1226
|
ExportDefaultDeclaration: (nodePath) => {
|
|
861
1227
|
const declaration = nodePath.node.declaration;
|
|
862
1228
|
if (BabelTypes__namespace.isFunctionDeclaration(declaration) && declaration.id?.name) {
|
|
@@ -881,7 +1247,7 @@ var ScreenAnalyzer = class {
|
|
|
881
1247
|
*/
|
|
882
1248
|
extractNavigationTargets(ast) {
|
|
883
1249
|
const targets = /* @__PURE__ */ new Set();
|
|
884
|
-
|
|
1250
|
+
traverse5__default.default(ast, {
|
|
885
1251
|
CallExpression: (nodePath) => {
|
|
886
1252
|
const callee = nodePath.node.callee;
|
|
887
1253
|
if (BabelTypes__namespace.isMemberExpression(callee) && BabelTypes__namespace.isIdentifier(callee.object) && callee.object.name === "navigation" && BabelTypes__namespace.isIdentifier(callee.property) && callee.property.name === "navigate") {
|
|
@@ -900,7 +1266,7 @@ var ScreenAnalyzer = class {
|
|
|
900
1266
|
extractForms(ast) {
|
|
901
1267
|
const forms = [];
|
|
902
1268
|
const fields = /* @__PURE__ */ new Map();
|
|
903
|
-
|
|
1269
|
+
traverse5__default.default(ast, {
|
|
904
1270
|
JSXOpeningElement: (nodePath) => {
|
|
905
1271
|
const element = nodePath.node;
|
|
906
1272
|
if (BabelTypes__namespace.isJSXIdentifier(element.name)) {
|
|
@@ -1021,7 +1387,7 @@ var ScreenAnalyzer = class {
|
|
|
1021
1387
|
extractComponents(ast) {
|
|
1022
1388
|
const components = [];
|
|
1023
1389
|
const seen = /* @__PURE__ */ new Set();
|
|
1024
|
-
|
|
1390
|
+
traverse5__default.default(ast, {
|
|
1025
1391
|
JSXOpeningElement: (nodePath) => {
|
|
1026
1392
|
const element = nodePath.node;
|
|
1027
1393
|
if (BabelTypes__namespace.isJSXIdentifier(element.name)) {
|
|
@@ -1085,7 +1451,7 @@ var ScreenAnalyzer = class {
|
|
|
1085
1451
|
const actionLabels = new Map(
|
|
1086
1452
|
actions.filter((a) => a.label).map((a) => [this.normalizeLabel(a.label), a])
|
|
1087
1453
|
);
|
|
1088
|
-
|
|
1454
|
+
traverse5__default.default(ast, {
|
|
1089
1455
|
JSXOpeningElement: (nodePath) => {
|
|
1090
1456
|
const element = nodePath.node;
|
|
1091
1457
|
if (BabelTypes__namespace.isJSXIdentifier(element.name)) {
|
|
@@ -1154,7 +1520,7 @@ var ScreenAnalyzer = class {
|
|
|
1154
1520
|
}
|
|
1155
1521
|
collectButtonHandlersByLabel(ast) {
|
|
1156
1522
|
const out = /* @__PURE__ */ new Map();
|
|
1157
|
-
|
|
1523
|
+
traverse5__default.default(ast, {
|
|
1158
1524
|
JSXOpeningElement: (nodePath) => {
|
|
1159
1525
|
const element = nodePath.node;
|
|
1160
1526
|
if (!BabelTypes__namespace.isJSXIdentifier(element.name)) return;
|
|
@@ -1236,7 +1602,7 @@ var ScreenAnalyzer = class {
|
|
|
1236
1602
|
}
|
|
1237
1603
|
};
|
|
1238
1604
|
if (fn.body) {
|
|
1239
|
-
|
|
1605
|
+
traverse5__default.default(
|
|
1240
1606
|
fn.body,
|
|
1241
1607
|
{
|
|
1242
1608
|
noScope: true,
|
|
@@ -1433,7 +1799,7 @@ var ScreenAnalyzer = class {
|
|
|
1433
1799
|
extractCollections(ast) {
|
|
1434
1800
|
const renderItemFns = this.collectRenderItemFunctions(ast);
|
|
1435
1801
|
const collections = [];
|
|
1436
|
-
|
|
1802
|
+
traverse5__default.default(ast, {
|
|
1437
1803
|
JSXOpeningElement: (nodePath) => {
|
|
1438
1804
|
const element = nodePath.node;
|
|
1439
1805
|
if (!BabelTypes__namespace.isJSXIdentifier(element.name)) return;
|
|
@@ -1470,7 +1836,7 @@ var ScreenAnalyzer = class {
|
|
|
1470
1836
|
}
|
|
1471
1837
|
collectRenderItemFunctions(ast) {
|
|
1472
1838
|
const out = /* @__PURE__ */ new Map();
|
|
1473
|
-
|
|
1839
|
+
traverse5__default.default(ast, {
|
|
1474
1840
|
VariableDeclarator: (nodePath) => {
|
|
1475
1841
|
if (!BabelTypes__namespace.isIdentifier(nodePath.node.id)) return;
|
|
1476
1842
|
const init = nodePath.node.init;
|
|
@@ -1513,7 +1879,7 @@ var ScreenAnalyzer = class {
|
|
|
1513
1879
|
}
|
|
1514
1880
|
extractRowAction(fn) {
|
|
1515
1881
|
let action;
|
|
1516
|
-
|
|
1882
|
+
traverse5__default.default(
|
|
1517
1883
|
fn.body,
|
|
1518
1884
|
{
|
|
1519
1885
|
noScope: true,
|
|
@@ -1566,7 +1932,7 @@ var ScreenAnalyzer = class {
|
|
|
1566
1932
|
} else if (BabelTypes__namespace.isIdentifier(firstParam)) {
|
|
1567
1933
|
itemNames.add(firstParam.name);
|
|
1568
1934
|
}
|
|
1569
|
-
|
|
1935
|
+
traverse5__default.default(
|
|
1570
1936
|
fn.body,
|
|
1571
1937
|
{
|
|
1572
1938
|
noScope: true,
|
|
@@ -1608,7 +1974,7 @@ var ScreenAnalyzer = class {
|
|
|
1608
1974
|
inferSearchField(ast, dataSource) {
|
|
1609
1975
|
if (!dataSource) return void 0;
|
|
1610
1976
|
let queryBinding;
|
|
1611
|
-
|
|
1977
|
+
traverse5__default.default(ast, {
|
|
1612
1978
|
CallExpression: (nodePath) => {
|
|
1613
1979
|
const node = nodePath.node;
|
|
1614
1980
|
if (!BabelTypes__namespace.isMemberExpression(node.callee)) return;
|
|
@@ -1619,7 +1985,7 @@ var ScreenAnalyzer = class {
|
|
|
1619
1985
|
const fn = node.arguments[0];
|
|
1620
1986
|
if (!BabelTypes__namespace.isArrowFunctionExpression(fn) && !BabelTypes__namespace.isFunctionExpression(fn))
|
|
1621
1987
|
return;
|
|
1622
|
-
|
|
1988
|
+
traverse5__default.default(
|
|
1623
1989
|
fn.body,
|
|
1624
1990
|
{
|
|
1625
1991
|
noScope: true,
|
|
@@ -1656,43 +2022,871 @@ var ScreenAnalyzer = class {
|
|
|
1656
2022
|
* E.g., /src/screens/ItemListScreen.tsx -> ItemListScreen
|
|
1657
2023
|
*/
|
|
1658
2024
|
extractScreenName(filePath) {
|
|
1659
|
-
const basename2 =
|
|
2025
|
+
const basename2 = path2__namespace.default.basename(filePath);
|
|
1660
2026
|
return basename2.replace(/\.(tsx?|jsx?)$/, "");
|
|
1661
2027
|
}
|
|
1662
2028
|
};
|
|
2029
|
+
var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
|
|
2030
|
+
var MAX_HOPS2 = 8;
|
|
2031
|
+
var ModuleGraph = class {
|
|
2032
|
+
asts = /* @__PURE__ */ new Map();
|
|
2033
|
+
resolved = /* @__PURE__ */ new Map();
|
|
2034
|
+
/** `@src/*` → `<root>/src/*`, lido do tsconfig do app. */
|
|
2035
|
+
aliases;
|
|
2036
|
+
/** `uniswap` → `<repo>/packages/uniswap`, lido do workspace do monorepo. */
|
|
2037
|
+
workspacePackages;
|
|
2038
|
+
constructor(rootDir) {
|
|
2039
|
+
this.aliases = rootDir ? readTsconfigAliases(rootDir) : [];
|
|
2040
|
+
this.workspacePackages = rootDir ? readWorkspacePackages(rootDir) : [];
|
|
2041
|
+
}
|
|
2042
|
+
/** AST de um arquivo, memoizada. `null` quando não parseia. */
|
|
2043
|
+
parse(file) {
|
|
2044
|
+
const cached = this.asts.get(file);
|
|
2045
|
+
if (cached !== void 0) return cached;
|
|
2046
|
+
let ast = null;
|
|
2047
|
+
try {
|
|
2048
|
+
ast = parser__namespace.parse(fs$1.readFileSync(file, "utf-8"), {
|
|
2049
|
+
sourceType: "module",
|
|
2050
|
+
plugins: ["jsx", "typescript"]
|
|
2051
|
+
});
|
|
2052
|
+
} catch {
|
|
2053
|
+
ast = null;
|
|
2054
|
+
}
|
|
2055
|
+
this.asts.set(file, ast);
|
|
2056
|
+
return ast;
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* `./account/Home` a partir de `src/navigation/index.tsx` → caminho absoluto.
|
|
2060
|
+
* Só resolve caminho relativo: import de pacote (`@react-navigation/native`)
|
|
2061
|
+
* é de terceiro e não tem tela nossa dentro.
|
|
2062
|
+
*/
|
|
2063
|
+
resolve(fromFile, spec) {
|
|
2064
|
+
const key = `${fromFile} ${spec}`;
|
|
2065
|
+
const cached = this.resolved.get(key);
|
|
2066
|
+
if (cached !== void 0) return cached;
|
|
2067
|
+
let base = null;
|
|
2068
|
+
if (spec.startsWith(".")) {
|
|
2069
|
+
base = path2__namespace.default.resolve(path2__namespace.default.dirname(fromFile), spec);
|
|
2070
|
+
} else {
|
|
2071
|
+
for (const { prefix, target } of this.aliases) {
|
|
2072
|
+
if (spec === prefix || spec.startsWith(prefix + "/")) {
|
|
2073
|
+
base = path2__namespace.default.join(target, spec.slice(prefix.length));
|
|
2074
|
+
break;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
if (!base) {
|
|
2078
|
+
for (const pkg of this.workspacePackages) {
|
|
2079
|
+
if (spec === pkg.name || spec.startsWith(pkg.name + "/")) {
|
|
2080
|
+
base = path2__namespace.default.join(pkg.dir, spec.slice(pkg.name.length));
|
|
2081
|
+
break;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
if (!base) {
|
|
2087
|
+
this.resolved.set(key, null);
|
|
2088
|
+
return null;
|
|
2089
|
+
}
|
|
2090
|
+
const candidates = [
|
|
2091
|
+
base,
|
|
2092
|
+
...EXTENSIONS.map((e) => base + e),
|
|
2093
|
+
...EXTENSIONS.map((e) => path2__namespace.default.join(base, "index" + e))
|
|
2094
|
+
];
|
|
2095
|
+
let found = null;
|
|
2096
|
+
for (const c of candidates) {
|
|
2097
|
+
try {
|
|
2098
|
+
if (fs$1.existsSync(c) && fs$1.statSync(c).isFile()) {
|
|
2099
|
+
found = c;
|
|
2100
|
+
break;
|
|
2101
|
+
}
|
|
2102
|
+
} catch {
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
this.resolved.set(key, found);
|
|
2106
|
+
return found;
|
|
2107
|
+
}
|
|
2108
|
+
/** `import`s do arquivo, por nome local. */
|
|
2109
|
+
imports(file) {
|
|
2110
|
+
const out = /* @__PURE__ */ new Map();
|
|
2111
|
+
const ast = this.parse(file);
|
|
2112
|
+
if (!ast) return out;
|
|
2113
|
+
for (const stmt of ast.program.body) {
|
|
2114
|
+
if (!BabelTypes__namespace.isImportDeclaration(stmt)) continue;
|
|
2115
|
+
const source = stmt.source.value;
|
|
2116
|
+
for (const spec of stmt.specifiers) {
|
|
2117
|
+
if (BabelTypes__namespace.isImportDefaultSpecifier(spec)) {
|
|
2118
|
+
out.set(spec.local.name, { source, imported: "default" });
|
|
2119
|
+
} else if (BabelTypes__namespace.isImportNamespaceSpecifier(spec)) {
|
|
2120
|
+
out.set(spec.local.name, { source, imported: "*" });
|
|
2121
|
+
} else if (BabelTypes__namespace.isImportSpecifier(spec)) {
|
|
2122
|
+
const imported = BabelTypes__namespace.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
|
|
2123
|
+
out.set(spec.local.name, { source, imported });
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
return out;
|
|
2128
|
+
}
|
|
2129
|
+
/** `const X = <init>` no topo do arquivo, incluindo `export const`. */
|
|
2130
|
+
topLevelInit(file, name) {
|
|
2131
|
+
const ast = this.parse(file);
|
|
2132
|
+
if (!ast) return null;
|
|
2133
|
+
for (const stmt of ast.program.body) {
|
|
2134
|
+
const decl = BabelTypes__namespace.isExportNamedDeclaration(stmt) ? stmt.declaration : stmt;
|
|
2135
|
+
if (!BabelTypes__namespace.isVariableDeclaration(decl)) continue;
|
|
2136
|
+
for (const d of decl.declarations) {
|
|
2137
|
+
if (BabelTypes__namespace.isIdentifier(d.id) && d.id.name === name && d.init) {
|
|
2138
|
+
return BabelTypes__namespace.isTSAsExpression(d.init) ? d.init.expression : d.init;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
return null;
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Onde `name` é DEFINIDO — segue import e reexport de barrel.
|
|
2146
|
+
* Devolve o arquivo e o nome sob o qual ele é definido lá.
|
|
2147
|
+
*/
|
|
2148
|
+
resolveBinding(file, name, hops = 0) {
|
|
2149
|
+
if (hops > MAX_HOPS2) return null;
|
|
2150
|
+
if (this.topLevelInit(file, name) !== null) return { file, name };
|
|
2151
|
+
const binding = this.imports(file).get(name);
|
|
2152
|
+
if (binding) {
|
|
2153
|
+
const target = this.resolve(file, binding.source);
|
|
2154
|
+
if (!target) return null;
|
|
2155
|
+
const next = binding.imported === "default" || binding.imported === "*" ? name : binding.imported;
|
|
2156
|
+
const deeper = this.resolveBinding(target, next, hops + 1);
|
|
2157
|
+
return deeper ?? { file: target, name: next };
|
|
2158
|
+
}
|
|
2159
|
+
const ast = this.parse(file);
|
|
2160
|
+
if (ast) {
|
|
2161
|
+
for (const stmt of ast.program.body) {
|
|
2162
|
+
if (!BabelTypes__namespace.isExportNamedDeclaration(stmt) || !stmt.source) continue;
|
|
2163
|
+
for (const spec of stmt.specifiers) {
|
|
2164
|
+
if (!BabelTypes__namespace.isExportSpecifier(spec)) continue;
|
|
2165
|
+
const exported = BabelTypes__namespace.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value;
|
|
2166
|
+
if (exported !== name) continue;
|
|
2167
|
+
const target = this.resolve(file, stmt.source.value);
|
|
2168
|
+
if (!target) return null;
|
|
2169
|
+
const local = spec.local.name;
|
|
2170
|
+
return this.resolveBinding(target, local, hops + 1) ?? { file: target, name: local };
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
if (ast) {
|
|
2175
|
+
for (const stmt of ast.program.body) {
|
|
2176
|
+
if (!BabelTypes__namespace.isExportAllDeclaration(stmt)) continue;
|
|
2177
|
+
const target = this.resolve(file, stmt.source.value);
|
|
2178
|
+
if (!target || target === file) continue;
|
|
2179
|
+
const deeper = this.resolveBinding(target, name, hops + 1);
|
|
2180
|
+
if (deeper) return deeper;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
if (ast) {
|
|
2184
|
+
for (const stmt of ast.program.body) {
|
|
2185
|
+
if (!BabelTypes__namespace.isExportDefaultDeclaration(stmt)) continue;
|
|
2186
|
+
if (BabelTypes__namespace.isIdentifier(stmt.declaration)) {
|
|
2187
|
+
const local = stmt.declaration.name;
|
|
2188
|
+
if (local === name) return null;
|
|
2189
|
+
return this.resolveBinding(file, local, hops + 1) ?? { file, name: local };
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
return null;
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* O valor string de uma expressão de nome de rota, ou `null`.
|
|
2197
|
+
*
|
|
2198
|
+
* Cobre `"Chat"`, `ROUTES.CHAT`, `ROUTES.ONBOARDING.SPLASH` e `SOME_CONST` —
|
|
2199
|
+
* seguindo import quando o objeto vem de outro arquivo. NÃO cobre template
|
|
2200
|
+
* com interpolação nem valor calculado, de propósito.
|
|
2201
|
+
*/
|
|
2202
|
+
stringConstant(file, node, hops = 0) {
|
|
2203
|
+
if (!node || hops > MAX_HOPS2) return null;
|
|
2204
|
+
if (BabelTypes__namespace.isStringLiteral(node)) return node.value;
|
|
2205
|
+
if (BabelTypes__namespace.isTemplateLiteral(node)) {
|
|
2206
|
+
return node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? null : null;
|
|
2207
|
+
}
|
|
2208
|
+
if (BabelTypes__namespace.isTSAsExpression(node)) return this.stringConstant(file, node.expression, hops + 1);
|
|
2209
|
+
const chain = memberChain(node);
|
|
2210
|
+
if (!chain) return null;
|
|
2211
|
+
const origin = this.resolveBinding(file, chain.root);
|
|
2212
|
+
if (!origin) return null;
|
|
2213
|
+
let current = this.topLevelInit(origin.file, origin.name);
|
|
2214
|
+
if (!current) return null;
|
|
2215
|
+
for (const key of chain.path) {
|
|
2216
|
+
if (!BabelTypes__namespace.isObjectExpression(current)) return null;
|
|
2217
|
+
const prop = objectProperty(current, key);
|
|
2218
|
+
if (!prop) return null;
|
|
2219
|
+
current = BabelTypes__namespace.isTSAsExpression(prop) ? prop.expression : prop;
|
|
2220
|
+
}
|
|
2221
|
+
return BabelTypes__namespace.isStringLiteral(current) ? current.value : null;
|
|
2222
|
+
}
|
|
2223
|
+
/**
|
|
2224
|
+
* O ARQUIVO onde vive o componente de uma rota, ou `null`.
|
|
2225
|
+
*
|
|
2226
|
+
* Aceita as três formas que o corpus mostrou: identificador
|
|
2227
|
+
* (`component={Home}`), membro de barrel (`component={screens.AccountHome}`)
|
|
2228
|
+
* e componente embrulhado em HOC (`component={gestureHandlerRootHOC(Chat)}`,
|
|
2229
|
+
* que é como o `pocketpal` monta todas as telas do Drawer).
|
|
2230
|
+
*/
|
|
2231
|
+
componentFile(file, node, hops = 0) {
|
|
2232
|
+
if (!node || hops > MAX_HOPS2) return null;
|
|
2233
|
+
if (BabelTypes__namespace.isCallExpression(node)) {
|
|
2234
|
+
for (const arg of node.arguments) {
|
|
2235
|
+
if (BabelTypes__namespace.isIdentifier(arg) || BabelTypes__namespace.isMemberExpression(arg)) {
|
|
2236
|
+
const inner = this.componentFile(file, arg, hops + 1);
|
|
2237
|
+
if (inner) return inner;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
return null;
|
|
2241
|
+
}
|
|
2242
|
+
if (BabelTypes__namespace.isTSAsExpression(node)) return this.componentFile(file, node.expression, hops + 1);
|
|
2243
|
+
const chain = memberChain(node);
|
|
2244
|
+
if (!chain) return null;
|
|
2245
|
+
const origin = this.resolveBinding(file, chain.root);
|
|
2246
|
+
if (!origin) return null;
|
|
2247
|
+
if (chain.path.length === 0) return origin.file;
|
|
2248
|
+
const init = this.topLevelInit(origin.file, origin.name);
|
|
2249
|
+
if (init && BabelTypes__namespace.isObjectExpression(init)) {
|
|
2250
|
+
const prop = objectProperty(init, chain.path[0]);
|
|
2251
|
+
if (prop) return this.componentFile(origin.file, prop, hops + 1);
|
|
2252
|
+
}
|
|
2253
|
+
const viaExport = this.resolveBinding(origin.file, chain.path[0], hops + 1);
|
|
2254
|
+
return viaExport?.file ?? null;
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Os arquivos DO PROJETO que este arquivo renderiza como JSX.
|
|
2258
|
+
*
|
|
2259
|
+
* `<ChatView …/>` em `ChatScreen.tsx` → `components/ChatView/ChatView.tsx`.
|
|
2260
|
+
* Import de pacote devolve `null` no `resolve` e fica de fora: componente de
|
|
2261
|
+
* terceiro não tem tela nossa dentro.
|
|
2262
|
+
*
|
|
2263
|
+
* Ignora `<X.Screen>` e `<X.Navigator>` de propósito — navegação é outro
|
|
2264
|
+
* extractor, e um navegador não é conteúdo de tela.
|
|
2265
|
+
*/
|
|
2266
|
+
renderedComponentFiles(file) {
|
|
2267
|
+
const ast = this.parse(file);
|
|
2268
|
+
if (!ast) return [];
|
|
2269
|
+
const names = /* @__PURE__ */ new Set();
|
|
2270
|
+
const visit = (node) => {
|
|
2271
|
+
if (!node || typeof node !== "object") return;
|
|
2272
|
+
if (BabelTypes__namespace.isJSXOpeningElement(node) && BabelTypes__namespace.isJSXIdentifier(node.name)) {
|
|
2273
|
+
const n = node.name.name;
|
|
2274
|
+
if (/^[A-Z]/.test(n)) names.add(n);
|
|
2275
|
+
}
|
|
2276
|
+
for (const key of Object.keys(node)) {
|
|
2277
|
+
const value = node[key];
|
|
2278
|
+
if (Array.isArray(value)) {
|
|
2279
|
+
for (const item of value) visit(item);
|
|
2280
|
+
} else if (value && typeof value === "object" && "type" in value) {
|
|
2281
|
+
visit(value);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
};
|
|
2285
|
+
visit(ast.program);
|
|
2286
|
+
const out = /* @__PURE__ */ new Set();
|
|
2287
|
+
for (const name of names) {
|
|
2288
|
+
const origin = this.resolveBinding(file, name);
|
|
2289
|
+
if (origin && origin.file !== file) out.add(origin.file);
|
|
2290
|
+
}
|
|
2291
|
+
return [...out];
|
|
2292
|
+
}
|
|
2293
|
+
};
|
|
2294
|
+
function memberChain(node) {
|
|
2295
|
+
const chain = [];
|
|
2296
|
+
let current = node;
|
|
2297
|
+
while (BabelTypes__namespace.isMemberExpression(current)) {
|
|
2298
|
+
if (current.computed) {
|
|
2299
|
+
if (!BabelTypes__namespace.isStringLiteral(current.property)) return null;
|
|
2300
|
+
chain.unshift(current.property.value);
|
|
2301
|
+
} else if (BabelTypes__namespace.isIdentifier(current.property)) {
|
|
2302
|
+
chain.unshift(current.property.name);
|
|
2303
|
+
} else {
|
|
2304
|
+
return null;
|
|
2305
|
+
}
|
|
2306
|
+
current = current.object;
|
|
2307
|
+
}
|
|
2308
|
+
return BabelTypes__namespace.isIdentifier(current) ? { root: current.name, path: chain } : null;
|
|
2309
|
+
}
|
|
2310
|
+
function objectProperty(obj, key) {
|
|
2311
|
+
for (const prop of obj.properties) {
|
|
2312
|
+
if (!BabelTypes__namespace.isObjectProperty(prop)) continue;
|
|
2313
|
+
const name = BabelTypes__namespace.isIdentifier(prop.key) ? prop.key.name : BabelTypes__namespace.isStringLiteral(prop.key) ? prop.key.value : null;
|
|
2314
|
+
if (name === key && BabelTypes__namespace.isExpression(prop.value)) return prop.value;
|
|
2315
|
+
}
|
|
2316
|
+
return null;
|
|
2317
|
+
}
|
|
2318
|
+
function readTsconfigAliases(rootDir) {
|
|
2319
|
+
const file = path2__namespace.default.join(rootDir, "tsconfig.json");
|
|
2320
|
+
if (!fs$1.existsSync(file)) return [];
|
|
2321
|
+
let parsed;
|
|
2322
|
+
try {
|
|
2323
|
+
const raw = stripJsonComments(fs$1.readFileSync(file, "utf-8")).replace(/,(\s*[}\]])/g, "$1");
|
|
2324
|
+
parsed = JSON.parse(raw);
|
|
2325
|
+
} catch {
|
|
2326
|
+
return [];
|
|
2327
|
+
}
|
|
2328
|
+
const paths = parsed.compilerOptions?.paths;
|
|
2329
|
+
if (!paths) return [];
|
|
2330
|
+
const baseUrl = path2__namespace.default.resolve(rootDir, parsed.compilerOptions?.baseUrl ?? ".");
|
|
2331
|
+
const out = [];
|
|
2332
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
2333
|
+
const first = targets[0];
|
|
2334
|
+
if (typeof first !== "string") continue;
|
|
2335
|
+
out.push({
|
|
2336
|
+
prefix: pattern.replace(/\/?\*$/, ""),
|
|
2337
|
+
target: path2__namespace.default.resolve(baseUrl, first.replace(/\/?\*$/, ""))
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
return out.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
2341
|
+
}
|
|
2342
|
+
function stripJsonComments(text) {
|
|
2343
|
+
let out = "";
|
|
2344
|
+
let inString = false;
|
|
2345
|
+
let escaped = false;
|
|
2346
|
+
for (let i = 0; i < text.length; i++) {
|
|
2347
|
+
const c = text[i];
|
|
2348
|
+
if (inString) {
|
|
2349
|
+
out += c;
|
|
2350
|
+
if (escaped) escaped = false;
|
|
2351
|
+
else if (c === "\\") escaped = true;
|
|
2352
|
+
else if (c === '"') inString = false;
|
|
2353
|
+
continue;
|
|
2354
|
+
}
|
|
2355
|
+
if (c === '"') {
|
|
2356
|
+
inString = true;
|
|
2357
|
+
out += c;
|
|
2358
|
+
continue;
|
|
2359
|
+
}
|
|
2360
|
+
if (c === "/" && text[i + 1] === "/") {
|
|
2361
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
2362
|
+
out += "\n";
|
|
2363
|
+
continue;
|
|
2364
|
+
}
|
|
2365
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
2366
|
+
i += 2;
|
|
2367
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
2368
|
+
i++;
|
|
2369
|
+
continue;
|
|
2370
|
+
}
|
|
2371
|
+
out += c;
|
|
2372
|
+
}
|
|
2373
|
+
return out;
|
|
2374
|
+
}
|
|
2375
|
+
function readWorkspacePackages(rootDir) {
|
|
2376
|
+
const start = path2__namespace.default.resolve(rootDir);
|
|
2377
|
+
let dir = start;
|
|
2378
|
+
for (let i = 0; i < 6; i++) {
|
|
2379
|
+
const globs = workspaceGlobs(dir);
|
|
2380
|
+
if (globs.length > 0) {
|
|
2381
|
+
const packages = expandWorkspaceGlobs(dir, globs);
|
|
2382
|
+
const containsApp = packages.some(
|
|
2383
|
+
(p) => start === p.dir || start.startsWith(p.dir + path2__namespace.default.sep)
|
|
2384
|
+
);
|
|
2385
|
+
return containsApp ? packages : [];
|
|
2386
|
+
}
|
|
2387
|
+
const parent = path2__namespace.default.dirname(dir);
|
|
2388
|
+
if (parent === dir) break;
|
|
2389
|
+
dir = parent;
|
|
2390
|
+
}
|
|
2391
|
+
return [];
|
|
2392
|
+
}
|
|
2393
|
+
function workspaceGlobs(dir) {
|
|
2394
|
+
const pnpm = path2__namespace.default.join(dir, "pnpm-workspace.yaml");
|
|
2395
|
+
if (fs$1.existsSync(pnpm)) {
|
|
2396
|
+
try {
|
|
2397
|
+
const lines = fs$1.readFileSync(pnpm, "utf-8").split(/\r?\n/);
|
|
2398
|
+
const out = [];
|
|
2399
|
+
let inPackages = false;
|
|
2400
|
+
for (const line of lines) {
|
|
2401
|
+
if (/^packages:/.test(line)) {
|
|
2402
|
+
inPackages = true;
|
|
2403
|
+
continue;
|
|
2404
|
+
}
|
|
2405
|
+
if (inPackages) {
|
|
2406
|
+
const m = /^\s*-\s*["']?([^"'#]+?)["']?\s*$/.exec(line);
|
|
2407
|
+
if (m) out.push(m[1].trim());
|
|
2408
|
+
else if (/^\S/.test(line)) break;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
if (out.length) return out;
|
|
2412
|
+
} catch {
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
const pkgPath = path2__namespace.default.join(dir, "package.json");
|
|
2416
|
+
if (!fs$1.existsSync(pkgPath)) return [];
|
|
2417
|
+
try {
|
|
2418
|
+
const pkg = JSON.parse(fs$1.readFileSync(pkgPath, "utf-8"));
|
|
2419
|
+
const ws = pkg.workspaces;
|
|
2420
|
+
if (Array.isArray(ws)) return ws;
|
|
2421
|
+
if (ws && Array.isArray(ws.packages)) return ws.packages;
|
|
2422
|
+
} catch {
|
|
2423
|
+
}
|
|
2424
|
+
return [];
|
|
2425
|
+
}
|
|
2426
|
+
function expandWorkspaceGlobs(root, globs) {
|
|
2427
|
+
const out = [];
|
|
2428
|
+
const add2 = (dir) => {
|
|
2429
|
+
const pkgPath = path2__namespace.default.join(dir, "package.json");
|
|
2430
|
+
if (!fs$1.existsSync(pkgPath)) return;
|
|
2431
|
+
try {
|
|
2432
|
+
const name = JSON.parse(fs$1.readFileSync(pkgPath, "utf-8")).name;
|
|
2433
|
+
if (name) out.push({ name, dir });
|
|
2434
|
+
} catch {
|
|
2435
|
+
}
|
|
2436
|
+
};
|
|
2437
|
+
for (const glob of globs) {
|
|
2438
|
+
if (glob.endsWith("/*")) {
|
|
2439
|
+
const parent = path2__namespace.default.join(root, glob.slice(0, -2));
|
|
2440
|
+
let entries = [];
|
|
2441
|
+
try {
|
|
2442
|
+
entries = fs$1.readdirSync(parent);
|
|
2443
|
+
} catch {
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
for (const entry of entries) {
|
|
2447
|
+
const dir = path2__namespace.default.join(parent, entry);
|
|
2448
|
+
try {
|
|
2449
|
+
if (fs$1.statSync(dir).isDirectory()) add2(dir);
|
|
2450
|
+
} catch {
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
} else if (!glob.includes("*")) {
|
|
2454
|
+
add2(path2__namespace.default.join(root, glob));
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
return out.sort((a, b) => b.name.length - a.name.length);
|
|
2458
|
+
}
|
|
2459
|
+
var ROUTE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
2460
|
+
var PLATFORM_SUFFIXES = [".ios", ".android", ".native", ".web"];
|
|
2461
|
+
var ROUTE_DIR_CANDIDATES = ["app", "src/app"];
|
|
2462
|
+
function baseName(file) {
|
|
2463
|
+
let name = file;
|
|
2464
|
+
for (const ext of ROUTE_EXTENSIONS) {
|
|
2465
|
+
if (name.endsWith(ext)) {
|
|
2466
|
+
name = name.slice(0, -ext.length);
|
|
2467
|
+
break;
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
for (const suffix of PLATFORM_SUFFIXES) {
|
|
2471
|
+
if (name.endsWith(suffix)) return name.slice(0, -suffix.length);
|
|
2472
|
+
}
|
|
2473
|
+
return name;
|
|
2474
|
+
}
|
|
2475
|
+
function isRouteFile(file) {
|
|
2476
|
+
return ROUTE_EXTENSIONS.some((ext) => file.endsWith(ext));
|
|
2477
|
+
}
|
|
2478
|
+
function isSpecial(base) {
|
|
2479
|
+
if (base === "_layout") return true;
|
|
2480
|
+
if (base.startsWith("+")) return base !== "+not-found";
|
|
2481
|
+
return base.startsWith("_");
|
|
2482
|
+
}
|
|
2483
|
+
function layoutType(file) {
|
|
2484
|
+
try {
|
|
2485
|
+
const src = fs$1.readFileSync(file, "utf-8");
|
|
2486
|
+
if (/<(?:Native)?Tabs\b/.test(src)) return "tab";
|
|
2487
|
+
if (/<Drawer\b/.test(src)) return "drawer";
|
|
2488
|
+
} catch {
|
|
2489
|
+
}
|
|
2490
|
+
return "stack";
|
|
2491
|
+
}
|
|
2492
|
+
function findLayout(dir) {
|
|
2493
|
+
for (const ext of ROUTE_EXTENSIONS) {
|
|
2494
|
+
const candidate = path2__namespace.default.join(dir, "_layout" + ext);
|
|
2495
|
+
if (fs$1.existsSync(candidate)) return candidate;
|
|
2496
|
+
}
|
|
2497
|
+
return null;
|
|
2498
|
+
}
|
|
2499
|
+
function analyzeExpoRouter(appRoot) {
|
|
2500
|
+
let routeDir = null;
|
|
2501
|
+
for (const candidate of ROUTE_DIR_CANDIDATES) {
|
|
2502
|
+
const full = path2__namespace.default.join(appRoot, candidate);
|
|
2503
|
+
if (fs$1.existsSync(full) && findLayout(full)) {
|
|
2504
|
+
routeDir = full;
|
|
2505
|
+
break;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
if (!routeDir) return null;
|
|
2509
|
+
const routes = [];
|
|
2510
|
+
const navigators = /* @__PURE__ */ new Map();
|
|
2511
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2512
|
+
const walk = (dir, segments, navigator) => {
|
|
2513
|
+
const layout = findLayout(dir);
|
|
2514
|
+
const current = layout ? segments.join("/") || "/" : navigator;
|
|
2515
|
+
if (layout && !navigators.has(current)) navigators.set(current, layoutType(layout));
|
|
2516
|
+
let entries;
|
|
2517
|
+
try {
|
|
2518
|
+
entries = fs$1.readdirSync(dir);
|
|
2519
|
+
} catch {
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
for (const entry of entries.sort()) {
|
|
2523
|
+
const full = path2__namespace.default.join(dir, entry);
|
|
2524
|
+
let isDir = false;
|
|
2525
|
+
try {
|
|
2526
|
+
isDir = fs$1.statSync(full).isDirectory();
|
|
2527
|
+
} catch {
|
|
2528
|
+
continue;
|
|
2529
|
+
}
|
|
2530
|
+
if (isDir) {
|
|
2531
|
+
if (entry === "node_modules" || entry.startsWith(".")) continue;
|
|
2532
|
+
walk(full, [...segments, entry], current);
|
|
2533
|
+
continue;
|
|
2534
|
+
}
|
|
2535
|
+
if (!isRouteFile(entry)) continue;
|
|
2536
|
+
const base = baseName(entry);
|
|
2537
|
+
if (isSpecial(base)) continue;
|
|
2538
|
+
const routeSegments = base === "index" ? segments : [...segments, base];
|
|
2539
|
+
const name = "/" + routeSegments.join("/");
|
|
2540
|
+
if (seen.has(name)) continue;
|
|
2541
|
+
seen.add(name);
|
|
2542
|
+
routes.push({
|
|
2543
|
+
name,
|
|
2544
|
+
componentFile: full,
|
|
2545
|
+
navigatorName: current,
|
|
2546
|
+
navigatorType: navigators.get(current) ?? "stack",
|
|
2547
|
+
params: routeSegments.filter((s) => s.startsWith("[") && s.endsWith("]")).map((s) => s.slice(1, -1).replace(/^\.\.\./, ""))
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
};
|
|
2551
|
+
walk(routeDir, [], "/");
|
|
2552
|
+
return {
|
|
2553
|
+
routeDir: path2__namespace.default.relative(appRoot, routeDir),
|
|
2554
|
+
routes,
|
|
2555
|
+
navigators: [...navigators.entries()].map(([name, type]) => ({ name, type }))
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// src/analyzers/NavigationAnalyzer.ts
|
|
2560
|
+
var NAVIGATOR_FACTORIES = {
|
|
2561
|
+
createStackNavigator: "stack",
|
|
2562
|
+
createNativeStackNavigator: "stack",
|
|
2563
|
+
createBottomTabNavigator: "tab",
|
|
2564
|
+
createTabNavigator: "tab",
|
|
2565
|
+
createMaterialTopTabNavigator: "tab",
|
|
2566
|
+
createMaterialBottomTabNavigator: "tab",
|
|
2567
|
+
createDrawerNavigator: "drawer"
|
|
2568
|
+
};
|
|
2569
|
+
var CUSTOM_FACTORY = /^create[A-Za-z0-9]*(Navigator|Stack|Tabs?|Drawer)$/;
|
|
2570
|
+
function unwrapStaticScreen(value) {
|
|
2571
|
+
if (BabelTypes__namespace.isCallExpression(value)) {
|
|
2572
|
+
const arg = value.arguments[0];
|
|
2573
|
+
return arg ? unwrapStaticScreen(arg) : null;
|
|
2574
|
+
}
|
|
2575
|
+
if (BabelTypes__namespace.isObjectExpression(value)) {
|
|
2576
|
+
for (const prop of value.properties) {
|
|
2577
|
+
if (!BabelTypes__namespace.isObjectProperty(prop)) continue;
|
|
2578
|
+
const key = BabelTypes__namespace.isIdentifier(prop.key) ? prop.key.name : null;
|
|
2579
|
+
if (key === "screen" && BabelTypes__namespace.isExpression(prop.value)) return prop.value;
|
|
2580
|
+
}
|
|
2581
|
+
return null;
|
|
2582
|
+
}
|
|
2583
|
+
return BabelTypes__namespace.isExpression(value) ? value : null;
|
|
2584
|
+
}
|
|
2585
|
+
function inferNavigatorType(factoryName) {
|
|
2586
|
+
if (/drawer/i.test(factoryName)) return "drawer";
|
|
2587
|
+
if (/tab/i.test(factoryName)) return "tab";
|
|
2588
|
+
return "stack";
|
|
2589
|
+
}
|
|
2590
|
+
var NAVIGATION_EVIDENCE = new RegExp(
|
|
2591
|
+
// `\.Navigator`/`\.Screen` entram porque o JSX pode estar num arquivo que
|
|
2592
|
+
// não menciona fábrica nenhuma: `comapeo` declara `RootStack` em
|
|
2593
|
+
// `Stack/RootStack.ts` e escreve todas as 120 rotas em `Stack/index.tsx`,
|
|
2594
|
+
// que não cita `createNativeStackNavigator` uma vez sequer. Sem isto a
|
|
2595
|
+
// junção entre arquivos nunca chega a ser tentada.
|
|
2596
|
+
// Sem `\\b` depois da palavra — `createNativeStackNavigatorWithAuth` não tem
|
|
2597
|
+
// fronteira ali, e exigi-la fecharia o portão para o arquivo que declara os
|
|
2598
|
+
// seis navegadores do `bluesky`.
|
|
2599
|
+
`create[A-Za-z0-9]*(?:Navigator|Stack|Tabs?|Drawer)|ParamList|\\.Navigator\\b|\\.Screen\\b`
|
|
2600
|
+
);
|
|
1663
2601
|
var NavigationAnalyzer = class {
|
|
1664
2602
|
config;
|
|
1665
2603
|
/** Extra glob patterns for navigation file discovery (added to defaults) */
|
|
1666
2604
|
navigationInclude;
|
|
1667
2605
|
/** Extra glob patterns to exclude from navigation analysis */
|
|
1668
2606
|
navigationExclude;
|
|
2607
|
+
/** Exposto para a composição reusar o cache de AST em vez de reparsear a árvore. */
|
|
2608
|
+
graph;
|
|
2609
|
+
/**
|
|
2610
|
+
* Rota → arquivo do componente que ela monta. É a única ligação entre o grafo
|
|
2611
|
+
* de navegação e a árvore de telas; sem ela as duas metades do documento
|
|
2612
|
+
* falam de coisas diferentes. Populada por `analyze()`.
|
|
2613
|
+
*/
|
|
2614
|
+
routeTargets = /* @__PURE__ */ new Map();
|
|
2615
|
+
/**
|
|
2616
|
+
* Arquivos que DECLARAM uma fábrica de navegador.
|
|
2617
|
+
*
|
|
2618
|
+
* Um navegador aninhado é montado como `component=` de uma rota — no
|
|
2619
|
+
* `apps/example-app`, `<Stack.Screen name="Main" component={MainTabNavigator} />`.
|
|
2620
|
+
* Promover esse arquivo a tela criaria seis "telas" sem uma única ação, que é
|
|
2621
|
+
* exatamente o falso positivo que o corpus existe para não repetir. Um
|
|
2622
|
+
* navegador é um contêiner de rotas; a tela está um nível abaixo.
|
|
2623
|
+
*
|
|
2624
|
+
* Entram aqui os dois lados: o arquivo que CHAMA a fábrica e o arquivo que
|
|
2625
|
+
* RENDERIZA `<X.Navigator>`. Não são o mesmo — `bluewallet` declara
|
|
2626
|
+
* `DetailViewStack` num arquivo e escreve o JSX em
|
|
2627
|
+
* `navigation/DetailViewScreensStack.tsx`, e checar só o primeiro deixava o
|
|
2628
|
+
* segundo entrar como tela.
|
|
2629
|
+
*/
|
|
2630
|
+
navigatorFiles = /* @__PURE__ */ new Set();
|
|
2631
|
+
/** Preenchido por `analyze()`. Vazio antes disso. */
|
|
2632
|
+
diagnostics = {
|
|
2633
|
+
filesScanned: 0,
|
|
2634
|
+
filesWithEvidence: 0,
|
|
2635
|
+
navigatorsDeclared: 0,
|
|
2636
|
+
navigatorsWithJsx: 0,
|
|
2637
|
+
routes: 0,
|
|
2638
|
+
routesLinkedToFile: 0
|
|
2639
|
+
};
|
|
1669
2640
|
constructor(config, options) {
|
|
1670
2641
|
this.config = config;
|
|
2642
|
+
this.graph = new ModuleGraph(config.rootDir);
|
|
1671
2643
|
this.navigationInclude = options?.navigationInclude ?? [];
|
|
1672
2644
|
this.navigationExclude = options?.navigationExclude ?? [];
|
|
1673
2645
|
}
|
|
1674
2646
|
/** Build the full navigation graph */
|
|
1675
2647
|
async analyze() {
|
|
1676
2648
|
const navigationFiles = await this.findNavigationFiles();
|
|
1677
|
-
const parsedNavigators = [];
|
|
1678
2649
|
const typeExports = [];
|
|
2650
|
+
this.routeTargets.clear();
|
|
2651
|
+
const declarations = /* @__PURE__ */ new Map();
|
|
2652
|
+
const evidenceFiles = [];
|
|
2653
|
+
this.navigatorFiles.clear();
|
|
1679
2654
|
for (const filePath of navigationFiles) {
|
|
1680
2655
|
try {
|
|
1681
2656
|
const content = await fs$1.promises.readFile(filePath, "utf-8");
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
2657
|
+
if (!NAVIGATION_EVIDENCE.test(content)) continue;
|
|
2658
|
+
evidenceFiles.push(filePath);
|
|
2659
|
+
for (const decl of this.collectDeclarations(filePath)) {
|
|
2660
|
+
declarations.set(`${decl.file}#${decl.name}`, decl);
|
|
2661
|
+
if (decl.confident) this.navigatorFiles.add(decl.file);
|
|
2662
|
+
}
|
|
2663
|
+
typeExports.push(...this.parseParamTypes(content, filePath));
|
|
1686
2664
|
} catch (error) {
|
|
1687
2665
|
console.warn(`Failed to parse ${filePath}:`, error);
|
|
1688
2666
|
}
|
|
1689
2667
|
}
|
|
2668
|
+
const parsedNavigators = [];
|
|
2669
|
+
for (const filePath of evidenceFiles) {
|
|
2670
|
+
try {
|
|
2671
|
+
parsedNavigators.push(...this.parseNavigatorUsages(filePath, declarations));
|
|
2672
|
+
} catch (error) {
|
|
2673
|
+
console.warn(`Failed to parse navigators in ${filePath}:`, error);
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
let detachedScreens = 0;
|
|
2677
|
+
for (const filePath of evidenceFiles) {
|
|
2678
|
+
try {
|
|
2679
|
+
for (const detached of this.parseDetachedScreens(filePath, declarations)) {
|
|
2680
|
+
const existing = parsedNavigators.find(
|
|
2681
|
+
(nav) => nav.name === detached.name && nav.type === detached.type
|
|
2682
|
+
);
|
|
2683
|
+
if (!existing) {
|
|
2684
|
+
parsedNavigators.push(detached);
|
|
2685
|
+
detachedScreens += detached.screens.length;
|
|
2686
|
+
continue;
|
|
2687
|
+
}
|
|
2688
|
+
const seen = new Set(existing.screens.map((screen) => screen.name));
|
|
2689
|
+
for (const screen of detached.screens) {
|
|
2690
|
+
if (seen.has(screen.name)) continue;
|
|
2691
|
+
seen.add(screen.name);
|
|
2692
|
+
existing.screens.push(screen);
|
|
2693
|
+
detachedScreens++;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
} catch (error) {
|
|
2697
|
+
console.warn(`Failed to parse detached screens in ${filePath}:`, error);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
const expo = analyzeExpoRouter(this.config.rootDir);
|
|
2701
|
+
if (expo) {
|
|
2702
|
+
console.log(
|
|
2703
|
+
`[NavigationAnalyzer] expo-router em "${expo.routeDir}": ${expo.routes.length} rotas, ${expo.navigators.length} layouts`
|
|
2704
|
+
);
|
|
2705
|
+
this.diagnostics.fileBasedRouter = {
|
|
2706
|
+
kind: "expo-router",
|
|
2707
|
+
routeDir: expo.routeDir,
|
|
2708
|
+
routes: expo.routes.length
|
|
2709
|
+
};
|
|
2710
|
+
const byNavigator = /* @__PURE__ */ new Map();
|
|
2711
|
+
for (const nav of expo.navigators) {
|
|
2712
|
+
byNavigator.set(nav.name, { name: nav.name, type: nav.type, screens: [] });
|
|
2713
|
+
}
|
|
2714
|
+
for (const route of expo.routes) {
|
|
2715
|
+
const navigator = byNavigator.get(route.navigatorName) ?? byNavigator.set(route.navigatorName, {
|
|
2716
|
+
name: route.navigatorName,
|
|
2717
|
+
type: route.navigatorType,
|
|
2718
|
+
screens: []
|
|
2719
|
+
}).get(route.navigatorName);
|
|
2720
|
+
navigator.screens.push({
|
|
2721
|
+
name: route.name,
|
|
2722
|
+
navigatorName: route.navigatorName,
|
|
2723
|
+
navigatorType: route.navigatorType,
|
|
2724
|
+
componentFile: route.componentFile,
|
|
2725
|
+
...route.params.length ? {
|
|
2726
|
+
params: route.params.map((name) => ({ name, type: "string", required: true }))
|
|
2727
|
+
} : {}
|
|
2728
|
+
});
|
|
2729
|
+
this.routeTargets.set(route.name, route.componentFile);
|
|
2730
|
+
}
|
|
2731
|
+
parsedNavigators.push(...byNavigator.values());
|
|
2732
|
+
}
|
|
2733
|
+
let staticOnly = 0;
|
|
2734
|
+
for (const decl of declarations.values()) {
|
|
2735
|
+
if (!decl.confident || !decl.staticScreens?.length) continue;
|
|
2736
|
+
const existing = parsedNavigators.find((n) => n.name === decl.name && n.type === decl.type);
|
|
2737
|
+
if (existing) {
|
|
2738
|
+
const seen = new Set(existing.screens.map((s) => s.name));
|
|
2739
|
+
for (const screen of decl.staticScreens) {
|
|
2740
|
+
if (!seen.has(screen.name)) existing.screens.push(screen);
|
|
2741
|
+
}
|
|
2742
|
+
continue;
|
|
2743
|
+
}
|
|
2744
|
+
parsedNavigators.push({ name: decl.name, type: decl.type, screens: decl.staticScreens });
|
|
2745
|
+
staticOnly++;
|
|
2746
|
+
}
|
|
2747
|
+
const confidentDeclarations = [...declarations.values()].filter((d) => d.confident).length;
|
|
2748
|
+
const uniqueRoutes = new Set(
|
|
2749
|
+
parsedNavigators.flatMap((nav) => nav.screens.map((screen) => screen.name))
|
|
2750
|
+
);
|
|
2751
|
+
this.diagnostics = {
|
|
2752
|
+
...this.diagnostics,
|
|
2753
|
+
filesScanned: navigationFiles.length,
|
|
2754
|
+
filesWithEvidence: evidenceFiles.length,
|
|
2755
|
+
navigatorsDeclared: confidentDeclarations,
|
|
2756
|
+
navigatorsWithJsx: parsedNavigators.length,
|
|
2757
|
+
routes: uniqueRoutes.size,
|
|
2758
|
+
// `routeTargets` é indexado por nome de rota, e uma rota pode ter sido
|
|
2759
|
+
// registrada por um navegador que não sobreviveu. O mínimo evita a
|
|
2760
|
+
// cobertura acima de 100% que a medição do corpus expôs no `coopcycle`.
|
|
2761
|
+
routesLinkedToFile: Math.min(this.routeTargets.size, uniqueRoutes.size)
|
|
2762
|
+
};
|
|
2763
|
+
console.log(
|
|
2764
|
+
`[NavigationAnalyzer] ${staticOnly} navegador(es) s\xF3 de config est\xE1tica \xB7 ${detachedScreens} rota(s) fora de um <X.Navigator> \xB7 ${navigationFiles.length} arquivos varridos, ${evidenceFiles.length} com evid\xEAncia, ${confidentDeclarations} navegadores declarados, ${parsedNavigators.length} com JSX, ${this.routeTargets.size} rotas ligadas a um arquivo`
|
|
2765
|
+
);
|
|
1690
2766
|
this.attachParamsToNavigators(parsedNavigators, typeExports);
|
|
1691
2767
|
return this.buildNavigationGraph(parsedNavigators);
|
|
1692
2768
|
}
|
|
1693
|
-
/**
|
|
2769
|
+
/** Fase 1: as fábricas `create*Navigator()` atribuídas a um nome neste arquivo. */
|
|
2770
|
+
collectDeclarations(filePath) {
|
|
2771
|
+
const ast = this.graph.parse(filePath);
|
|
2772
|
+
if (!ast) return [];
|
|
2773
|
+
const out = /* @__PURE__ */ new Map();
|
|
2774
|
+
traverse5__default.default(ast, {
|
|
2775
|
+
CallExpression: (nodePath) => {
|
|
2776
|
+
const callee = nodePath.node.callee;
|
|
2777
|
+
if (!BabelTypes__namespace.isIdentifier(callee)) return;
|
|
2778
|
+
const known = NAVIGATOR_FACTORIES[callee.name] ?? (CUSTOM_FACTORY.test(callee.name) ? inferNavigatorType(callee.name) : null);
|
|
2779
|
+
const type = known ?? inferNavigatorType(callee.name);
|
|
2780
|
+
const confident = known !== null;
|
|
2781
|
+
let up = nodePath.parentPath;
|
|
2782
|
+
for (let i = 0; i < 4 && up; i++) {
|
|
2783
|
+
if (BabelTypes__namespace.isVariableDeclarator(up.node)) break;
|
|
2784
|
+
if (!BabelTypes__namespace.isMemberExpression(up.node) && !BabelTypes__namespace.isCallExpression(up.node)) {
|
|
2785
|
+
up = null;
|
|
2786
|
+
break;
|
|
2787
|
+
}
|
|
2788
|
+
up = up.parentPath;
|
|
2789
|
+
}
|
|
2790
|
+
const declarator = up?.node;
|
|
2791
|
+
if (!declarator || !BabelTypes__namespace.isVariableDeclarator(declarator) || !BabelTypes__namespace.isIdentifier(declarator.id)) {
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2794
|
+
const name = declarator.id.name;
|
|
2795
|
+
if (out.get(name)?.confident) return;
|
|
2796
|
+
const staticScreens = confident ? this.parseStaticScreens(filePath, nodePath.node.arguments[0], name, type) : [];
|
|
2797
|
+
out.set(name, {
|
|
2798
|
+
file: filePath,
|
|
2799
|
+
name,
|
|
2800
|
+
type,
|
|
2801
|
+
confident,
|
|
2802
|
+
...staticScreens.length ? { staticScreens } : {}
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2805
|
+
});
|
|
2806
|
+
return [...out.values()];
|
|
2807
|
+
}
|
|
2808
|
+
/**
|
|
2809
|
+
* As rotas de `createXNavigator({ screens: { … } })`.
|
|
2810
|
+
*
|
|
2811
|
+
* Quatro formas no corpus, todas em `rocketchat`:
|
|
2812
|
+
* `NewServerView` — shorthand, nome = componente
|
|
2813
|
+
* `LoginView: createNativeStackScreen({ screen })` — embrulho da própria lib
|
|
2814
|
+
* `SelectListView: SelectListViewScreen` — identificador direto
|
|
2815
|
+
* `ChatsStackNavigator: ChatsStack` — navegador aninhado
|
|
2816
|
+
*
|
|
2817
|
+
* `groups: { G: { screens: { … } } }` também entra: faz parte da API estática
|
|
2818
|
+
* e agrupar rotas não muda o que elas são.
|
|
2819
|
+
*/
|
|
2820
|
+
parseStaticScreens(filePath, config, navigatorName, navigatorType, depth = 0) {
|
|
2821
|
+
if (!config || !BabelTypes__namespace.isObjectExpression(config) || depth > 4) return [];
|
|
2822
|
+
const out = [];
|
|
2823
|
+
for (const prop of config.properties) {
|
|
2824
|
+
if (!BabelTypes__namespace.isObjectProperty(prop)) continue;
|
|
2825
|
+
const key = BabelTypes__namespace.isIdentifier(prop.key) ? prop.key.name : BabelTypes__namespace.isStringLiteral(prop.key) ? prop.key.value : null;
|
|
2826
|
+
if (!key) continue;
|
|
2827
|
+
if (key === "groups" && BabelTypes__namespace.isObjectExpression(prop.value)) {
|
|
2828
|
+
for (const group of prop.value.properties) {
|
|
2829
|
+
if (!BabelTypes__namespace.isObjectProperty(group) || !BabelTypes__namespace.isExpression(group.value)) continue;
|
|
2830
|
+
out.push(
|
|
2831
|
+
...this.parseStaticScreens(
|
|
2832
|
+
filePath,
|
|
2833
|
+
group.value,
|
|
2834
|
+
navigatorName,
|
|
2835
|
+
navigatorType,
|
|
2836
|
+
depth + 1
|
|
2837
|
+
)
|
|
2838
|
+
);
|
|
2839
|
+
}
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
if (key !== "screens" || !BabelTypes__namespace.isObjectExpression(prop.value)) continue;
|
|
2843
|
+
for (const entry of prop.value.properties) {
|
|
2844
|
+
if (!BabelTypes__namespace.isObjectProperty(entry)) continue;
|
|
2845
|
+
const routeName = BabelTypes__namespace.isIdentifier(entry.key) ? entry.key.name : BabelTypes__namespace.isStringLiteral(entry.key) ? entry.key.value : null;
|
|
2846
|
+
if (!routeName) continue;
|
|
2847
|
+
const componentExpr = unwrapStaticScreen(entry.value);
|
|
2848
|
+
const componentFile = componentExpr ? this.graph.componentFile(filePath, componentExpr) : null;
|
|
2849
|
+
if (componentFile) this.routeTargets.set(routeName, componentFile);
|
|
2850
|
+
out.push({
|
|
2851
|
+
name: routeName,
|
|
2852
|
+
navigatorName,
|
|
2853
|
+
navigatorType,
|
|
2854
|
+
...componentFile ? { componentFile } : {}
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
return out;
|
|
2859
|
+
}
|
|
2860
|
+
/**
|
|
2861
|
+
* Todo arquivo-fonte do projeto — não os que ficam num diretório com o nome
|
|
2862
|
+
* certo.
|
|
2863
|
+
*
|
|
2864
|
+
* POR QUE ISTO MUDOU. A lista anterior era `**\/navigation/**`,
|
|
2865
|
+
* `**\/navigator*` e `**\/routes*`, o que fazia da descoberta de navegação uma
|
|
2866
|
+
* convenção de caminho. Medido contra 20 apps React Native de terceiros
|
|
2867
|
+
* (`qa/eval-corpus`), o filtro errava por motivos que nada têm a ver com o
|
|
2868
|
+
* app não ter navegação:
|
|
2869
|
+
*
|
|
2870
|
+
* - `pocketpal` declara 4 navegadores em `App.tsx` e dentro de `src/screens/`;
|
|
2871
|
+
* - `comapeo` usa `src/frontend/Navigation/` — `N` maiúsculo, e o glob é
|
|
2872
|
+
* sensível a caixa;
|
|
2873
|
+
* - `abacus` usa `src/routes/index.tsx` — `routes` é o DIRETÓRIO, e o glob
|
|
2874
|
+
* pedia um arquivo chamado `routes*`;
|
|
2875
|
+
* - `discourse` declara em `js/Discourse.js` — o glob só aceitava `.ts`/`.tsx`;
|
|
2876
|
+
* - `rainbow` tem 11 arquivos com navegador e o glob alcançava 1.
|
|
2877
|
+
*
|
|
2878
|
+
* Varrer tudo não custa uma varredura nova: `ReactNativePlatformAnalyzer` já
|
|
2879
|
+
* globa e parseia a árvore inteira para `ComponentAnalyzer` e `FormAnalyzer`.
|
|
2880
|
+
* O que segura o custo aqui é o portão por evidência em `analyze()`, que só
|
|
2881
|
+
* parseia arquivo cujo texto menciona uma fábrica de navegador.
|
|
2882
|
+
*
|
|
2883
|
+
* Os globs antigos continuam na lista: se um projeto restringir `include`, o
|
|
2884
|
+
* que era encontrado antes continua sendo.
|
|
2885
|
+
*/
|
|
1694
2886
|
async findNavigationFiles() {
|
|
1695
2887
|
const patterns = [
|
|
2888
|
+
...this.config.include ?? ["**/*.{ts,tsx,js,jsx}"],
|
|
2889
|
+
// Piso de compatibilidade — nunca encontrar MENOS que a versão anterior.
|
|
1696
2890
|
"**/navigation/**/*.{ts,tsx}",
|
|
1697
2891
|
"**/navigator*.{ts,tsx}",
|
|
1698
2892
|
"**/routes*.{ts,tsx}",
|
|
@@ -1703,91 +2897,188 @@ var NavigationAnalyzer = class {
|
|
|
1703
2897
|
"**/node_modules/**",
|
|
1704
2898
|
"**/dist/**",
|
|
1705
2899
|
"**/build/**",
|
|
2900
|
+
// Um navegador declarado dentro de um teste é fixture, não a navegação
|
|
2901
|
+
// do app. Isto passou a importar quando a varredura deixou de ser por
|
|
2902
|
+
// caminho: em `expensify`, os únicos arquivos que o glob antigo
|
|
2903
|
+
// alcançava eram três de `tests/`.
|
|
2904
|
+
"**/__tests__/**",
|
|
2905
|
+
"**/test/**",
|
|
2906
|
+
"**/tests/**",
|
|
2907
|
+
"**/*.test.{ts,tsx,js,jsx}",
|
|
2908
|
+
"**/*.tests.{ts,tsx,js,jsx}",
|
|
2909
|
+
"**/*.spec.{ts,tsx,js,jsx}",
|
|
2910
|
+
// Sufixo `Test` sem ponto: `expensify` chama os fixtures dele de
|
|
2911
|
+
// `LegalNameStepTest.tsx`, e cada um monta um `<Stack.Screen>` próprio.
|
|
2912
|
+
// Auditando as 144 rotas lidas dele contra `SCREENS.ts`, 2 vinham daqui —
|
|
2913
|
+
// destinos que existem no teste e não no app, para os quais o agente
|
|
2914
|
+
// tentaria navegar.
|
|
2915
|
+
"**/*Test.{ts,tsx,js,jsx}",
|
|
2916
|
+
"**/*Tests.{ts,tsx,js,jsx}",
|
|
1706
2917
|
...this.config.exclude || [],
|
|
1707
2918
|
// §5: Add user-configured navigation excludes
|
|
1708
2919
|
...this.navigationExclude
|
|
1709
2920
|
];
|
|
1710
|
-
const files = await
|
|
2921
|
+
const files = await globSorted(patterns, {
|
|
1711
2922
|
cwd: this.config.rootDir,
|
|
1712
2923
|
ignore: excludePatterns
|
|
1713
2924
|
});
|
|
1714
|
-
return files.map((file) =>
|
|
2925
|
+
return files.map((file) => path2__namespace.default.join(this.config.rootDir, file));
|
|
1715
2926
|
}
|
|
1716
|
-
/**
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
traverse4__default.default(ast, {
|
|
1731
|
-
// Detect createStackNavigator / createTabNavigator / createDrawerNavigator calls
|
|
1732
|
-
CallExpression: (nodePath) => {
|
|
1733
|
-
const { node } = nodePath;
|
|
1734
|
-
const callee = node.callee;
|
|
1735
|
-
let navigatorType = null;
|
|
1736
|
-
if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createNativeStackNavigator") {
|
|
1737
|
-
navigatorType = "stack";
|
|
1738
|
-
} else if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createStackNavigator") {
|
|
1739
|
-
navigatorType = "stack";
|
|
1740
|
-
} else if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createBottomTabNavigator") {
|
|
1741
|
-
navigatorType = "tab";
|
|
1742
|
-
} else if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createTabNavigator") {
|
|
1743
|
-
navigatorType = "tab";
|
|
1744
|
-
} else if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createDrawerNavigator") {
|
|
1745
|
-
navigatorType = "drawer";
|
|
1746
|
-
}
|
|
1747
|
-
if (navigatorType) {
|
|
1748
|
-
const parent = nodePath.parent;
|
|
1749
|
-
if (BabelTypes__namespace.isVariableDeclarator(parent) && BabelTypes__namespace.isIdentifier(parent.id)) {
|
|
1750
|
-
const varName = parent.id.name;
|
|
1751
|
-
navigatorCalls.set(varName, {
|
|
1752
|
-
name: varName,
|
|
1753
|
-
type: navigatorType,
|
|
1754
|
-
screens: []
|
|
1755
|
-
});
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
},
|
|
1759
|
-
// Detect Stack.Navigator / Tab.Navigator JSX elements
|
|
1760
|
-
JSXElement: (nodePath) => {
|
|
1761
|
-
const { node } = nodePath;
|
|
1762
|
-
const openingElement = node.openingElement;
|
|
1763
|
-
if (BabelTypes__namespace.isJSXMemberExpression(openingElement.name) && BabelTypes__namespace.isJSXIdentifier(openingElement.name.object)) {
|
|
1764
|
-
const objectName = openingElement.name.object.name;
|
|
1765
|
-
const propertyName = BabelTypes__namespace.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
|
|
1766
|
-
if (propertyName === "Navigator") {
|
|
1767
|
-
const navigator = navigatorCalls.get(objectName);
|
|
1768
|
-
if (navigator) {
|
|
1769
|
-
const initialRouteAttr = openingElement.attributes.find(
|
|
1770
|
-
(attr) => BabelTypes__namespace.isJSXAttribute(attr) && BabelTypes__namespace.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
|
|
1771
|
-
);
|
|
1772
|
-
if (BabelTypes__namespace.isJSXAttribute(initialRouteAttr) && BabelTypes__namespace.isStringLiteral(initialRouteAttr.value)) {
|
|
1773
|
-
navigator.initialRouteName = initialRouteAttr.value.value;
|
|
1774
|
-
}
|
|
1775
|
-
const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
|
|
1776
|
-
screensByNavigator.set(objectName, screens);
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
2927
|
+
/**
|
|
2928
|
+
* Fase 2: o JSX `<X.Navigator>` deste arquivo, ligado à declaração de `X` —
|
|
2929
|
+
* que pode estar aqui ou em qualquer arquivo que este importe.
|
|
2930
|
+
*/
|
|
2931
|
+
parseNavigatorUsages(filePath, declarations) {
|
|
2932
|
+
const ast = this.graph.parse(filePath);
|
|
2933
|
+
if (!ast) return [];
|
|
2934
|
+
const found = /* @__PURE__ */ new Map();
|
|
2935
|
+
traverse5__default.default(ast, {
|
|
2936
|
+
JSXElement: (nodePath) => {
|
|
2937
|
+
const { node } = nodePath;
|
|
2938
|
+
const openingElement = node.openingElement;
|
|
2939
|
+
if (!BabelTypes__namespace.isJSXMemberExpression(openingElement.name) || !BabelTypes__namespace.isJSXIdentifier(openingElement.name.object)) {
|
|
2940
|
+
return;
|
|
1780
2941
|
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
2942
|
+
const objectName = openingElement.name.object.name;
|
|
2943
|
+
const propertyName = BabelTypes__namespace.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
|
|
2944
|
+
if (propertyName !== "Navigator") return;
|
|
2945
|
+
const decl = this.resolveNavigator(filePath, objectName, declarations);
|
|
2946
|
+
if (!decl) return;
|
|
2947
|
+
const navigator = found.get(objectName) ?? {
|
|
2948
|
+
name: objectName,
|
|
2949
|
+
type: decl.type,
|
|
2950
|
+
screens: []
|
|
2951
|
+
};
|
|
2952
|
+
const initialRouteAttr = openingElement.attributes.find(
|
|
2953
|
+
(attr) => BabelTypes__namespace.isJSXAttribute(attr) && BabelTypes__namespace.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
|
|
2954
|
+
);
|
|
2955
|
+
if (BabelTypes__namespace.isJSXAttribute(initialRouteAttr)) {
|
|
2956
|
+
const initial = this.attributeString(filePath, initialRouteAttr);
|
|
2957
|
+
if (initial) navigator.initialRouteName = initial;
|
|
2958
|
+
}
|
|
2959
|
+
this.navigatorFiles.add(filePath);
|
|
2960
|
+
this.navigatorFiles.add(decl.file);
|
|
2961
|
+
navigator.screens = this.extractScreensFromNavigator(filePath, node, objectName, decl.type);
|
|
2962
|
+
found.set(objectName, navigator);
|
|
2963
|
+
}
|
|
2964
|
+
});
|
|
2965
|
+
return [...found.values()];
|
|
2966
|
+
}
|
|
2967
|
+
/**
|
|
2968
|
+
* Rotas declaradas FORA de qualquer `<X.Navigator>`.
|
|
2969
|
+
*
|
|
2970
|
+
* O CASO. A fase 2 desce a partir do `<X.Navigator>` e lê as `<X.Screen>`
|
|
2971
|
+
* que estão DENTRO dele. Dois alvos do corpus não escrevem assim, e entre os
|
|
2972
|
+
* dois são 171 rotas invisíveis:
|
|
2973
|
+
*
|
|
2974
|
+
* `bluesky` — `function commonScreens(Stack: typeof Flat) { return (<>
|
|
2975
|
+
* <Stack.Screen name="NotFound" … /> … </>) }`, chamada de
|
|
2976
|
+
* dentro de seis navegadores diferentes. 70 rotas.
|
|
2977
|
+
* `comapeo` — `export const createAppScreens = ({intl}) => (<>
|
|
2978
|
+
* <RootStack.Group><RootStack.Screen … /></RootStack.Group></>)`,
|
|
2979
|
+
* num arquivo sem `<RootStack.Navigator>` nenhum. 101 rotas.
|
|
2980
|
+
*
|
|
2981
|
+
* A REGRA. Uma `<X.Screen name="…">` sem `<Y.Navigator>` ancestral é rota do
|
|
2982
|
+
* navegador ao qual `X` se resolve. Não há palpite em jogo: o nome da rota é
|
|
2983
|
+
* literal do fonte (ou constante que o resolvedor segue), e `X` precisa
|
|
2984
|
+
* chegar a uma declaração de navegador que já existe. O que não resolve não
|
|
2985
|
+
* vira nada.
|
|
2986
|
+
*
|
|
2987
|
+
* O ancestral é o que evita contar duas vezes — dentro do `<X.Navigator>` a
|
|
2988
|
+
* fase 2 já leu, e somar aqui duplicaria cada rota do corpus inteiro.
|
|
2989
|
+
*
|
|
2990
|
+
* DUAS FORMAS DE RESOLVER `X`, e a segunda é o que o `bluesky` exige. A
|
|
2991
|
+
* primeira é a de sempre (declaração local, ou `import` seguido até a
|
|
2992
|
+
* origem) e resolve o `comapeo`. A segunda lê a ANOTAÇÃO DE TIPO do
|
|
2993
|
+
* parâmetro: em `commonScreens(Stack: typeof Flat)`, quem diz que `Stack` é
|
|
2994
|
+
* o navegador `Flat` é o próprio app, no fonte.
|
|
2995
|
+
*
|
|
2996
|
+
* O QUE ISTO NÃO FAZ. As 70 do `bluesky` ficam atribuídas a `Flat` — que as
|
|
2997
|
+
* contém de fato (`{commonScreens(Flat, numUnread)}`) — e não aos outros
|
|
2998
|
+
* cinco stacks que também chamam a mesma função. Seguir os seis pontos de
|
|
2999
|
+
* chamada é análise interprocedural, e o ganho seria só de atribuição: o nome
|
|
3000
|
+
* da rota e o arquivo da tela, que é o que o agente usa, já saem certos.
|
|
3001
|
+
*/
|
|
3002
|
+
parseDetachedScreens(filePath, declarations) {
|
|
3003
|
+
const ast = this.graph.parse(filePath);
|
|
3004
|
+
if (!ast) return [];
|
|
3005
|
+
const byNavigator = /* @__PURE__ */ new Map();
|
|
3006
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
3007
|
+
traverse5__default.default(ast, {
|
|
3008
|
+
JSXElement: (nodePath) => {
|
|
3009
|
+
const name = nodePath.node.openingElement.name;
|
|
3010
|
+
if (!BabelTypes__namespace.isJSXMemberExpression(name) || !BabelTypes__namespace.isJSXIdentifier(name.object)) return;
|
|
3011
|
+
if (!BabelTypes__namespace.isJSXIdentifier(name.property) || name.property.name !== "Screen") return;
|
|
3012
|
+
const insideNavigator = nodePath.findParent((parent) => {
|
|
3013
|
+
if (!parent.isJSXElement()) return false;
|
|
3014
|
+
const parentName = parent.node.openingElement.name;
|
|
3015
|
+
return BabelTypes__namespace.isJSXMemberExpression(parentName) && BabelTypes__namespace.isJSXIdentifier(parentName.property) && parentName.property.name === "Navigator";
|
|
3016
|
+
});
|
|
3017
|
+
if (insideNavigator) return;
|
|
3018
|
+
const local = name.object.name;
|
|
3019
|
+
if (!resolved.has(local)) {
|
|
3020
|
+
resolved.set(
|
|
3021
|
+
local,
|
|
3022
|
+
this.resolveDetachedNavigator(filePath, local, nodePath, declarations)
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
const decl = resolved.get(local);
|
|
3026
|
+
if (!decl) return;
|
|
3027
|
+
const screen = this.parseScreenElement(filePath, nodePath.node, decl.name, decl.type);
|
|
3028
|
+
if (!screen) return;
|
|
3029
|
+
const nav = byNavigator.get(decl.name) ?? {
|
|
3030
|
+
name: decl.name,
|
|
3031
|
+
type: decl.type,
|
|
3032
|
+
screens: []
|
|
3033
|
+
};
|
|
3034
|
+
if (!nav.screens.some((existing) => existing.name === screen.name))
|
|
3035
|
+
nav.screens.push(screen);
|
|
3036
|
+
byNavigator.set(decl.name, nav);
|
|
3037
|
+
}
|
|
3038
|
+
});
|
|
3039
|
+
return [...byNavigator.values()];
|
|
3040
|
+
}
|
|
3041
|
+
/**
|
|
3042
|
+
* De um `X` usado como `<X.Screen>` até a declaração do navegador.
|
|
3043
|
+
*
|
|
3044
|
+
* Além do caminho normal — declaração local ou `import` seguido até a origem
|
|
3045
|
+
* — aceita `X` como PARÂMETRO anotado com `typeof Y`. É o idioma do
|
|
3046
|
+
* `bluesky`, e a anotação é declaração do app: nada aqui é inferido do nome.
|
|
3047
|
+
*/
|
|
3048
|
+
resolveDetachedNavigator(filePath, localName, nodePath, declarations) {
|
|
3049
|
+
const direct = this.resolveNavigator(filePath, localName, declarations);
|
|
3050
|
+
if (direct) return direct;
|
|
3051
|
+
const binding = nodePath.scope.getBinding(localName);
|
|
3052
|
+
if (!binding || binding.kind !== "param") return null;
|
|
3053
|
+
const param = binding.path.node;
|
|
3054
|
+
if (!BabelTypes__namespace.isIdentifier(param) || !BabelTypes__namespace.isTSTypeAnnotation(param.typeAnnotation)) return null;
|
|
3055
|
+
const annotation = param.typeAnnotation.typeAnnotation;
|
|
3056
|
+
if (!BabelTypes__namespace.isTSTypeQuery(annotation) || !BabelTypes__namespace.isIdentifier(annotation.exprName)) return null;
|
|
3057
|
+
return this.resolveNavigator(filePath, annotation.exprName.name, declarations);
|
|
3058
|
+
}
|
|
3059
|
+
/**
|
|
3060
|
+
* De `<X.Navigator>` até a declaração de `X`.
|
|
3061
|
+
*
|
|
3062
|
+
* Primeiro no próprio arquivo; se não estiver, segue o `import` até onde `X`
|
|
3063
|
+
* é definido. Resolver pelo import é o que torna a junção entre arquivos
|
|
3064
|
+
* segura: dois `Stack` de arquivos diferentes nunca colidem, porque a chave
|
|
3065
|
+
* é o arquivo de DEFINIÇÃO.
|
|
3066
|
+
*/
|
|
3067
|
+
resolveNavigator(filePath, localName, declarations) {
|
|
3068
|
+
const local = declarations.get(`${filePath}#${localName}`);
|
|
3069
|
+
if (local) return local;
|
|
3070
|
+
const origin = this.graph.resolveBinding(filePath, localName);
|
|
3071
|
+
if (!origin) return null;
|
|
3072
|
+
return declarations.get(`${origin.file}#${origin.name}`) ?? null;
|
|
3073
|
+
}
|
|
3074
|
+
/** O valor string de um atributo JSX, resolvendo constante importada. */
|
|
3075
|
+
attributeString(filePath, attr) {
|
|
3076
|
+
const value = attr.value;
|
|
3077
|
+
if (BabelTypes__namespace.isStringLiteral(value)) return value.value;
|
|
3078
|
+
if (BabelTypes__namespace.isJSXExpressionContainer(value) && BabelTypes__namespace.isExpression(value.expression)) {
|
|
3079
|
+
return this.graph.stringConstant(filePath, value.expression);
|
|
1789
3080
|
}
|
|
1790
|
-
return
|
|
3081
|
+
return null;
|
|
1791
3082
|
}
|
|
1792
3083
|
/**
|
|
1793
3084
|
* Is this JSX element `<navigatorVarName.MEMBER …>`?
|
|
@@ -1882,22 +3173,48 @@ var NavigationAnalyzer = class {
|
|
|
1882
3173
|
return found;
|
|
1883
3174
|
}
|
|
1884
3175
|
/** Extract screens from a navigator JSX element */
|
|
1885
|
-
extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
|
|
3176
|
+
extractScreensFromNavigator(filePath, navigatorElement, navigatorVarName, navigatorType) {
|
|
1886
3177
|
const screens = [];
|
|
1887
3178
|
if (!navigatorElement.children) return screens;
|
|
1888
3179
|
const seen = /* @__PURE__ */ new Set();
|
|
1889
3180
|
for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
|
|
1890
|
-
const
|
|
1891
|
-
if (!
|
|
1892
|
-
seen.add(
|
|
1893
|
-
screens.push(
|
|
1894
|
-
name: screenName,
|
|
1895
|
-
navigatorName: navigatorVarName,
|
|
1896
|
-
navigatorType
|
|
1897
|
-
});
|
|
3181
|
+
const screen = this.parseScreenElement(filePath, element, navigatorVarName, navigatorType);
|
|
3182
|
+
if (!screen || seen.has(screen.name)) continue;
|
|
3183
|
+
seen.add(screen.name);
|
|
3184
|
+
screens.push(screen);
|
|
1898
3185
|
}
|
|
1899
3186
|
return screens;
|
|
1900
3187
|
}
|
|
3188
|
+
/**
|
|
3189
|
+
* Uma `<X.Screen>` isolada até a rota que ela declara.
|
|
3190
|
+
*
|
|
3191
|
+
* Separado de `extractScreensFromNavigator` porque a MESMA leitura serve para
|
|
3192
|
+
* a `<X.Screen>` que mora dentro do `<X.Navigator>` e para a que mora fora
|
|
3193
|
+
* dele — só a forma de chegar até o elemento muda.
|
|
3194
|
+
*/
|
|
3195
|
+
parseScreenElement(filePath, element, navigatorName, navigatorType) {
|
|
3196
|
+
const attrs = element.openingElement.attributes;
|
|
3197
|
+
const nameAttr = attrs.find(
|
|
3198
|
+
(a) => BabelTypes__namespace.isJSXAttribute(a) && BabelTypes__namespace.isJSXIdentifier(a.name) && a.name.name === "name"
|
|
3199
|
+
);
|
|
3200
|
+
const screenName = BabelTypes__namespace.isJSXAttribute(nameAttr) ? this.attributeString(filePath, nameAttr) : null;
|
|
3201
|
+
if (!screenName) return null;
|
|
3202
|
+
const componentAttr = attrs.find(
|
|
3203
|
+
(a) => BabelTypes__namespace.isJSXAttribute(a) && BabelTypes__namespace.isJSXIdentifier(a.name) && a.name.name === "component"
|
|
3204
|
+
);
|
|
3205
|
+
let componentFile = null;
|
|
3206
|
+
if (BabelTypes__namespace.isJSXAttribute(componentAttr) && BabelTypes__namespace.isJSXExpressionContainer(componentAttr.value)) {
|
|
3207
|
+
const expr = componentAttr.value.expression;
|
|
3208
|
+
if (BabelTypes__namespace.isExpression(expr)) componentFile = this.graph.componentFile(filePath, expr);
|
|
3209
|
+
}
|
|
3210
|
+
if (componentFile) this.routeTargets.set(screenName, componentFile);
|
|
3211
|
+
return {
|
|
3212
|
+
name: screenName,
|
|
3213
|
+
navigatorName,
|
|
3214
|
+
navigatorType,
|
|
3215
|
+
...componentFile ? { componentFile } : {}
|
|
3216
|
+
};
|
|
3217
|
+
}
|
|
1901
3218
|
/** Extract string attribute value from JSX attributes */
|
|
1902
3219
|
extractAttributeValue(attributes, attrName) {
|
|
1903
3220
|
const attr = attributes.find(
|
|
@@ -1920,7 +3237,7 @@ var NavigationAnalyzer = class {
|
|
|
1920
3237
|
...this.config.parserPlugins || []
|
|
1921
3238
|
]
|
|
1922
3239
|
});
|
|
1923
|
-
|
|
3240
|
+
traverse5__default.default(ast, {
|
|
1924
3241
|
TSTypeAliasDeclaration: (nodePath) => {
|
|
1925
3242
|
const { node } = nodePath;
|
|
1926
3243
|
const typeName = node.id.name;
|
|
@@ -1993,7 +3310,7 @@ var NavigationAnalyzer = class {
|
|
|
1993
3310
|
if (type.type === "TSUndefinedKeyword") return "undefined";
|
|
1994
3311
|
if (type.type === "TSNullKeyword") return "null";
|
|
1995
3312
|
if (type.type === "TSUnionType") {
|
|
1996
|
-
return type.types.map((
|
|
3313
|
+
return type.types.map((t15) => this.typeToString(t15)).join(" | ");
|
|
1997
3314
|
}
|
|
1998
3315
|
if (type.type === "TSTypeLiteral") {
|
|
1999
3316
|
return "object";
|
|
@@ -2014,7 +3331,7 @@ var NavigationAnalyzer = class {
|
|
|
2014
3331
|
/** Attach parsed type params to navigator screens */
|
|
2015
3332
|
attachParamsToNavigators(navigators, types) {
|
|
2016
3333
|
for (const navigator of navigators) {
|
|
2017
|
-
const matchingType = types.find((
|
|
3334
|
+
const matchingType = types.find((t15) => t15.type === navigator.type);
|
|
2018
3335
|
if (matchingType) {
|
|
2019
3336
|
for (const screen of navigator.screens) {
|
|
2020
3337
|
const screenParams = matchingType.paramEntries.get(screen.name);
|
|
@@ -2103,9 +3420,9 @@ var ComponentAnalyzer = class {
|
|
|
2103
3420
|
plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
|
|
2104
3421
|
});
|
|
2105
3422
|
const components = [];
|
|
2106
|
-
|
|
2107
|
-
JSXElement: (
|
|
2108
|
-
const component = this.extractComponentFromJSXElement(
|
|
3423
|
+
traverse5__default.default(ast, {
|
|
3424
|
+
JSXElement: (path11) => {
|
|
3425
|
+
const component = this.extractComponentFromJSXElement(path11.node);
|
|
2109
3426
|
if (component) {
|
|
2110
3427
|
components.push(component);
|
|
2111
3428
|
}
|
|
@@ -2231,14 +3548,14 @@ var FormAnalyzer = class {
|
|
|
2231
3548
|
this.stateVariables.clear();
|
|
2232
3549
|
this.inputElements = [];
|
|
2233
3550
|
this.submitButtons = [];
|
|
2234
|
-
|
|
2235
|
-
CallExpression: (
|
|
2236
|
-
this.extractStateVariables(
|
|
3551
|
+
traverse5__default.default(ast, {
|
|
3552
|
+
CallExpression: (path11) => {
|
|
3553
|
+
this.extractStateVariables(path11.node);
|
|
2237
3554
|
}
|
|
2238
3555
|
});
|
|
2239
|
-
|
|
2240
|
-
JSXElement: (
|
|
2241
|
-
this.extractFormElements(
|
|
3556
|
+
traverse5__default.default(ast, {
|
|
3557
|
+
JSXElement: (path11) => {
|
|
3558
|
+
this.extractFormElements(path11.node);
|
|
2242
3559
|
}
|
|
2243
3560
|
});
|
|
2244
3561
|
const validationRules = this.extractValidationRules(ast);
|
|
@@ -2272,23 +3589,23 @@ var FormAnalyzer = class {
|
|
|
2272
3589
|
for (const attr of openingElement.attributes) {
|
|
2273
3590
|
if (BabelTypes__namespace.isJSXAttribute(attr) && BabelTypes__namespace.isJSXIdentifier(attr.name)) {
|
|
2274
3591
|
const propName = attr.name.name;
|
|
2275
|
-
const
|
|
3592
|
+
const propValue2 = this.extractAttributeValue(attr.value);
|
|
2276
3593
|
switch (propName) {
|
|
2277
3594
|
case "label":
|
|
2278
|
-
info.label =
|
|
3595
|
+
info.label = propValue2;
|
|
2279
3596
|
break;
|
|
2280
3597
|
case "placeholder":
|
|
2281
|
-
info.placeholder =
|
|
3598
|
+
info.placeholder = propValue2;
|
|
2282
3599
|
break;
|
|
2283
3600
|
case "keyboardType":
|
|
2284
|
-
info.keyboardType =
|
|
3601
|
+
info.keyboardType = propValue2;
|
|
2285
3602
|
break;
|
|
2286
3603
|
case "testID":
|
|
2287
|
-
info.testID =
|
|
3604
|
+
info.testID = propValue2;
|
|
2288
3605
|
break;
|
|
2289
3606
|
case "appilotsId":
|
|
2290
|
-
info.appilotsId =
|
|
2291
|
-
if (
|
|
3607
|
+
info.appilotsId = propValue2;
|
|
3608
|
+
if (propValue2) info.varName = propValue2;
|
|
2292
3609
|
break;
|
|
2293
3610
|
case "value":
|
|
2294
3611
|
if (attr.value && BabelTypes__namespace.isJSXExpressionContainer(attr.value) && BabelTypes__namespace.isIdentifier(attr.value.expression)) {
|
|
@@ -2337,9 +3654,9 @@ var FormAnalyzer = class {
|
|
|
2337
3654
|
}
|
|
2338
3655
|
extractValidationRules(ast) {
|
|
2339
3656
|
const rules = {};
|
|
2340
|
-
|
|
2341
|
-
IfStatement: (
|
|
2342
|
-
const test =
|
|
3657
|
+
traverse5__default.default(ast, {
|
|
3658
|
+
IfStatement: (path11) => {
|
|
3659
|
+
const test = path11.node.test;
|
|
2343
3660
|
const rule = this.extractRuleFromCondition(test);
|
|
2344
3661
|
if (rule) {
|
|
2345
3662
|
const { field, description } = rule;
|
|
@@ -2393,7 +3710,7 @@ var FormAnalyzer = class {
|
|
|
2393
3710
|
}
|
|
2394
3711
|
buildForms(filePath, validationRules) {
|
|
2395
3712
|
if (this.inputElements.length === 0) return [];
|
|
2396
|
-
const fileName =
|
|
3713
|
+
const fileName = path2__namespace.basename(filePath, path2__namespace.extname(filePath));
|
|
2397
3714
|
const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
|
|
2398
3715
|
const fields = this.inputElements.map((input) => {
|
|
2399
3716
|
const fieldType = this.inferFieldType(input);
|
|
@@ -2442,13 +3759,105 @@ var FormAnalyzer = class {
|
|
|
2442
3759
|
return rule !== void 0 && rule.includes("required");
|
|
2443
3760
|
}
|
|
2444
3761
|
};
|
|
3762
|
+
|
|
3763
|
+
// src/pipeline/composition.ts
|
|
3764
|
+
var MAX_DEPTH = 3;
|
|
3765
|
+
var MAX_VISITED_PER_SCREEN = 60;
|
|
3766
|
+
var MAX_FAN_IN = 3;
|
|
3767
|
+
var ADDRESSABLE = /* @__PURE__ */ new Set(["testID", "appilotsId", "accessibilityLabel", "label"]);
|
|
3768
|
+
function descendants(graph, from, screenFiles) {
|
|
3769
|
+
const seen = /* @__PURE__ */ new Set([from]);
|
|
3770
|
+
const out = [];
|
|
3771
|
+
let frontier = [from];
|
|
3772
|
+
for (let depth = 0; depth < MAX_DEPTH && frontier.length > 0; depth++) {
|
|
3773
|
+
const next = [];
|
|
3774
|
+
for (const file of frontier) {
|
|
3775
|
+
for (const child of graph.renderedComponentFiles(file)) {
|
|
3776
|
+
if (seen.has(child)) continue;
|
|
3777
|
+
seen.add(child);
|
|
3778
|
+
if (screenFiles.has(child)) continue;
|
|
3779
|
+
out.push(child);
|
|
3780
|
+
next.push(child);
|
|
3781
|
+
if (out.length >= MAX_VISITED_PER_SCREEN) return out;
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
frontier = next;
|
|
3785
|
+
}
|
|
3786
|
+
return out;
|
|
3787
|
+
}
|
|
3788
|
+
function addressable(locator) {
|
|
3789
|
+
if (!locator || typeof locator !== "object") return false;
|
|
3790
|
+
const l = locator;
|
|
3791
|
+
if (typeof l["source"] === "string" && ADDRESSABLE.has(l["source"])) return true;
|
|
3792
|
+
return typeof l["testID"] === "string" && l["testID"].length > 0;
|
|
3793
|
+
}
|
|
3794
|
+
async function composeAffordances(options) {
|
|
3795
|
+
const { screens, graph, analyzeFile, screenFiles } = options;
|
|
3796
|
+
const byScreen = /* @__PURE__ */ new Map();
|
|
3797
|
+
const fanIn = /* @__PURE__ */ new Map();
|
|
3798
|
+
for (const screen of screens) {
|
|
3799
|
+
if (!screen.filePath) continue;
|
|
3800
|
+
const children = descendants(graph, screen.filePath, screenFiles);
|
|
3801
|
+
byScreen.set(screen, children);
|
|
3802
|
+
for (const child of children) fanIn.set(child, (fanIn.get(child) ?? 0) + 1);
|
|
3803
|
+
}
|
|
3804
|
+
const analyzed = /* @__PURE__ */ new Map();
|
|
3805
|
+
let screensEnriched = 0;
|
|
3806
|
+
let filesMerged = 0;
|
|
3807
|
+
for (const [screen, children] of byScreen) {
|
|
3808
|
+
const exclusive = children.filter((c) => (fanIn.get(c) ?? 0) <= MAX_FAN_IN);
|
|
3809
|
+
if (exclusive.length === 0) continue;
|
|
3810
|
+
const actionIds = new Set(screen.actions.map((a) => a.id));
|
|
3811
|
+
const targetIds = new Set((screen.targets ?? []).map((t15) => t15.id));
|
|
3812
|
+
const formIds = new Set(screen.forms.map((f) => f.id));
|
|
3813
|
+
let gained = false;
|
|
3814
|
+
for (const file of exclusive) {
|
|
3815
|
+
if (!analyzed.has(file)) {
|
|
3816
|
+
try {
|
|
3817
|
+
analyzed.set(file, await analyzeFile(file));
|
|
3818
|
+
} catch {
|
|
3819
|
+
analyzed.set(file, null);
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
const child = analyzed.get(file);
|
|
3823
|
+
if (!child) continue;
|
|
3824
|
+
let merged = false;
|
|
3825
|
+
for (const action of child.actions) {
|
|
3826
|
+
if (!addressable(action.locator)) continue;
|
|
3827
|
+
if (actionIds.has(action.id)) continue;
|
|
3828
|
+
actionIds.add(action.id);
|
|
3829
|
+
screen.actions.push(action);
|
|
3830
|
+
merged = true;
|
|
3831
|
+
}
|
|
3832
|
+
for (const target of child.targets ?? []) {
|
|
3833
|
+
if (!addressable(target.locator)) continue;
|
|
3834
|
+
if (targetIds.has(target.id)) continue;
|
|
3835
|
+
targetIds.add(target.id);
|
|
3836
|
+
(screen.targets ??= []).push(target);
|
|
3837
|
+
merged = true;
|
|
3838
|
+
}
|
|
3839
|
+
for (const form of child.forms) {
|
|
3840
|
+
if (form.fields.length === 0) continue;
|
|
3841
|
+
const id = formIds.has(form.id) ? `${form.id}:${child.name}` : form.id;
|
|
3842
|
+
if (formIds.has(id)) continue;
|
|
3843
|
+
formIds.add(id);
|
|
3844
|
+
screen.forms.push({ ...form, id });
|
|
3845
|
+
merged = true;
|
|
3846
|
+
}
|
|
3847
|
+
if (merged) {
|
|
3848
|
+
filesMerged++;
|
|
3849
|
+
gained = true;
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
if (gained) screensEnriched++;
|
|
3853
|
+
}
|
|
3854
|
+
return { screens, screensEnriched, filesMerged };
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3857
|
+
// src/analyzers/ReactNativePlatformAnalyzer.ts
|
|
2445
3858
|
var ReactNativePlatformAnalyzer = class {
|
|
2446
3859
|
platform = "react-native";
|
|
2447
3860
|
async analyze(config, options) {
|
|
2448
|
-
const screenAnalyzer = new ScreenAnalyzer(config, {
|
|
2449
|
-
strictScreens: options.strictScreens ?? true,
|
|
2450
|
-
screenPatterns: options.screenPatterns
|
|
2451
|
-
});
|
|
2452
3861
|
const navigationAnalyzer = new NavigationAnalyzer(config, {
|
|
2453
3862
|
navigationInclude: options.navigationInclude,
|
|
2454
3863
|
navigationExclude: options.navigationExclude
|
|
@@ -2456,14 +3865,31 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2456
3865
|
const componentAnalyzer = new ComponentAnalyzer(config);
|
|
2457
3866
|
const formAnalyzer = new FormAnalyzer(config);
|
|
2458
3867
|
console.log("[ReactNativePlatformAnalyzer] Running analyzers...");
|
|
2459
|
-
const
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
3868
|
+
const navigation = await navigationAnalyzer.analyze();
|
|
3869
|
+
const screenAnalyzer = new ScreenAnalyzer(config, {
|
|
3870
|
+
strictScreens: options.strictScreens ?? true,
|
|
3871
|
+
screenPatterns: options.screenPatterns,
|
|
3872
|
+
// Menos os arquivos que declaram navegador: ver `navigatorFiles`.
|
|
3873
|
+
routeTargetFiles: new Set(
|
|
3874
|
+
[...navigationAnalyzer.routeTargets.values()].filter(
|
|
3875
|
+
(file) => !navigationAnalyzer.navigatorFiles.has(file)
|
|
3876
|
+
)
|
|
3877
|
+
)
|
|
3878
|
+
});
|
|
3879
|
+
const screens = await screenAnalyzer.analyze();
|
|
3880
|
+
const composed = await composeAffordances({
|
|
3881
|
+
screens,
|
|
3882
|
+
graph: navigationAnalyzer.graph,
|
|
3883
|
+
analyzeFile: (file) => screenAnalyzer.analyzeFile(file),
|
|
3884
|
+
screenFiles: new Set(screens.map((s) => s.filePath).filter(Boolean))
|
|
3885
|
+
});
|
|
3886
|
+
console.log(
|
|
3887
|
+
`[ReactNativePlatformAnalyzer] Composi\xE7\xE3o: ${composed.filesMerged} componente(s) exclusivo(s) fundido(s) em ${composed.screensEnriched} tela(s)`
|
|
3888
|
+
);
|
|
2463
3889
|
console.log(
|
|
2464
3890
|
`[ReactNativePlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens`
|
|
2465
3891
|
);
|
|
2466
|
-
const screenFiles = await
|
|
3892
|
+
const screenFiles = await globSorted(config.include || ["**/*.tsx", "**/*.ts"], {
|
|
2467
3893
|
cwd: config.rootDir,
|
|
2468
3894
|
ignore: config.exclude || ["**/node_modules/**"]
|
|
2469
3895
|
});
|
|
@@ -2471,7 +3897,7 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2471
3897
|
`[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
|
|
2472
3898
|
);
|
|
2473
3899
|
const enrichmentPromises = screenFiles.map(async (file) => {
|
|
2474
|
-
const filePath =
|
|
3900
|
+
const filePath = path2__namespace.default.resolve(config.rootDir, file);
|
|
2475
3901
|
try {
|
|
2476
3902
|
const [components, forms] = await Promise.all([
|
|
2477
3903
|
componentAnalyzer.analyzeFile(filePath),
|
|
@@ -2524,9 +3950,11 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2524
3950
|
});
|
|
2525
3951
|
return {
|
|
2526
3952
|
screens: enrichedScreens,
|
|
3953
|
+
controlEvidenceFiles: screenAnalyzer.controlEvidenceFiles,
|
|
2527
3954
|
navigation,
|
|
2528
3955
|
analyzedFiles: screenFiles.length,
|
|
2529
|
-
...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
|
|
3956
|
+
...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {},
|
|
3957
|
+
diagnostics: { navigation: navigationAnalyzer.diagnostics }
|
|
2530
3958
|
};
|
|
2531
3959
|
}
|
|
2532
3960
|
mergeForm(target, source) {
|
|
@@ -2544,9 +3972,12 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2544
3972
|
findEquivalentField(fields, incoming) {
|
|
2545
3973
|
return fields.find((field) => {
|
|
2546
3974
|
if (field.name && incoming.name && field.name === incoming.name) return true;
|
|
2547
|
-
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
2548
|
-
|
|
2549
|
-
if (field.
|
|
3975
|
+
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
3976
|
+
return true;
|
|
3977
|
+
if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
|
|
3978
|
+
return true;
|
|
3979
|
+
if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
|
|
3980
|
+
return true;
|
|
2550
3981
|
if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
|
|
2551
3982
|
return true;
|
|
2552
3983
|
}
|
|
@@ -2579,7 +4010,9 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2579
4010
|
if (incoming.fields.length === 0) return void 0;
|
|
2580
4011
|
let best;
|
|
2581
4012
|
for (const form of forms) {
|
|
2582
|
-
const overlap = incoming.fields.filter(
|
|
4013
|
+
const overlap = incoming.fields.filter(
|
|
4014
|
+
(field) => this.findEquivalentField(form.fields, field)
|
|
4015
|
+
).length;
|
|
2583
4016
|
if (overlap > 0 && (!best || overlap > best.overlap)) {
|
|
2584
4017
|
best = { form, overlap };
|
|
2585
4018
|
}
|
|
@@ -2733,13 +4166,13 @@ function staticRoutePath(node) {
|
|
|
2733
4166
|
if (!node) return void 0;
|
|
2734
4167
|
if (BabelTypes__namespace.isStringLiteral(node)) return node.value;
|
|
2735
4168
|
if (BabelTypes__namespace.isTemplateLiteral(node)) {
|
|
2736
|
-
let
|
|
4169
|
+
let path11 = "";
|
|
2737
4170
|
node.quasis.forEach((quasi, index) => {
|
|
2738
|
-
|
|
4171
|
+
path11 += quasi.value.cooked ?? quasi.value.raw;
|
|
2739
4172
|
const expr = node.expressions[index];
|
|
2740
|
-
if (expr)
|
|
4173
|
+
if (expr) path11 += `:${paramNameOf(expr)}`;
|
|
2741
4174
|
});
|
|
2742
|
-
return
|
|
4175
|
+
return path11;
|
|
2743
4176
|
}
|
|
2744
4177
|
return void 0;
|
|
2745
4178
|
}
|
|
@@ -2782,7 +4215,7 @@ function extractWebNavigationCalls(ast) {
|
|
|
2782
4215
|
calls.push({ method: "navigate", targetPath: node.right.value });
|
|
2783
4216
|
}
|
|
2784
4217
|
};
|
|
2785
|
-
|
|
4218
|
+
traverse5__default.default(ast, {
|
|
2786
4219
|
noScope: !BabelTypes__namespace.isFile(ast),
|
|
2787
4220
|
enter: (nodePath) => inspect(nodePath.node)
|
|
2788
4221
|
});
|
|
@@ -2824,17 +4257,17 @@ var WebScreenAnalyzer = class {
|
|
|
2824
4257
|
include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
|
|
2825
4258
|
exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
2826
4259
|
} = this.config;
|
|
2827
|
-
const files = (await
|
|
4260
|
+
const files = (await globSorted(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
|
|
2828
4261
|
(file) => !file.endsWith(".d.ts")
|
|
2829
4262
|
);
|
|
2830
|
-
const patternMatches = await
|
|
4263
|
+
const patternMatches = await globSorted(this.screenPatterns, {
|
|
2831
4264
|
cwd: this.config.rootDir,
|
|
2832
4265
|
ignore: exclude
|
|
2833
4266
|
});
|
|
2834
|
-
const patternSet = new Set(patternMatches.map((f) =>
|
|
4267
|
+
const patternSet = new Set(patternMatches.map((f) => path2__namespace.default.resolve(this.config.rootDir, f)));
|
|
2835
4268
|
const candidates = [];
|
|
2836
4269
|
for (const file of files) {
|
|
2837
|
-
const filePath =
|
|
4270
|
+
const filePath = path2__namespace.default.resolve(this.config.rootDir, file);
|
|
2838
4271
|
try {
|
|
2839
4272
|
const candidate = await this.analyzeFile(filePath);
|
|
2840
4273
|
if (!candidate) continue;
|
|
@@ -2878,7 +4311,7 @@ var WebScreenAnalyzer = class {
|
|
|
2878
4311
|
}
|
|
2879
4312
|
}
|
|
2880
4313
|
}
|
|
2881
|
-
const name = registerScreenMeta?.name || componentName ||
|
|
4314
|
+
const name = registerScreenMeta?.name || componentName || path2__namespace.default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
|
|
2882
4315
|
const descriptor = {
|
|
2883
4316
|
name,
|
|
2884
4317
|
filePath,
|
|
@@ -2904,7 +4337,7 @@ var WebScreenAnalyzer = class {
|
|
|
2904
4337
|
// ── registerScreen ────────────────────────────────────────────────
|
|
2905
4338
|
detectRegisterScreenCall(ast) {
|
|
2906
4339
|
let found = false;
|
|
2907
|
-
|
|
4340
|
+
traverse5__default.default(ast, {
|
|
2908
4341
|
CallExpression: (nodePath) => {
|
|
2909
4342
|
if (found) return;
|
|
2910
4343
|
if (isRegisterScreenCallee(nodePath.node.callee)) {
|
|
@@ -2917,7 +4350,7 @@ var WebScreenAnalyzer = class {
|
|
|
2917
4350
|
}
|
|
2918
4351
|
extractRegisterScreenMetadata(ast) {
|
|
2919
4352
|
let plain = null;
|
|
2920
|
-
|
|
4353
|
+
traverse5__default.default(ast, {
|
|
2921
4354
|
CallExpression: (nodePath) => {
|
|
2922
4355
|
if (!isRegisterScreenCallee(nodePath.node.callee)) return;
|
|
2923
4356
|
const arg = nodePath.node.arguments[0];
|
|
@@ -2965,7 +4398,7 @@ var WebScreenAnalyzer = class {
|
|
|
2965
4398
|
extractComponentName(ast) {
|
|
2966
4399
|
let defaultName = "";
|
|
2967
4400
|
let firstExported = "";
|
|
2968
|
-
|
|
4401
|
+
traverse5__default.default(ast, {
|
|
2969
4402
|
ExportDefaultDeclaration: (nodePath) => {
|
|
2970
4403
|
const declaration = nodePath.node.declaration;
|
|
2971
4404
|
if (BabelTypes__namespace.isFunctionDeclaration(declaration) && declaration.id?.name) {
|
|
@@ -2994,7 +4427,7 @@ var WebScreenAnalyzer = class {
|
|
|
2994
4427
|
extractComponents(ast) {
|
|
2995
4428
|
const components = [];
|
|
2996
4429
|
const seen = /* @__PURE__ */ new Set();
|
|
2997
|
-
|
|
4430
|
+
traverse5__default.default(ast, {
|
|
2998
4431
|
JSXOpeningElement: (nodePath) => {
|
|
2999
4432
|
const element = nodePath.node;
|
|
3000
4433
|
const name = getJsxElementName(element);
|
|
@@ -3015,7 +4448,7 @@ var WebScreenAnalyzer = class {
|
|
|
3015
4448
|
// ── <label htmlFor> association ───────────────────────────────────
|
|
3016
4449
|
collectHtmlForLabels(ast) {
|
|
3017
4450
|
const labels = /* @__PURE__ */ new Map();
|
|
3018
|
-
|
|
4451
|
+
traverse5__default.default(ast, {
|
|
3019
4452
|
JSXElement: (nodePath) => {
|
|
3020
4453
|
const element = nodePath.node;
|
|
3021
4454
|
if (getJsxElementName(element.openingElement) !== "label") return;
|
|
@@ -3058,7 +4491,7 @@ var WebScreenAnalyzer = class {
|
|
|
3058
4491
|
formBuckets.set(formElement, bucket);
|
|
3059
4492
|
return bucket;
|
|
3060
4493
|
};
|
|
3061
|
-
|
|
4494
|
+
traverse5__default.default(ast, {
|
|
3062
4495
|
JSXElement: (nodePath) => {
|
|
3063
4496
|
const element = nodePath.node;
|
|
3064
4497
|
const name = getJsxElementName(element.openingElement);
|
|
@@ -3075,7 +4508,7 @@ var WebScreenAnalyzer = class {
|
|
|
3075
4508
|
if (!bucket.fields.has(field.name)) bucket.fields.set(field.name, field);
|
|
3076
4509
|
}
|
|
3077
4510
|
});
|
|
3078
|
-
|
|
4511
|
+
traverse5__default.default(ast, {
|
|
3079
4512
|
JSXElement: (nodePath) => {
|
|
3080
4513
|
const element = nodePath.node;
|
|
3081
4514
|
const name = getJsxElementName(element.openingElement);
|
|
@@ -3253,7 +4686,7 @@ var WebScreenAnalyzer = class {
|
|
|
3253
4686
|
const actionLabels = new Map(
|
|
3254
4687
|
actions.filter((a) => a.label).map((a) => [normalizeLabel(a.label), a])
|
|
3255
4688
|
);
|
|
3256
|
-
|
|
4689
|
+
traverse5__default.default(ast, {
|
|
3257
4690
|
JSXElement: (nodePath) => {
|
|
3258
4691
|
const element = nodePath.node;
|
|
3259
4692
|
const name = getJsxElementName(element.openingElement);
|
|
@@ -3484,7 +4917,7 @@ var WebScreenAnalyzer = class {
|
|
|
3484
4917
|
for (const call of extractWebNavigationCalls(ast)) {
|
|
3485
4918
|
if (call.targetPath && call.targetPath.startsWith("/")) targets.add(call.targetPath);
|
|
3486
4919
|
}
|
|
3487
|
-
|
|
4920
|
+
traverse5__default.default(ast, {
|
|
3488
4921
|
JSXOpeningElement: (nodePath) => {
|
|
3489
4922
|
const element = nodePath.node;
|
|
3490
4923
|
const name = getJsxElementName(element);
|
|
@@ -3504,7 +4937,7 @@ var WebScreenAnalyzer = class {
|
|
|
3504
4937
|
extractCollections(ast) {
|
|
3505
4938
|
const collections = [];
|
|
3506
4939
|
const seen = /* @__PURE__ */ new Set();
|
|
3507
|
-
|
|
4940
|
+
traverse5__default.default(ast, {
|
|
3508
4941
|
JSXExpressionContainer: (nodePath) => {
|
|
3509
4942
|
const expr = nodePath.node.expression;
|
|
3510
4943
|
if (!BabelTypes__namespace.isCallExpression(expr) || !BabelTypes__namespace.isMemberExpression(expr.callee) || !BabelTypes__namespace.isIdentifier(expr.callee.object) || !BabelTypes__namespace.isIdentifier(expr.callee.property) || expr.callee.property.name !== "map") {
|
|
@@ -3616,7 +5049,7 @@ function firstCalledFunctionName(node) {
|
|
|
3616
5049
|
}
|
|
3617
5050
|
function containsWindowConfirm(body) {
|
|
3618
5051
|
let found = false;
|
|
3619
|
-
|
|
5052
|
+
traverse5__default.default(
|
|
3620
5053
|
body,
|
|
3621
5054
|
{
|
|
3622
5055
|
noScope: true,
|
|
@@ -3668,7 +5101,7 @@ function collectionItemNames(callback) {
|
|
|
3668
5101
|
function collectionDisplayFields(callback, itemNames) {
|
|
3669
5102
|
const fields = /* @__PURE__ */ new Set();
|
|
3670
5103
|
if (!callback.body) return [];
|
|
3671
|
-
|
|
5104
|
+
traverse5__default.default(
|
|
3672
5105
|
callback.body,
|
|
3673
5106
|
{
|
|
3674
5107
|
noScope: true,
|
|
@@ -3685,7 +5118,7 @@ function collectionDisplayFields(callback, itemNames) {
|
|
|
3685
5118
|
function collectionKeyField(callback, itemNames) {
|
|
3686
5119
|
let keyField;
|
|
3687
5120
|
if (!callback.body) return void 0;
|
|
3688
|
-
|
|
5121
|
+
traverse5__default.default(
|
|
3689
5122
|
callback.body,
|
|
3690
5123
|
{
|
|
3691
5124
|
noScope: true,
|
|
@@ -3767,7 +5200,9 @@ var WebNavigationAnalyzer = class {
|
|
|
3767
5200
|
for (const filePath of files) {
|
|
3768
5201
|
try {
|
|
3769
5202
|
const content = await fs$1.promises.readFile(filePath, "utf-8");
|
|
3770
|
-
if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
|
|
5203
|
+
if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
|
|
5204
|
+
content
|
|
5205
|
+
)) {
|
|
3771
5206
|
continue;
|
|
3772
5207
|
}
|
|
3773
5208
|
const ast = parseSource(content, this.config.parserPlugins);
|
|
@@ -3797,8 +5232,8 @@ var WebNavigationAnalyzer = class {
|
|
|
3797
5232
|
...this.config.exclude || [],
|
|
3798
5233
|
...this.navigationExclude
|
|
3799
5234
|
];
|
|
3800
|
-
const files = await
|
|
3801
|
-
return files.map((file) =>
|
|
5235
|
+
const files = await globSorted(patterns, { cwd: this.config.rootDir, ignore });
|
|
5236
|
+
return files.map((file) => path2__namespace.default.join(this.config.rootDir, file));
|
|
3802
5237
|
}
|
|
3803
5238
|
// ── JSX <Route> style ────────────────────────────────────────────
|
|
3804
5239
|
extractJsxRoutes(ast) {
|
|
@@ -3826,7 +5261,7 @@ var WebNavigationAnalyzer = class {
|
|
|
3826
5261
|
if (BabelTypes__namespace.isJSXElement(child)) visitRoute(child, fullPath);
|
|
3827
5262
|
}
|
|
3828
5263
|
};
|
|
3829
|
-
|
|
5264
|
+
traverse5__default.default(ast, {
|
|
3830
5265
|
JSXElement: (nodePath) => {
|
|
3831
5266
|
const name = getJsxElementName(nodePath.node.openingElement);
|
|
3832
5267
|
if (name !== "Routes" && name !== "Route") return;
|
|
@@ -3865,7 +5300,7 @@ var WebNavigationAnalyzer = class {
|
|
|
3865
5300
|
"createMemoryRouter",
|
|
3866
5301
|
"useRoutes"
|
|
3867
5302
|
]);
|
|
3868
|
-
|
|
5303
|
+
traverse5__default.default(ast, {
|
|
3869
5304
|
CallExpression: (nodePath) => {
|
|
3870
5305
|
const callee = nodePath.node.callee;
|
|
3871
5306
|
if (!BabelTypes__namespace.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
|
|
@@ -3970,11 +5405,13 @@ var WebNavigationAnalyzer = class {
|
|
|
3970
5405
|
return {
|
|
3971
5406
|
screens,
|
|
3972
5407
|
initialScreen: initialRoute?.screenName ?? "",
|
|
3973
|
-
navigators: routes.length > 0 ? [
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
5408
|
+
navigators: routes.length > 0 ? [
|
|
5409
|
+
{
|
|
5410
|
+
name: navigatorName,
|
|
5411
|
+
type: WEB_NAVIGATOR_TYPE,
|
|
5412
|
+
screens: screenNames
|
|
5413
|
+
}
|
|
5414
|
+
] : []
|
|
3978
5415
|
};
|
|
3979
5416
|
}
|
|
3980
5417
|
};
|
|
@@ -4296,8 +5733,8 @@ var ZodParsedType = util.arrayToEnum([
|
|
|
4296
5733
|
"set"
|
|
4297
5734
|
]);
|
|
4298
5735
|
var getParsedType = (data) => {
|
|
4299
|
-
const
|
|
4300
|
-
switch (
|
|
5736
|
+
const t15 = typeof data;
|
|
5737
|
+
switch (t15) {
|
|
4301
5738
|
case "undefined":
|
|
4302
5739
|
return ZodParsedType.undefined;
|
|
4303
5740
|
case "string":
|
|
@@ -4569,8 +6006,8 @@ function getErrorMap() {
|
|
|
4569
6006
|
|
|
4570
6007
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
4571
6008
|
var makeIssue = (params) => {
|
|
4572
|
-
const { data, path:
|
|
4573
|
-
const fullPath = [...
|
|
6009
|
+
const { data, path: path11, errorMaps, issueData } = params;
|
|
6010
|
+
const fullPath = [...path11, ...issueData.path || []];
|
|
4574
6011
|
const fullIssue = {
|
|
4575
6012
|
...issueData,
|
|
4576
6013
|
path: fullPath
|
|
@@ -4686,11 +6123,11 @@ var errorUtil;
|
|
|
4686
6123
|
|
|
4687
6124
|
// ../../node_modules/zod/v3/types.js
|
|
4688
6125
|
var ParseInputLazyPath = class {
|
|
4689
|
-
constructor(parent, value,
|
|
6126
|
+
constructor(parent, value, path11, key) {
|
|
4690
6127
|
this._cachedPath = [];
|
|
4691
6128
|
this.parent = parent;
|
|
4692
6129
|
this.data = value;
|
|
4693
|
-
this._path =
|
|
6130
|
+
this._path = path11;
|
|
4694
6131
|
this._key = key;
|
|
4695
6132
|
}
|
|
4696
6133
|
get path() {
|
|
@@ -8131,7 +9568,36 @@ var coerce = {
|
|
|
8131
9568
|
};
|
|
8132
9569
|
var NEVER = INVALID;
|
|
8133
9570
|
|
|
8134
|
-
// ../shared/dist/chunk-
|
|
9571
|
+
// ../shared/dist/chunk-SWQOZWHP.mjs
|
|
9572
|
+
var SAFE_IMAGE_URL_RE = /^(?:https?:\/\/|data:image\/(?:png|jpe?g|gif|webp);base64,)/i;
|
|
9573
|
+
var HAS_SCHEME_RE = /^[\s\u0000-\u001f]*[a-z][a-z0-9+.-]*:/i;
|
|
9574
|
+
function isSafeImageUrl(value) {
|
|
9575
|
+
if (value === null || value === void 0) return true;
|
|
9576
|
+
const trimmed = value.trim();
|
|
9577
|
+
if (trimmed === "") return true;
|
|
9578
|
+
if (!HAS_SCHEME_RE.test(trimmed)) return true;
|
|
9579
|
+
return SAFE_IMAGE_URL_RE.test(trimmed);
|
|
9580
|
+
}
|
|
9581
|
+
|
|
9582
|
+
// ../shared/dist/chunk-KUMPS6RH.mjs
|
|
9583
|
+
var SUPPORTED_APPILOTS_LOCALES = ["pt-BR", "en", "es", "fr"];
|
|
9584
|
+
|
|
9585
|
+
// ../shared/dist/chunk-2GTRKNPJ.mjs
|
|
9586
|
+
var symbols = external_exports.array(external_exports.string().max(240)).max(12);
|
|
9587
|
+
var controlEvidenceSchema = external_exports.object({
|
|
9588
|
+
version: external_exports.literal(1),
|
|
9589
|
+
siteId: external_exports.string().regex(/^[a-f0-9]{20}$/),
|
|
9590
|
+
component: external_exports.string().max(240),
|
|
9591
|
+
icons: symbols,
|
|
9592
|
+
handler: external_exports.string().max(240).optional(),
|
|
9593
|
+
calls: symbols,
|
|
9594
|
+
argumentBindings: symbols,
|
|
9595
|
+
conditions: symbols,
|
|
9596
|
+
nativeConfirmation: external_exports.object({
|
|
9597
|
+
title: external_exports.string().max(240).optional(),
|
|
9598
|
+
destructiveOption: external_exports.boolean()
|
|
9599
|
+
}).optional()
|
|
9600
|
+
});
|
|
8135
9601
|
var locatorSourceSchema = external_exports.enum([
|
|
8136
9602
|
"appilotsId",
|
|
8137
9603
|
"testID",
|
|
@@ -8310,6 +9776,23 @@ var actionDescriptorSchema = external_exports.object({
|
|
|
8310
9776
|
effect: external_exports.enum(["read", "write", "destructive"]).or(external_exports.string()).optional(),
|
|
8311
9777
|
riskLevel: external_exports.enum(["low", "medium", "high"]).or(external_exports.string()).optional(),
|
|
8312
9778
|
requiresConfirmation: external_exports.boolean().optional(),
|
|
9779
|
+
/**
|
|
9780
|
+
* How long this action's work is expected to take, in milliseconds,
|
|
9781
|
+
* DECLARED by the app — not inferred (that is `appilotsInferred`).
|
|
9782
|
+
*
|
|
9783
|
+
* The SDK's post-action wait was a constant: 6s, or 10s when the
|
|
9784
|
+
* generator could prove the handler awaits something. No constant
|
|
9785
|
+
* fits, because app operations run from ~100ms to minutes, and the
|
|
9786
|
+
* failure is silent in both directions — too short and the agent
|
|
9787
|
+
* photographs a loading screen with no controls on it, too long and
|
|
9788
|
+
* every fast press pays for the slowest one. Only the app knows.
|
|
9789
|
+
*
|
|
9790
|
+
* Absent means absent: the SDK keeps its current defaults, so a
|
|
9791
|
+
* document generated before this field existed behaves exactly as it
|
|
9792
|
+
* did. The SDK also clamps the value to its own safety ceiling — a
|
|
9793
|
+
* declared budget is a request, not a licence to hang the session.
|
|
9794
|
+
*/
|
|
9795
|
+
asyncBudgetMs: external_exports.number().int().positive().optional(),
|
|
8313
9796
|
appilotsInferred: appilotsInferredActionSchema.optional()
|
|
8314
9797
|
}).passthrough();
|
|
8315
9798
|
var screenPermissionDescriptorSchema = external_exports.object({
|
|
@@ -8492,7 +9975,7 @@ external_exports.object({
|
|
|
8492
9975
|
version: external_exports.string().default("1.0"),
|
|
8493
9976
|
content: external_exports.record(external_exports.unknown())
|
|
8494
9977
|
});
|
|
8495
|
-
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
|
|
9978
|
+
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator", "publish"]);
|
|
8496
9979
|
var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
|
|
8497
9980
|
external_exports.object({
|
|
8498
9981
|
name: external_exports.string().min(1).max(100),
|
|
@@ -8503,8 +9986,8 @@ external_exports.object({
|
|
|
8503
9986
|
scope: apiKeyScopeSchema.default("sdk"),
|
|
8504
9987
|
environment: apiKeyEnvironmentSchema.optional(),
|
|
8505
9988
|
expiresAt: external_exports.string().datetime().optional()
|
|
8506
|
-
}).refine((v) => v.scope
|
|
8507
|
-
message:
|
|
9989
|
+
}).refine((v) => v.scope === "operator" || !!v.projectId, {
|
|
9990
|
+
message: "projectId is required for SDK and publishing keys",
|
|
8508
9991
|
path: ["projectId"]
|
|
8509
9992
|
});
|
|
8510
9993
|
var boundedString = (max) => external_exports.string().max(max);
|
|
@@ -8529,10 +10012,21 @@ var snapshotInputSchema = external_exports.object({
|
|
|
8529
10012
|
type: boundedString(40).optional(),
|
|
8530
10013
|
required: external_exports.boolean().optional(),
|
|
8531
10014
|
invalid: external_exports.boolean().optional(),
|
|
10015
|
+
/**
|
|
10016
|
+
* Whether the field holds anything, as its own fact rather than an
|
|
10017
|
+
* inference over `value`. The value is the user's and is the first
|
|
10018
|
+
* thing a privacy policy withholds; the existence of a value is the
|
|
10019
|
+
* agent's and is what stops it filling the same field twice.
|
|
10020
|
+
*/
|
|
10021
|
+
filled: external_exports.boolean().optional(),
|
|
10022
|
+
/** This input has keyboard focus right now. */
|
|
10023
|
+
focused: external_exports.boolean().optional(),
|
|
8532
10024
|
inModal: external_exports.boolean().optional()
|
|
8533
10025
|
}).passthrough();
|
|
8534
10026
|
var snapshotButtonSchema = external_exports.object({
|
|
10027
|
+
controlEvidence: controlEvidenceSchema.optional(),
|
|
8535
10028
|
id: boundedString(160).optional(),
|
|
10029
|
+
dispatchable: external_exports.boolean().optional(),
|
|
8536
10030
|
provenance: identityProvenanceSchema.optional(),
|
|
8537
10031
|
/**
|
|
8538
10032
|
* False when the control is mounted but currently OUTSIDE the window —
|
|
@@ -8593,7 +10087,36 @@ var snapshotListSchema = external_exports.object({
|
|
|
8593
10087
|
*/
|
|
8594
10088
|
source: boundedString(40).optional(),
|
|
8595
10089
|
itemCount: external_exports.number().int().optional(),
|
|
10090
|
+
/**
|
|
10091
|
+
* Size of the whole collection when the app declared it, for a list
|
|
10092
|
+
* that is a WINDOW onto more data than it holds.
|
|
10093
|
+
*
|
|
10094
|
+
* `itemCount` is how many rows the list is rendering from and
|
|
10095
|
+
* `visibleItemCount` how many of those are mounted; neither can
|
|
10096
|
+
* express "there are 36 and you are looking at the first 20",
|
|
10097
|
+
* because a paginated list's `data` is the page. Absent means
|
|
10098
|
+
* unknown — never "same as itemCount".
|
|
10099
|
+
*/
|
|
10100
|
+
totalItemCount: external_exports.number().int().optional(),
|
|
8596
10101
|
visibleItemCount: external_exports.number().int().optional(),
|
|
10102
|
+
viewportItemCount: external_exports.number().int().nonnegative().optional(),
|
|
10103
|
+
exploration: external_exports.object({
|
|
10104
|
+
revision: external_exports.number().int().nonnegative(),
|
|
10105
|
+
observedItemCount: external_exports.number().int().min(0).max(5e3),
|
|
10106
|
+
observedRanges: external_exports.array(
|
|
10107
|
+
external_exports.object({
|
|
10108
|
+
start: external_exports.number().int().nonnegative(),
|
|
10109
|
+
end: external_exports.number().int().nonnegative()
|
|
10110
|
+
})
|
|
10111
|
+
).max(32),
|
|
10112
|
+
rangesTruncated: external_exports.boolean(),
|
|
10113
|
+
coverage: external_exports.enum(["partial", "all-loaded"]),
|
|
10114
|
+
pagination: external_exports.enum(["possible", "not-declared"]),
|
|
10115
|
+
scrollSteps: external_exports.number().int().nonnegative(),
|
|
10116
|
+
remainingScrollSteps: external_exports.number().int().nonnegative(),
|
|
10117
|
+
consecutiveNoProgress: external_exports.number().int().nonnegative(),
|
|
10118
|
+
lastScroll: external_exports.enum(["moved", "no-progress", "boundary", "unverified"]).optional()
|
|
10119
|
+
}).optional(),
|
|
8597
10120
|
refreshing: external_exports.boolean().optional(),
|
|
8598
10121
|
empty: external_exports.boolean().optional(),
|
|
8599
10122
|
label: boundedString(300).optional(),
|
|
@@ -8641,6 +10164,7 @@ var snapshotChoiceGroupSchema = external_exports.object({
|
|
|
8641
10164
|
}).passthrough();
|
|
8642
10165
|
var snapshotElementSchema = external_exports.object({
|
|
8643
10166
|
id: boundedString(160).optional(),
|
|
10167
|
+
dispatchable: external_exports.boolean().optional(),
|
|
8644
10168
|
role: boundedString(40).optional(),
|
|
8645
10169
|
label: boundedString(300).optional(),
|
|
8646
10170
|
texts: external_exports.array(boundedString(500)).max(50).optional(),
|
|
@@ -8684,6 +10208,34 @@ var agentSnapshotSchema = external_exports.object({
|
|
|
8684
10208
|
* it. Optional: older SDKs never clamp and never send it.
|
|
8685
10209
|
*/
|
|
8686
10210
|
truncated: external_exports.boolean().optional(),
|
|
10211
|
+
/**
|
|
10212
|
+
* What moved since the previous observation, computed on the device
|
|
10213
|
+
* because that is the only side holding both snapshots.
|
|
10214
|
+
*
|
|
10215
|
+
* Shape only — counts, booleans, and ids the app declared itself — so
|
|
10216
|
+
* it survives an observation whose content was withheld. ABSENT means
|
|
10217
|
+
* there was no previous observation to compare against; `unchanged:
|
|
10218
|
+
* true` means we compared and nothing moved, which is the strongest
|
|
10219
|
+
* evidence available that an action did nothing. The two must not be
|
|
10220
|
+
* collapsed, for the same reason `truncated` exists.
|
|
10221
|
+
*/
|
|
10222
|
+
delta: external_exports.object({
|
|
10223
|
+
routeChanged: external_exports.boolean().optional(),
|
|
10224
|
+
modalOpened: external_exports.boolean().optional(),
|
|
10225
|
+
modalClosed: external_exports.boolean().optional(),
|
|
10226
|
+
loadingStarted: external_exports.boolean().optional(),
|
|
10227
|
+
loadingFinished: external_exports.boolean().optional(),
|
|
10228
|
+
textsAdded: external_exports.number().int().nonnegative().optional(),
|
|
10229
|
+
textsRemoved: external_exports.number().int().nonnegative().optional(),
|
|
10230
|
+
buttonsAddedIndices: external_exports.array(external_exports.number().int().nonnegative()).max(12).optional(),
|
|
10231
|
+
buttonsRemoved: external_exports.number().int().nonnegative().optional(),
|
|
10232
|
+
visibleRowsDelta: external_exports.number().int().optional(),
|
|
10233
|
+
totalRowsDelta: external_exports.number().int().optional(),
|
|
10234
|
+
fieldsNewlyFilled: external_exports.array(boundedString(160)).max(12).optional(),
|
|
10235
|
+
fieldsCleared: external_exports.array(boundedString(160)).max(12).optional(),
|
|
10236
|
+
invalidAppeared: external_exports.boolean().optional(),
|
|
10237
|
+
unchanged: external_exports.boolean().optional()
|
|
10238
|
+
}).passthrough().optional(),
|
|
8687
10239
|
/**
|
|
8688
10240
|
* How many of this screen's controls the client could name, split by
|
|
8689
10241
|
* `identityProvenance`. Diagnostic only — the relay never grounds an
|
|
@@ -8702,6 +10254,10 @@ var agentSnapshotSchema = external_exports.object({
|
|
|
8702
10254
|
}).passthrough().optional()
|
|
8703
10255
|
}).passthrough();
|
|
8704
10256
|
var agentContextSchema = external_exports.object({
|
|
10257
|
+
missionProtocol: external_exports.literal(1).optional(),
|
|
10258
|
+
missionId: boundedString(160).optional(),
|
|
10259
|
+
/** Preferred supported device language, reported by the SDK independently of map/UI labels. */
|
|
10260
|
+
deviceLocale: external_exports.enum(SUPPORTED_APPILOTS_LOCALES).optional(),
|
|
8705
10261
|
/**
|
|
8706
10262
|
* Client platform this observation was captured on. Optional and
|
|
8707
10263
|
* additive (see `clientPlatformSchema`) — absent means
|
|
@@ -8717,7 +10273,19 @@ var agentContextSchema = external_exports.object({
|
|
|
8717
10273
|
rootRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
|
|
8718
10274
|
currentRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
|
|
8719
10275
|
routeNames: external_exports.array(boundedString(200)).max(500).optional(),
|
|
8720
|
-
canGoBack: external_exports.boolean().optional()
|
|
10276
|
+
canGoBack: external_exports.boolean().optional(),
|
|
10277
|
+
/**
|
|
10278
|
+
* The stack a back press pops through, oldest first, ending on
|
|
10279
|
+
* the current screen. `canGoBack` says a back exists; this says
|
|
10280
|
+
* where it goes.
|
|
10281
|
+
*/
|
|
10282
|
+
backStack: external_exports.array(boundedString(200)).max(50).optional(),
|
|
10283
|
+
/**
|
|
10284
|
+
* Screens the session has been on, oldest first. Names only —
|
|
10285
|
+
* params carry record ids and often personal data, and a route
|
|
10286
|
+
* name is structure.
|
|
10287
|
+
*/
|
|
10288
|
+
visited: external_exports.array(boundedString(200)).max(8).optional()
|
|
8721
10289
|
}).passthrough().optional(),
|
|
8722
10290
|
screenMetadata: external_exports.object({
|
|
8723
10291
|
name: boundedString(200).optional(),
|
|
@@ -8823,7 +10391,7 @@ var formFillPayloadSchema = external_exports.object({
|
|
|
8823
10391
|
submitAfterFill: external_exports.boolean().optional()
|
|
8824
10392
|
}).passthrough();
|
|
8825
10393
|
var uiInteractionPayloadSchema = external_exports.object({
|
|
8826
|
-
action: external_exports.enum(["press", "longPress", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
|
|
10394
|
+
action: external_exports.enum(["press", "longPress", "toggle", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
|
|
8827
10395
|
// `targetId` is the canonical server/LLM field. SDK runtimes still
|
|
8828
10396
|
// accept `componentId` as a compatibility alias.
|
|
8829
10397
|
targetId: external_exports.string().min(1),
|
|
@@ -8940,6 +10508,12 @@ var actionDiagnoseSchema = external_exports.object({
|
|
|
8940
10508
|
requiresUserInput: external_exports.boolean().optional()
|
|
8941
10509
|
});
|
|
8942
10510
|
var actionResultSchema = external_exports.object({
|
|
10511
|
+
nativeConfirmation: external_exports.object({
|
|
10512
|
+
title: external_exports.string().max(300),
|
|
10513
|
+
message: external_exports.string().max(1e3).optional(),
|
|
10514
|
+
buttonLabel: external_exports.string().max(160).optional(),
|
|
10515
|
+
handlerCompleted: external_exports.boolean()
|
|
10516
|
+
}).optional(),
|
|
8943
10517
|
actionId: external_exports.string(),
|
|
8944
10518
|
type: external_exports.string(),
|
|
8945
10519
|
success: external_exports.boolean(),
|
|
@@ -8991,8 +10565,15 @@ external_exports.object({
|
|
|
8991
10565
|
// issue #169 — the observation the server grounds targets against, now
|
|
8992
10566
|
// validated + bounded at the border instead of z.record(z.unknown()).
|
|
8993
10567
|
context: agentContextSchema.optional(),
|
|
8994
|
-
/**
|
|
8995
|
-
|
|
10568
|
+
/**
|
|
10569
|
+
* Hop counter — server enforces a cap to prevent runaway loops.
|
|
10570
|
+
* The max here must stay ABOVE the server's `MAX_AGENT_HOPS` (10 since
|
|
10571
|
+
* #503, apps/api/src/modules/agents/routes.ts): the first over-budget
|
|
10572
|
+
* hop has to get through validation so the route can answer it with
|
|
10573
|
+
* the friendly automation-limit message instead of a 422. The +3
|
|
10574
|
+
* headroom mirrors what 7/10 was before the recalibration.
|
|
10575
|
+
*/
|
|
10576
|
+
hop: external_exports.number().int().min(1).max(13).optional()
|
|
8996
10577
|
});
|
|
8997
10578
|
var agentAccessLevelSchema = external_exports.enum(["read", "write", "none"]);
|
|
8998
10579
|
var screenPermissionSchema = external_exports.object({
|
|
@@ -9060,7 +10641,7 @@ external_exports.object({
|
|
|
9060
10641
|
/** null = remove webhook, undefined = leave unchanged. */
|
|
9061
10642
|
budgetWebhookUrl: external_exports.string().url().nullable().optional()
|
|
9062
10643
|
}).strict();
|
|
9063
|
-
var localeSchema = external_exports.enum(
|
|
10644
|
+
var localeSchema = external_exports.enum(SUPPORTED_APPILOTS_LOCALES);
|
|
9064
10645
|
var hexColorSchema = external_exports.string().regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
|
|
9065
10646
|
message: "Must be a hex color like #6366f1"
|
|
9066
10647
|
});
|
|
@@ -9098,15 +10679,18 @@ var themeTokensSchema = external_exports.object({
|
|
|
9098
10679
|
*/
|
|
9099
10680
|
mode: external_exports.enum(["auto", "light", "dark"]).optional()
|
|
9100
10681
|
}).strict();
|
|
10682
|
+
var imageSourceField = external_exports.string().max(500).refine(isSafeImageUrl, {
|
|
10683
|
+
message: "Only http(s) URLs, base64 image data URIs, emoji or asset ids are allowed here"
|
|
10684
|
+
});
|
|
9101
10685
|
external_exports.object({
|
|
9102
10686
|
/** Tone / persona instructions appended to the system prompt. */
|
|
9103
10687
|
personaPrompt: external_exports.string().max(2048).nullable(),
|
|
9104
10688
|
/** Display name in the chat header (e.g. "Aria"). */
|
|
9105
10689
|
assistantName: external_exports.string().max(60).nullable(),
|
|
9106
10690
|
/** URL or remote asset id for the avatar shown next to assistant turns. */
|
|
9107
|
-
assistantAvatar:
|
|
10691
|
+
assistantAvatar: imageSourceField.nullable(),
|
|
9108
10692
|
/** Emoji or image URL for the empty-state icon. Auto-detected by prefix. */
|
|
9109
|
-
emptyStateIcon:
|
|
10693
|
+
emptyStateIcon: imageSourceField.nullable(),
|
|
9110
10694
|
/** First-message text shown in the empty state. */
|
|
9111
10695
|
welcomeMessage: external_exports.string().max(500).nullable(),
|
|
9112
10696
|
/** Title rendered at the top of the chat. */
|
|
@@ -9120,20 +10704,20 @@ external_exports.object({
|
|
|
9120
10704
|
/** Chat-open FAB background color. Null falls back to theme.colors.primary. */
|
|
9121
10705
|
triggerButtonColor: hexColorSchema.nullable(),
|
|
9122
10706
|
/** Image URL rendered inside the FAB instead of the default chat-bubble icon. */
|
|
9123
|
-
triggerButtonImageUrl:
|
|
10707
|
+
triggerButtonImageUrl: imageSourceField.nullable()
|
|
9124
10708
|
});
|
|
9125
10709
|
external_exports.object({
|
|
9126
10710
|
personaPrompt: external_exports.string().max(2048).nullable().optional(),
|
|
9127
10711
|
assistantName: external_exports.string().max(60).nullable().optional(),
|
|
9128
|
-
assistantAvatar:
|
|
9129
|
-
emptyStateIcon:
|
|
10712
|
+
assistantAvatar: imageSourceField.nullable().optional(),
|
|
10713
|
+
emptyStateIcon: imageSourceField.nullable().optional(),
|
|
9130
10714
|
welcomeMessage: external_exports.string().max(500).nullable().optional(),
|
|
9131
10715
|
chatTitle: external_exports.string().max(60).nullable().optional(),
|
|
9132
10716
|
poweredByVisible: external_exports.boolean().optional(),
|
|
9133
10717
|
theme: themeTokensSchema.nullable().optional(),
|
|
9134
10718
|
defaultLocale: localeSchema.nullable().optional(),
|
|
9135
10719
|
triggerButtonColor: hexColorSchema.nullable().optional(),
|
|
9136
|
-
triggerButtonImageUrl:
|
|
10720
|
+
triggerButtonImageUrl: imageSourceField.nullable().optional()
|
|
9137
10721
|
}).strict();
|
|
9138
10722
|
var sandboxObservationSchema = external_exports.object({
|
|
9139
10723
|
route: external_exports.string().max(200).optional(),
|
|
@@ -9153,7 +10737,19 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
9153
10737
|
type: external_exports.string().max(40).optional(),
|
|
9154
10738
|
placeholder: external_exports.string().max(200).optional(),
|
|
9155
10739
|
/** Inline validation state (SDK snapshot field). */
|
|
9156
|
-
invalid: external_exports.boolean().optional()
|
|
10740
|
+
invalid: external_exports.boolean().optional(),
|
|
10741
|
+
/**
|
|
10742
|
+
* Whether the field holds anything, as its own fact rather than
|
|
10743
|
+
* an inference over `value`.
|
|
10744
|
+
*
|
|
10745
|
+
* Here because the eval posts through this schema: a fact the
|
|
10746
|
+
* SDK produces and this contract has no word for is a fact the
|
|
10747
|
+
* corpus can never grade the agent on. `invalid` and `required`
|
|
10748
|
+
* were already here; these two were the half that was missing.
|
|
10749
|
+
*/
|
|
10750
|
+
filled: external_exports.boolean().optional(),
|
|
10751
|
+
/** This input has keyboard focus right now. */
|
|
10752
|
+
focused: external_exports.boolean().optional()
|
|
9157
10753
|
})
|
|
9158
10754
|
).max(100).optional(),
|
|
9159
10755
|
buttons: external_exports.array(
|
|
@@ -9632,7 +11228,7 @@ var manifestSchema = external_exports.object({
|
|
|
9632
11228
|
navigation: navigationGraphSchema.partial().optional()
|
|
9633
11229
|
}).passthrough();
|
|
9634
11230
|
async function loadManifest(rootDir, manifestPath) {
|
|
9635
|
-
const resolvedPath =
|
|
11231
|
+
const resolvedPath = path2__namespace.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
|
|
9636
11232
|
let raw;
|
|
9637
11233
|
try {
|
|
9638
11234
|
raw = await fs.readFile(resolvedPath, "utf-8");
|
|
@@ -9715,6 +11311,12 @@ function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
|
|
|
9715
11311
|
};
|
|
9716
11312
|
}
|
|
9717
11313
|
|
|
11314
|
+
// src/generators/checksum.ts
|
|
11315
|
+
function serializeForChecksum(document) {
|
|
11316
|
+
const { generatedAt: _generatedAt, ...content } = document;
|
|
11317
|
+
return JSON.stringify(content, null, 2);
|
|
11318
|
+
}
|
|
11319
|
+
|
|
9718
11320
|
// src/pipeline/enrichment.ts
|
|
9719
11321
|
function enrichScreenForAgent(screen) {
|
|
9720
11322
|
const targets = mergeTargets([
|
|
@@ -9791,7 +11393,10 @@ function mergeTargets(targets) {
|
|
|
9791
11393
|
for (const target of targets) {
|
|
9792
11394
|
if (!target.id) continue;
|
|
9793
11395
|
const existing = byId.get(target.id);
|
|
9794
|
-
byId.set(
|
|
11396
|
+
byId.set(
|
|
11397
|
+
target.id,
|
|
11398
|
+
existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target
|
|
11399
|
+
);
|
|
9795
11400
|
}
|
|
9796
11401
|
return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
9797
11402
|
}
|
|
@@ -9836,7 +11441,10 @@ function synthesizeFlows(screen, targets) {
|
|
|
9836
11441
|
intent: "destructive_action",
|
|
9837
11442
|
steps: [
|
|
9838
11443
|
{ type: "press", target: action.id, label: action.label },
|
|
9839
|
-
{
|
|
11444
|
+
{
|
|
11445
|
+
type: "confirm",
|
|
11446
|
+
description: "Wait for native or custom confirmation before continuing"
|
|
11447
|
+
},
|
|
9840
11448
|
{ type: "wait", description: describeWait(action) }
|
|
9841
11449
|
],
|
|
9842
11450
|
waitPolicy: waitPolicyForAction(action),
|
|
@@ -9851,10 +11459,19 @@ function synthesizeFlows(screen, targets) {
|
|
|
9851
11459
|
title: `Act on an item in ${collection.id}`,
|
|
9852
11460
|
intent: "list_action",
|
|
9853
11461
|
steps: [
|
|
9854
|
-
{
|
|
9855
|
-
|
|
11462
|
+
{
|
|
11463
|
+
type: "choose-list-item",
|
|
11464
|
+
target: collection.id,
|
|
11465
|
+
description: "Resolve the user reference to a visible or searchable row"
|
|
11466
|
+
},
|
|
11467
|
+
{
|
|
11468
|
+
type: "press",
|
|
11469
|
+
description: collection.rowAction?.description ?? "Open the row action"
|
|
11470
|
+
}
|
|
9856
11471
|
],
|
|
9857
|
-
waitPolicy: {
|
|
11472
|
+
waitPolicy: {
|
|
11473
|
+
expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback"
|
|
11474
|
+
}
|
|
9858
11475
|
});
|
|
9859
11476
|
}
|
|
9860
11477
|
}
|
|
@@ -9862,25 +11479,32 @@ function synthesizeFlows(screen, targets) {
|
|
|
9862
11479
|
}
|
|
9863
11480
|
function waitPolicyForAction(action) {
|
|
9864
11481
|
const expectedOutcome = action.successSignal?.type === "goBack" ? "goBack" : action.appilotsInferred?.expectedOutcome ?? (action.targetScreen ? "navigation" : void 0);
|
|
9865
|
-
const signals = [action.successSignal, action.failureSignal].filter(
|
|
11482
|
+
const signals = [action.successSignal, action.failureSignal].filter(
|
|
11483
|
+
Boolean
|
|
11484
|
+
);
|
|
11485
|
+
const maxMs = action.asyncBudgetMs ?? (action.appilotsInferred?.isAsyncTrigger ? 1e4 : void 0);
|
|
9866
11486
|
return {
|
|
9867
11487
|
...expectedOutcome ? { expectedOutcome } : {},
|
|
9868
11488
|
...signals && signals.length > 0 ? { signals } : {},
|
|
9869
|
-
...
|
|
11489
|
+
...maxMs !== void 0 ? { maxMs } : {}
|
|
9870
11490
|
};
|
|
9871
11491
|
}
|
|
9872
11492
|
function describeWait(action) {
|
|
9873
11493
|
if (action.successSignal?.description) return action.successSignal.description;
|
|
9874
|
-
if (action.successSignal?.type === "goBack")
|
|
11494
|
+
if (action.successSignal?.type === "goBack")
|
|
11495
|
+
return "Wait for the app to return to the previous screen";
|
|
9875
11496
|
if (action.targetScreen) return `Wait for navigation to ${action.targetScreen}`;
|
|
9876
|
-
if (action.appilotsInferred?.expectedOutcome)
|
|
11497
|
+
if (action.appilotsInferred?.expectedOutcome)
|
|
11498
|
+
return `Wait for ${action.appilotsInferred.expectedOutcome}`;
|
|
9877
11499
|
return "Wait for the UI to settle";
|
|
9878
11500
|
}
|
|
9879
11501
|
function synthesizeAgentHints(screen, targets, flows) {
|
|
9880
11502
|
const preferredTargets = targets.filter((target) => ["submit", "button", "list"].includes(target.role)).slice(0, 8).map((target) => target.id);
|
|
9881
11503
|
const commonTasks = flows.slice(0, 6).map((flow) => flow.title);
|
|
9882
11504
|
const safetyNotes = screen.actions.filter((action) => action.destructive || action.requiresConfirmation).map((action) => `${action.id} requires confirmation`);
|
|
9883
|
-
const firstAsyncAction = screen.actions.find(
|
|
11505
|
+
const firstAsyncAction = screen.actions.find(
|
|
11506
|
+
(action) => action.appilotsInferred?.isAsyncTrigger || action.asyncBudgetMs !== void 0
|
|
11507
|
+
);
|
|
9884
11508
|
const hints = {
|
|
9885
11509
|
primaryGoal: synthesizePrimaryGoal(screen),
|
|
9886
11510
|
commonTasks,
|
|
@@ -9907,7 +11531,9 @@ function synthesizePrimaryGoal(screen) {
|
|
|
9907
11531
|
}
|
|
9908
11532
|
const fieldCount = uniqueFields.size;
|
|
9909
11533
|
if (fieldCount > 0) {
|
|
9910
|
-
const requiredCount = Array.from(uniqueFields.values()).filter(
|
|
11534
|
+
const requiredCount = Array.from(uniqueFields.values()).filter(
|
|
11535
|
+
(field) => field.required
|
|
11536
|
+
).length;
|
|
9911
11537
|
const fieldLabel = fieldCount === 1 ? "field" : "fields";
|
|
9912
11538
|
goals.push(
|
|
9913
11539
|
requiredCount > 0 ? `Complete a form with ${fieldCount} ${fieldLabel} (${requiredCount} required)` : `Complete a form with ${fieldCount} ${fieldLabel}`
|
|
@@ -9921,7 +11547,9 @@ function synthesizePrimaryGoal(screen) {
|
|
|
9921
11547
|
if (submitCount > 0) {
|
|
9922
11548
|
goals.push(`Submit ${submitCount === 1 ? "the primary form" : `${submitCount} forms/actions`}`);
|
|
9923
11549
|
}
|
|
9924
|
-
const asyncCount = screen.actions.filter(
|
|
11550
|
+
const asyncCount = screen.actions.filter(
|
|
11551
|
+
(action) => action.appilotsInferred?.isAsyncTrigger
|
|
11552
|
+
).length;
|
|
9925
11553
|
if (asyncCount > 0) {
|
|
9926
11554
|
goals.push(`Wait for ${asyncCount === 1 ? "async feedback" : "async action feedback"}`);
|
|
9927
11555
|
}
|
|
@@ -9996,7 +11624,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
9996
11624
|
const agentReadyScreens = mergedScreens.map(
|
|
9997
11625
|
(screen) => enrichScreenForAgent({
|
|
9998
11626
|
...screen,
|
|
9999
|
-
filePath: screen.filePath ?
|
|
11627
|
+
filePath: screen.filePath ? path2__namespace.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
|
|
10000
11628
|
})
|
|
10001
11629
|
);
|
|
10002
11630
|
const projectInfo = await this.getProjectInfo();
|
|
@@ -10020,19 +11648,33 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10020
11648
|
}
|
|
10021
11649
|
};
|
|
10022
11650
|
const serialized = JSON.stringify(document, null, 2);
|
|
10023
|
-
const checksum = this.calculateChecksum(
|
|
10024
|
-
const filePath =
|
|
11651
|
+
const checksum = this.calculateChecksum(serializeForChecksum(document));
|
|
11652
|
+
const filePath = path2__namespace.default.resolve(outputDir, `mcp-document.${this.options.format}`);
|
|
10025
11653
|
await fs.writeFile(filePath, serialized, "utf-8");
|
|
10026
11654
|
console.log(`[MCPGenerator] Document written to: ${filePath}`);
|
|
10027
|
-
const
|
|
11655
|
+
const controlFiles = analyzed.controlEvidenceFiles ?? {};
|
|
11656
|
+
await fs.writeFile(
|
|
11657
|
+
path2__namespace.default.resolve(outputDir, "control-evidence.json"),
|
|
11658
|
+
JSON.stringify({ version: 1, files: controlFiles }, null, 2),
|
|
11659
|
+
"utf-8"
|
|
11660
|
+
);
|
|
11661
|
+
const checksumFilePath = path2__namespace.default.resolve(outputDir, ".appilots-checksum");
|
|
10028
11662
|
await fs.writeFile(checksumFilePath, checksum, "utf-8");
|
|
10029
11663
|
console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
|
|
11664
|
+
const evidenceCount = Object.values(controlFiles).reduce(
|
|
11665
|
+
(sum, entries) => sum + entries.length,
|
|
11666
|
+
0
|
|
11667
|
+
);
|
|
11668
|
+
console.log(
|
|
11669
|
+
`[MCPGenerator] Source evidence: ${evidenceCount} icon controls across ${Object.keys(controlFiles).length} files (runtime binding required)`
|
|
11670
|
+
);
|
|
10030
11671
|
console.log("[MCPGenerator] Generation complete!");
|
|
10031
11672
|
return {
|
|
10032
11673
|
document,
|
|
10033
11674
|
filePath,
|
|
10034
11675
|
format: this.options.format,
|
|
10035
|
-
checksum
|
|
11676
|
+
checksum,
|
|
11677
|
+
...analyzed.diagnostics ? { diagnostics: analyzed.diagnostics } : {}
|
|
10036
11678
|
};
|
|
10037
11679
|
}
|
|
10038
11680
|
/**
|
|
@@ -10044,7 +11686,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10044
11686
|
* after calling `generate()`.
|
|
10045
11687
|
*/
|
|
10046
11688
|
static async readPreviousChecksum(outputDir) {
|
|
10047
|
-
const checksumFilePath =
|
|
11689
|
+
const checksumFilePath = path2__namespace.default.resolve(outputDir, ".appilots-checksum");
|
|
10048
11690
|
try {
|
|
10049
11691
|
const content = await fs.readFile(checksumFilePath, "utf-8");
|
|
10050
11692
|
return content.trim() || null;
|
|
@@ -10063,7 +11705,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10063
11705
|
*/
|
|
10064
11706
|
async getProjectInfo() {
|
|
10065
11707
|
try {
|
|
10066
|
-
const packageJsonPath =
|
|
11708
|
+
const packageJsonPath = path2__namespace.default.resolve(this.analyzerConfig.rootDir, "package.json");
|
|
10067
11709
|
const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
|
|
10068
11710
|
const packageJson = JSON.parse(packageJsonContent);
|
|
10069
11711
|
return {
|
|
@@ -10091,7 +11733,8 @@ var KNOWN_CONFIG_KEYS = [
|
|
|
10091
11733
|
"navigationExclude",
|
|
10092
11734
|
"platform",
|
|
10093
11735
|
"manifestPath",
|
|
10094
|
-
"eval"
|
|
11736
|
+
"eval",
|
|
11737
|
+
"knowledge"
|
|
10095
11738
|
];
|
|
10096
11739
|
var KEY_ALIASES = {
|
|
10097
11740
|
apiUrl: "serverUrl",
|
|
@@ -10146,12 +11789,12 @@ function getEnvOverrides(env = process.env) {
|
|
|
10146
11789
|
return trimmed ? trimmed : void 0;
|
|
10147
11790
|
};
|
|
10148
11791
|
return {
|
|
10149
|
-
apiKey: clean(env.APPILOTS_API_KEY),
|
|
11792
|
+
apiKey: clean(env.APPILOTS_PUBLISH_KEY) ?? clean(env.APPILOTS_API_KEY),
|
|
10150
11793
|
projectId: clean(env.APPILOTS_PROJECT_ID),
|
|
10151
11794
|
serverUrl: clean(env.APPILOTS_SERVER_URL)
|
|
10152
11795
|
};
|
|
10153
11796
|
}
|
|
10154
|
-
function loadConfig(onWarn) {
|
|
11797
|
+
function loadConfig(onWarn, options = {}) {
|
|
10155
11798
|
const configPath = getConfigPath();
|
|
10156
11799
|
const env = getEnvOverrides();
|
|
10157
11800
|
let fileConfig = {};
|
|
@@ -10190,6 +11833,10 @@ function loadConfig(onWarn) {
|
|
|
10190
11833
|
};
|
|
10191
11834
|
const validation = validateConfig(merged);
|
|
10192
11835
|
validation.warnings.unshift(...legacyWarnings);
|
|
11836
|
+
if (options.requireApiKey === false) {
|
|
11837
|
+
validation.errors = validation.errors.filter((e) => !e.startsWith("apiKey"));
|
|
11838
|
+
validation.valid = validation.errors.length === 0;
|
|
11839
|
+
}
|
|
10193
11840
|
if (fileConfig.serverUrl === void 0 && env.serverUrl === void 0) {
|
|
10194
11841
|
for (const key of Object.keys(fileConfig)) {
|
|
10195
11842
|
if (KNOWN_CONFIG_KEYS.includes(key)) continue;
|
|
@@ -10219,7 +11866,7 @@ function loadConfig(onWarn) {
|
|
|
10219
11866
|
return merged;
|
|
10220
11867
|
}
|
|
10221
11868
|
function saveConfig(dir, config) {
|
|
10222
|
-
const configPath =
|
|
11869
|
+
const configPath = path2.join(dir, ".appilotsrc");
|
|
10223
11870
|
const existingConfig = getConfigPath() ? loadConfig() : null;
|
|
10224
11871
|
const mergedConfig = {
|
|
10225
11872
|
apiKey: config.apiKey || existingConfig?.apiKey || "",
|
|
@@ -10235,7 +11882,8 @@ function saveConfig(dir, config) {
|
|
|
10235
11882
|
navigationExclude: config.navigationExclude || existingConfig?.navigationExclude,
|
|
10236
11883
|
platform: config.platform || existingConfig?.platform,
|
|
10237
11884
|
manifestPath: config.manifestPath || existingConfig?.manifestPath,
|
|
10238
|
-
eval: config.eval || existingConfig?.eval
|
|
11885
|
+
eval: config.eval || existingConfig?.eval,
|
|
11886
|
+
knowledge: config.knowledge || existingConfig?.knowledge
|
|
10239
11887
|
};
|
|
10240
11888
|
try {
|
|
10241
11889
|
fs$1.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
|
|
@@ -10246,19 +11894,19 @@ function saveConfig(dir, config) {
|
|
|
10246
11894
|
}
|
|
10247
11895
|
}
|
|
10248
11896
|
function getConfigPath() {
|
|
10249
|
-
let currentDir =
|
|
10250
|
-
const root =
|
|
11897
|
+
let currentDir = path2.resolve(process.cwd());
|
|
11898
|
+
const root = path2.resolve("/");
|
|
10251
11899
|
while (currentDir !== root) {
|
|
10252
|
-
const configPath =
|
|
11900
|
+
const configPath = path2.join(currentDir, ".appilotsrc");
|
|
10253
11901
|
try {
|
|
10254
11902
|
if (fs$1.existsSync(configPath) && fs$1.statSync(configPath).isFile()) {
|
|
10255
11903
|
return configPath;
|
|
10256
11904
|
}
|
|
10257
11905
|
} catch {
|
|
10258
11906
|
}
|
|
10259
|
-
currentDir =
|
|
11907
|
+
currentDir = path2.resolve(currentDir, "..");
|
|
10260
11908
|
}
|
|
10261
|
-
const rootConfigPath =
|
|
11909
|
+
const rootConfigPath = path2.join(root, ".appilotsrc");
|
|
10262
11910
|
try {
|
|
10263
11911
|
if (fs$1.existsSync(rootConfigPath) && fs$1.statSync(rootConfigPath).isFile()) {
|
|
10264
11912
|
return rootConfigPath;
|
|
@@ -10328,6 +11976,19 @@ function validateConfig(config) {
|
|
|
10328
11976
|
if (config.manifestPath !== void 0 && typeof config.manifestPath !== "string") {
|
|
10329
11977
|
errors.push("manifestPath must be a string");
|
|
10330
11978
|
}
|
|
11979
|
+
if (config.knowledge !== void 0) {
|
|
11980
|
+
if (typeof config.knowledge !== "object" || config.knowledge === null || Array.isArray(config.knowledge)) {
|
|
11981
|
+
errors.push("knowledge must be an object");
|
|
11982
|
+
} else {
|
|
11983
|
+
const kc = config.knowledge;
|
|
11984
|
+
for (const field of ["sources", "exclude"]) {
|
|
11985
|
+
const value = kc[field];
|
|
11986
|
+
if (value !== void 0 && (!Array.isArray(value) || !value.every((item) => typeof item === "string"))) {
|
|
11987
|
+
errors.push(`knowledge.${field} must be an array of strings`);
|
|
11988
|
+
}
|
|
11989
|
+
}
|
|
11990
|
+
}
|
|
11991
|
+
}
|
|
10331
11992
|
if (config.eval !== void 0) {
|
|
10332
11993
|
if (typeof config.eval !== "object" || config.eval === null || Array.isArray(config.eval)) {
|
|
10333
11994
|
errors.push("eval must be an object");
|
|
@@ -10604,6 +12265,48 @@ var AppilotsAPIClient = class {
|
|
|
10604
12265
|
};
|
|
10605
12266
|
}
|
|
10606
12267
|
}
|
|
12268
|
+
/**
|
|
12269
|
+
* Syncs knowledge documents with the Appilots backend.
|
|
12270
|
+
*
|
|
12271
|
+
* @param documents Array of documents to sync (filename, base64 content, mimeType, checksum)
|
|
12272
|
+
* @returns KnowledgeSyncResult with counts of uploaded, skipped, and errored docs
|
|
12273
|
+
*/
|
|
12274
|
+
async knowledgeSync(documents) {
|
|
12275
|
+
try {
|
|
12276
|
+
const response = await this.request(`${this.baseUrl}/cli/knowledge/sync`, {
|
|
12277
|
+
method: "POST",
|
|
12278
|
+
headers: {
|
|
12279
|
+
"Content-Type": "application/json",
|
|
12280
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
12281
|
+
},
|
|
12282
|
+
body: JSON.stringify({ documents })
|
|
12283
|
+
});
|
|
12284
|
+
if (!response.ok) {
|
|
12285
|
+
const errorData = await response.json().catch(() => ({}));
|
|
12286
|
+
return {
|
|
12287
|
+
success: false,
|
|
12288
|
+
uploaded: 0,
|
|
12289
|
+
skipped: 0,
|
|
12290
|
+
errors: 0,
|
|
12291
|
+
error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`)
|
|
12292
|
+
};
|
|
12293
|
+
}
|
|
12294
|
+
const json = await response.json();
|
|
12295
|
+
const inner = json.data ?? json;
|
|
12296
|
+
return {
|
|
12297
|
+
success: true,
|
|
12298
|
+
...inner
|
|
12299
|
+
};
|
|
12300
|
+
} catch (error) {
|
|
12301
|
+
return {
|
|
12302
|
+
success: false,
|
|
12303
|
+
uploaded: 0,
|
|
12304
|
+
skipped: 0,
|
|
12305
|
+
errors: 0,
|
|
12306
|
+
error: error instanceof Error ? error.message : "Failed to sync knowledge with Appilots API"
|
|
12307
|
+
};
|
|
12308
|
+
}
|
|
12309
|
+
}
|
|
10607
12310
|
/**
|
|
10608
12311
|
* Checks if the Appilots API server is healthy
|
|
10609
12312
|
*
|
|
@@ -10622,7 +12325,7 @@ var AppilotsAPIClient = class {
|
|
|
10622
12325
|
};
|
|
10623
12326
|
|
|
10624
12327
|
// src/version.ts
|
|
10625
|
-
var CLI_VERSION = "0.
|
|
12328
|
+
var CLI_VERSION = "0.13.0";
|
|
10626
12329
|
|
|
10627
12330
|
exports.AppilotsAPIClient = AppilotsAPIClient;
|
|
10628
12331
|
exports.CLI_VERSION = CLI_VERSION;
|