@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.mjs
CHANGED
|
@@ -1,19 +1,363 @@
|
|
|
1
1
|
import fs, { readFile, mkdir, writeFile } from 'fs/promises';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import traverse4 from '@babel/traverse';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import traverse5 from '@babel/traverse';
|
|
5
4
|
import * as BabelTypes from '@babel/types';
|
|
6
|
-
import
|
|
5
|
+
import * as path2 from 'path';
|
|
6
|
+
import path2__default, { join, resolve } from 'path';
|
|
7
|
+
import fastGlob from 'fast-glob';
|
|
7
8
|
import * as parser from '@babel/parser';
|
|
8
9
|
import { parse } from '@babel/parser';
|
|
9
|
-
import { promises, readFileSync, writeFileSync, existsSync, statSync } from 'fs';
|
|
10
|
-
import { createHash } from 'crypto';
|
|
10
|
+
import { promises, readFileSync, writeFileSync, existsSync, statSync, readdirSync } from 'fs';
|
|
11
11
|
|
|
12
12
|
var __defProp = Object.defineProperty;
|
|
13
13
|
var __export = (target, all) => {
|
|
14
14
|
for (var name in all)
|
|
15
15
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
16
|
};
|
|
17
|
+
var MAX_HOPS = 8;
|
|
18
|
+
function unwrap(path11) {
|
|
19
|
+
while (path11.isTSAsExpression() || path11.isTSTypeAssertion() || path11.isTSNonNullExpression() || path11.isTSSatisfiesExpression() || path11.isParenthesizedExpression())
|
|
20
|
+
path11 = path11.get("expression");
|
|
21
|
+
return path11;
|
|
22
|
+
}
|
|
23
|
+
function constantValue(path11, depth = 0) {
|
|
24
|
+
if (depth > MAX_HOPS) return void 0;
|
|
25
|
+
path11 = unwrap(path11);
|
|
26
|
+
if (!path11.isIdentifier()) return path11;
|
|
27
|
+
const binding = path11.scope.getBinding(path11.node.name);
|
|
28
|
+
if (!binding?.constant || !binding.path.isVariableDeclarator()) return path11;
|
|
29
|
+
const init = binding.path.get("init");
|
|
30
|
+
const resolved = init.node ? constantValue(init, depth + 1) : void 0;
|
|
31
|
+
if (resolved?.isObjectExpression() && binding.referencePaths.some(
|
|
32
|
+
(reference) => !reference.parentPath?.isJSXSpreadAttribute() && !reference.parentPath?.isSpreadElement()
|
|
33
|
+
))
|
|
34
|
+
return void 0;
|
|
35
|
+
return resolved;
|
|
36
|
+
}
|
|
37
|
+
function propValue(path11, name) {
|
|
38
|
+
const readObject = (path12) => {
|
|
39
|
+
const resolved = constantValue(path12);
|
|
40
|
+
if (!resolved?.isObjectExpression()) return { blocked: true };
|
|
41
|
+
for (const property of [...resolved.get("properties")].reverse()) {
|
|
42
|
+
if (property.isSpreadElement()) {
|
|
43
|
+
const found = readObjectBounded(property.get("argument"));
|
|
44
|
+
if (found.value || found.blocked) return found;
|
|
45
|
+
} else if (property.isObjectProperty() || property.isObjectMethod()) {
|
|
46
|
+
if (property.node.computed) return { blocked: true };
|
|
47
|
+
const key = property.node.key;
|
|
48
|
+
if ((BabelTypes.isIdentifier(key) ? key.name : BabelTypes.isStringLiteral(key) ? key.value : "") === name)
|
|
49
|
+
return property.isObjectProperty() ? { value: property.get("value") } : { value: property };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {};
|
|
53
|
+
};
|
|
54
|
+
let objectBudget = MAX_HOPS;
|
|
55
|
+
const readObjectBounded = (path12) => objectBudget-- > 0 ? readObject(path12) : { blocked: true };
|
|
56
|
+
for (const attr of [...path11.get("attributes")].reverse()) {
|
|
57
|
+
if (attr.isJSXAttribute() && BabelTypes.isJSXIdentifier(attr.node.name, { name })) {
|
|
58
|
+
const value = attr.get("value");
|
|
59
|
+
return value.isJSXExpressionContainer() ? unwrap(value.get("expression")) : value.node ? value : void 0;
|
|
60
|
+
}
|
|
61
|
+
if (attr.isJSXSpreadAttribute()) {
|
|
62
|
+
const found = readObjectBounded(attr.get("argument"));
|
|
63
|
+
if (found.value || found.blocked) return found.value;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
function importIdentity(path11, depth = 0) {
|
|
69
|
+
if (depth > MAX_HOPS) return void 0;
|
|
70
|
+
path11 = unwrap(path11);
|
|
71
|
+
if (path11.isIdentifier() || path11.isJSXIdentifier()) {
|
|
72
|
+
const binding = path11.scope.getBinding(path11.node.name);
|
|
73
|
+
if (!binding?.constant) return void 0;
|
|
74
|
+
const declaration = binding.path;
|
|
75
|
+
if (declaration.parentPath?.isImportDeclaration()) {
|
|
76
|
+
const module = declaration.parentPath.node.source.value;
|
|
77
|
+
if (declaration.isImportSpecifier()) {
|
|
78
|
+
const imported = declaration.node.imported;
|
|
79
|
+
return { module, imported: BabelTypes.isIdentifier(imported) ? imported.name : imported.value };
|
|
80
|
+
}
|
|
81
|
+
if (declaration.isImportDefaultSpecifier()) return { module, imported: "default" };
|
|
82
|
+
if (declaration.isImportNamespaceSpecifier()) return { module, imported: "*" };
|
|
83
|
+
}
|
|
84
|
+
if (declaration.isVariableDeclarator() && declaration.get("init").node)
|
|
85
|
+
return importIdentity(declaration.get("init"), depth + 1);
|
|
86
|
+
}
|
|
87
|
+
if (path11.isJSXMemberExpression() || path11.isMemberExpression() && !path11.node.computed) {
|
|
88
|
+
const origin = importIdentity(path11.get("object"), depth + 1);
|
|
89
|
+
const key = path11.node.property;
|
|
90
|
+
if (origin && (BabelTypes.isIdentifier(key) || BabelTypes.isJSXIdentifier(key)) && (origin.imported === "*" || origin.module === "react" && origin.imported === "default"))
|
|
91
|
+
return { module: origin.module, imported: key.name };
|
|
92
|
+
}
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
function handlerFunction(path11, depth = 0) {
|
|
96
|
+
if (depth > MAX_HOPS) return void 0;
|
|
97
|
+
path11 = unwrap(path11);
|
|
98
|
+
if (path11.isFunction()) return path11;
|
|
99
|
+
if (path11.isIdentifier()) {
|
|
100
|
+
const binding = path11.scope.getBinding(path11.node.name);
|
|
101
|
+
if (!binding?.constant) return void 0;
|
|
102
|
+
if (binding.path.isFunctionDeclaration()) return binding.path;
|
|
103
|
+
if (binding.path.isVariableDeclarator() && binding.path.get("init").node)
|
|
104
|
+
return handlerFunction(binding.path.get("init"), depth + 1);
|
|
105
|
+
}
|
|
106
|
+
if (path11.isCallExpression()) {
|
|
107
|
+
const origin = importIdentity(path11.get("callee"));
|
|
108
|
+
if (origin?.module === "react" && origin.imported === "useCallback") {
|
|
109
|
+
const callback = path11.get("arguments")[0];
|
|
110
|
+
if (callback) return handlerFunction(callback, depth + 1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (path11.isMemberExpression() && !path11.node.computed && BabelTypes.isThisExpression(path11.node.object)) {
|
|
114
|
+
const key = path11.node.property;
|
|
115
|
+
if (!BabelTypes.isIdentifier(key)) return void 0;
|
|
116
|
+
const owner = path11.findParent((p) => p.isClassDeclaration() || p.isClassExpression());
|
|
117
|
+
if (!owner || !(owner.isClassDeclaration() || owner.isClassExpression())) return void 0;
|
|
118
|
+
for (const member of owner.get("body").get("body")) {
|
|
119
|
+
if (!(member.isClassMethod() || member.isClassProperty()) || member.node.computed || member.node.static)
|
|
120
|
+
continue;
|
|
121
|
+
if (!BabelTypes.isIdentifier(member.node.key, { name: key.name })) continue;
|
|
122
|
+
if (member.isClassMethod() && member.node.kind === "method") return member;
|
|
123
|
+
if (member.isClassProperty() && member.get("value").node)
|
|
124
|
+
return handlerFunction(member.get("value"), depth + 1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return void 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/extractors/control-evidence.ts
|
|
131
|
+
function symbol(node) {
|
|
132
|
+
if (BabelTypes.isTSAsExpression(node) || BabelTypes.isTSTypeAssertion(node) || BabelTypes.isTSNonNullExpression(node) || BabelTypes.isTSSatisfiesExpression(node))
|
|
133
|
+
return symbol(node.expression);
|
|
134
|
+
if (BabelTypes.isIdentifier(node) || BabelTypes.isJSXIdentifier(node)) return node.name;
|
|
135
|
+
if (BabelTypes.isThisExpression(node)) return "this";
|
|
136
|
+
if (BabelTypes.isMemberExpression(node) && !node.computed || BabelTypes.isJSXMemberExpression(node)) {
|
|
137
|
+
const object = symbol(node.object), property = symbol(node.property);
|
|
138
|
+
return object && property ? `${object}.${property}` : void 0;
|
|
139
|
+
}
|
|
140
|
+
if (BabelTypes.isUnaryExpression(node) && node.operator === "!") {
|
|
141
|
+
const value = symbol(node.argument);
|
|
142
|
+
return value ? `!${value}` : void 0;
|
|
143
|
+
}
|
|
144
|
+
return void 0;
|
|
145
|
+
}
|
|
146
|
+
function attribute(node, name) {
|
|
147
|
+
return node.attributes.find(
|
|
148
|
+
(a) => BabelTypes.isJSXAttribute(a) && BabelTypes.isJSXIdentifier(a.name, { name })
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
var ICON_LIBRARIES = [
|
|
152
|
+
"lucide-react-native",
|
|
153
|
+
"lucide-react",
|
|
154
|
+
"@tamagui/lucide-icons",
|
|
155
|
+
"@expo/vector-icons",
|
|
156
|
+
"@react-native-vector-icons/",
|
|
157
|
+
"react-native-vector-icons/"
|
|
158
|
+
];
|
|
159
|
+
function iconLibrary(module) {
|
|
160
|
+
return ICON_LIBRARIES.some(
|
|
161
|
+
(name) => name.endsWith("/") ? module.startsWith(name) : module === name || module.startsWith(name + "/")
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
function add(values, value, max = 12) {
|
|
165
|
+
if (value && value.length <= 240 && values.length < max && !values.includes(value))
|
|
166
|
+
values.push(value);
|
|
167
|
+
}
|
|
168
|
+
function iconName(path11, explicitIcon = false) {
|
|
169
|
+
const origin = importIdentity(path11);
|
|
170
|
+
if (origin && iconLibrary(origin.module) && origin.imported !== "*")
|
|
171
|
+
return origin.imported === "default" ? origin.module : `${origin.module}:${origin.imported}`;
|
|
172
|
+
const name = symbol(path11.node);
|
|
173
|
+
return name && (explicitIcon || /icon/i.test(name)) ? name : void 0;
|
|
174
|
+
}
|
|
175
|
+
function iconsInElement(path11, explicitIcon = false) {
|
|
176
|
+
const name = iconName(path11.get("name"), explicitIcon);
|
|
177
|
+
if (!name) return void 0;
|
|
178
|
+
const glyph = propValue(path11, "name");
|
|
179
|
+
const value = glyph && constantValue(glyph);
|
|
180
|
+
return value?.isStringLiteral() ? `${name}:${value.node.value}` : name;
|
|
181
|
+
}
|
|
182
|
+
function hasInteraction(path11) {
|
|
183
|
+
return ["onPress", "onLongPress", "onClick"].some(
|
|
184
|
+
(name) => attribute(path11.node, name) || propValue(path11, name)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
function collectPresentationIcons(path11, icons) {
|
|
188
|
+
const namedIconProps = [
|
|
189
|
+
"icon",
|
|
190
|
+
"prefix",
|
|
191
|
+
"suffix",
|
|
192
|
+
"left",
|
|
193
|
+
"right",
|
|
194
|
+
"leadingIcon",
|
|
195
|
+
"trailingIcon",
|
|
196
|
+
"startIcon",
|
|
197
|
+
"endIcon",
|
|
198
|
+
"renderIcon"
|
|
199
|
+
];
|
|
200
|
+
const props = new Set(namedIconProps);
|
|
201
|
+
for (const attr of path11.node.attributes)
|
|
202
|
+
if (BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name) && !/^on[A-Z]/.test(attr.name.name))
|
|
203
|
+
props.add(attr.name.name);
|
|
204
|
+
for (const prop of props) {
|
|
205
|
+
const icon = propValue(path11, prop);
|
|
206
|
+
if (icon) {
|
|
207
|
+
const value = constantValue(icon);
|
|
208
|
+
if (value?.isStringLiteral()) {
|
|
209
|
+
if (prop.toLowerCase().includes("icon")) add(icons, value.node.value, 8);
|
|
210
|
+
} else {
|
|
211
|
+
const explicitIcon = prop.toLowerCase() === "icon";
|
|
212
|
+
if (icon.isJSXElement()) {
|
|
213
|
+
if (hasInteraction(icon.get("openingElement"))) continue;
|
|
214
|
+
add(icons, iconsInElement(icon.get("openingElement"), explicitIcon), 8);
|
|
215
|
+
}
|
|
216
|
+
icon.traverse({
|
|
217
|
+
JSXAttribute(attr) {
|
|
218
|
+
if (BabelTypes.isJSXIdentifier(attr.node.name) && /^on[A-Z]/.test(attr.node.name.name))
|
|
219
|
+
attr.skip();
|
|
220
|
+
},
|
|
221
|
+
JSXElement(child) {
|
|
222
|
+
const opening = child.get("openingElement");
|
|
223
|
+
if (hasInteraction(opening)) {
|
|
224
|
+
child.skip();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
add(icons, iconsInElement(opening, explicitIcon), 8);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
if (!icons.length && (namedIconProps.includes(prop) || explicitIcon))
|
|
231
|
+
add(icons, iconName(icon, explicitIcon), 8);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function collectIcons(path11) {
|
|
237
|
+
const icons = [];
|
|
238
|
+
collectPresentationIcons(path11, icons);
|
|
239
|
+
const jsx = path11.parentPath;
|
|
240
|
+
if (jsx.isJSXElement())
|
|
241
|
+
jsx.traverse({
|
|
242
|
+
// JSX mentioned inside an event callback is not a rendered child icon.
|
|
243
|
+
JSXAttribute(attr) {
|
|
244
|
+
attr.skip();
|
|
245
|
+
},
|
|
246
|
+
JSXElement(child) {
|
|
247
|
+
const opening = child.get("openingElement");
|
|
248
|
+
if (hasInteraction(opening)) {
|
|
249
|
+
child.skip();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
add(icons, iconsInElement(opening), 8);
|
|
253
|
+
collectPresentationIcons(opening, icons);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
return icons;
|
|
257
|
+
}
|
|
258
|
+
function conditionsAt(path11) {
|
|
259
|
+
const conditions = [];
|
|
260
|
+
let child = path11;
|
|
261
|
+
for (let parent = child.parentPath; parent && !parent.isFunction(); child = parent, parent = parent.parentPath) {
|
|
262
|
+
let test;
|
|
263
|
+
let negated = false;
|
|
264
|
+
if (parent.isLogicalExpression() && parent.node.right === child.node) {
|
|
265
|
+
if (parent.node.operator === "&&") test = parent.node.left;
|
|
266
|
+
if (parent.node.operator === "||") {
|
|
267
|
+
test = parent.node.left;
|
|
268
|
+
negated = true;
|
|
269
|
+
}
|
|
270
|
+
} else if (parent.isConditionalExpression() && parent.node.test !== child.node) {
|
|
271
|
+
test = parent.node.test;
|
|
272
|
+
negated = parent.node.alternate === child.node;
|
|
273
|
+
} else if (parent.isIfStatement() && parent.node.test !== child.node) {
|
|
274
|
+
test = parent.node.test;
|
|
275
|
+
negated = parent.node.alternate === child.node;
|
|
276
|
+
}
|
|
277
|
+
const name = symbol(test);
|
|
278
|
+
if (name) add(conditions, negated ? name.startsWith("!") ? name.slice(1) : `!${name}` : name);
|
|
279
|
+
}
|
|
280
|
+
return conditions;
|
|
281
|
+
}
|
|
282
|
+
function collectHandlerEvidence(expression, evidence) {
|
|
283
|
+
const seen = /* @__PURE__ */ new Set();
|
|
284
|
+
let budget = 100;
|
|
285
|
+
const visit = (path11, depth) => {
|
|
286
|
+
if (depth > 4 || budget-- <= 0) return;
|
|
287
|
+
const fn = handlerFunction(path11);
|
|
288
|
+
if (!fn || seen.has(fn.node)) return;
|
|
289
|
+
seen.add(fn.node);
|
|
290
|
+
fn.traverse({
|
|
291
|
+
// Ignore uncalled helper definitions; inline callbacks remain source evidence.
|
|
292
|
+
Function(nested) {
|
|
293
|
+
if (!nested.parentPath.isCallExpression() && !nested.parentPath.isObjectProperty())
|
|
294
|
+
nested.skip();
|
|
295
|
+
},
|
|
296
|
+
CallExpression(call) {
|
|
297
|
+
if (budget-- <= 0) {
|
|
298
|
+
call.skip();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
add(evidence.calls, symbol(call.node.callee));
|
|
302
|
+
for (const arg of call.node.arguments) add(evidence.argumentBindings, symbol(arg));
|
|
303
|
+
visit(call.get("callee"), depth + 1);
|
|
304
|
+
if (!BabelTypes.isMemberExpression(call.node.callee) || call.node.callee.computed || !BabelTypes.isIdentifier(call.node.callee.property, { name: "alert" }))
|
|
305
|
+
return;
|
|
306
|
+
const callee = call.get("callee");
|
|
307
|
+
if (!callee.isMemberExpression()) return;
|
|
308
|
+
const origin = importIdentity(callee.get("object"));
|
|
309
|
+
if (origin?.module !== "react-native" || origin.imported !== "Alert") return;
|
|
310
|
+
const options = call.node.arguments[2];
|
|
311
|
+
const destructiveOption = BabelTypes.isArrayExpression(options) && options.elements.some(
|
|
312
|
+
(option) => BabelTypes.isObjectExpression(option) && option.properties.some(
|
|
313
|
+
(p) => BabelTypes.isObjectProperty(p) && !p.computed && symbol(p.key) === "style" && BabelTypes.isStringLiteral(p.value, { value: "destructive" })
|
|
314
|
+
)
|
|
315
|
+
);
|
|
316
|
+
if (destructiveOption)
|
|
317
|
+
evidence.nativeConfirmation = {
|
|
318
|
+
title: symbol(call.node.arguments[0]),
|
|
319
|
+
destructiveOption
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
visit(unwrap(expression), 0);
|
|
325
|
+
}
|
|
326
|
+
function extractControlEvidence(ast, source, file) {
|
|
327
|
+
const candidates = [];
|
|
328
|
+
const sourceHash = createHash("sha256").update(source).digest("hex");
|
|
329
|
+
traverse5(ast, {
|
|
330
|
+
JSXOpeningElement(path11) {
|
|
331
|
+
const node = path11.node;
|
|
332
|
+
if (attribute(node, "__appilotsControl")) return;
|
|
333
|
+
const value = propValue(path11, "onPress");
|
|
334
|
+
if (!value && !attribute(node, "onPress") || value?.isNullLiteral() || value?.isJSXEmptyExpression())
|
|
335
|
+
return;
|
|
336
|
+
const icons = collectIcons(path11);
|
|
337
|
+
if (!icons.length || node.end == null) return;
|
|
338
|
+
const evidence = {
|
|
339
|
+
version: 1,
|
|
340
|
+
siteId: createHash("sha256").update(`${file}:${sourceHash}:${node.start}`).digest("hex").slice(0, 20),
|
|
341
|
+
component: symbol(node.name) ?? "unknown",
|
|
342
|
+
icons,
|
|
343
|
+
...value && symbol(value.node) ? { handler: symbol(value.node) } : {},
|
|
344
|
+
calls: [],
|
|
345
|
+
argumentBindings: [],
|
|
346
|
+
conditions: conditionsAt(path11)
|
|
347
|
+
};
|
|
348
|
+
if (value) collectHandlerEvidence(value, evidence);
|
|
349
|
+
candidates.push({ evidence, sourceHash, offset: node.end - (node.selfClosing ? 2 : 1) });
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
return candidates;
|
|
353
|
+
}
|
|
354
|
+
function byCodeUnit(a, b) {
|
|
355
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
356
|
+
}
|
|
357
|
+
async function globSorted(patterns, options) {
|
|
358
|
+
const files = await fastGlob(patterns, options);
|
|
359
|
+
return files.sort(byCodeUnit);
|
|
360
|
+
}
|
|
17
361
|
var DEFAULT_PARSER_PLUGINS = [
|
|
18
362
|
"jsx",
|
|
19
363
|
"typescript",
|
|
@@ -128,7 +472,7 @@ function classifyJsxComponent(name, element) {
|
|
|
128
472
|
}
|
|
129
473
|
function collectFunctions(ast) {
|
|
130
474
|
const handlers = /* @__PURE__ */ new Map();
|
|
131
|
-
|
|
475
|
+
traverse5(ast, {
|
|
132
476
|
FunctionDeclaration: (nodePath) => {
|
|
133
477
|
if (nodePath.node.id?.name) handlers.set(nodePath.node.id.name, nodePath.node);
|
|
134
478
|
},
|
|
@@ -231,7 +575,7 @@ function analyzeFunctionBehavior(name, fn, handlers, seen = /* @__PURE__ */ new
|
|
|
231
575
|
}
|
|
232
576
|
};
|
|
233
577
|
if (fn.body) {
|
|
234
|
-
|
|
578
|
+
traverse5(fn.body, {
|
|
235
579
|
noScope: true,
|
|
236
580
|
enter: (nodePath) => inspectNode(nodePath.node)
|
|
237
581
|
});
|
|
@@ -259,7 +603,7 @@ function setterToStateName(setterName) {
|
|
|
259
603
|
}
|
|
260
604
|
function extractNavigationCalls(ast) {
|
|
261
605
|
const calls = [];
|
|
262
|
-
|
|
606
|
+
traverse5(ast, {
|
|
263
607
|
noScope: !BabelTypes.isFile(ast),
|
|
264
608
|
CallExpression: (nodePath) => {
|
|
265
609
|
const node = nodePath.node;
|
|
@@ -398,14 +742,26 @@ var ScreenAnalyzer = class {
|
|
|
398
742
|
strictScreens;
|
|
399
743
|
/** Glob patterns that identify screen files in strict mode */
|
|
400
744
|
screenPatterns;
|
|
745
|
+
/**
|
|
746
|
+
* Arquivos que uma ROTA monta (`component={…}` resolvido pelo
|
|
747
|
+
* `NavigationAnalyzer`). Passam pelo filtro estrito sem depender de
|
|
748
|
+
* convenção de nome, porque um componente que uma rota monta É uma tela por
|
|
749
|
+
* definição — não é heurística, é o que o app declarou.
|
|
750
|
+
*
|
|
751
|
+
* É isto que resolve o caso `rocketchat`, cujas telas se chamam `*View.tsx`
|
|
752
|
+
* em `app/views/`, e o `coopcycle`, que não tem diretório `screens/` nenhum.
|
|
753
|
+
*/
|
|
754
|
+
routeTargetFiles;
|
|
401
755
|
/** §D: Count of screens filtered out in strict mode (available after analyze()) */
|
|
402
756
|
screensFilteredOut = 0;
|
|
757
|
+
controlEvidenceFiles = {};
|
|
403
758
|
constructor(config, options) {
|
|
404
759
|
this.config = config;
|
|
760
|
+
this.routeTargetFiles = options?.routeTargetFiles ?? /* @__PURE__ */ new Set();
|
|
405
761
|
this.strictScreens = options?.strictScreens ?? false;
|
|
406
762
|
this.screenPatterns = options?.screenPatterns ?? [
|
|
407
|
-
"**/*Screen.{ts,tsx}",
|
|
408
|
-
"**/screens/**/*.{ts,tsx}"
|
|
763
|
+
"**/*Screen.{ts,tsx,js,jsx}",
|
|
764
|
+
"**/screens/**/*.{ts,tsx,js,jsx}"
|
|
409
765
|
];
|
|
410
766
|
}
|
|
411
767
|
/** Analyze all screens in the project */
|
|
@@ -417,7 +773,7 @@ var ScreenAnalyzer = class {
|
|
|
417
773
|
console.log(`[ScreenAnalyzer] Strict mode ON \u2014 screen patterns:`, this.screenPatterns);
|
|
418
774
|
}
|
|
419
775
|
}
|
|
420
|
-
const files = await
|
|
776
|
+
const files = await globSorted(include, {
|
|
421
777
|
cwd: this.config.rootDir,
|
|
422
778
|
ignore: exclude
|
|
423
779
|
});
|
|
@@ -426,23 +782,24 @@ var ScreenAnalyzer = class {
|
|
|
426
782
|
}
|
|
427
783
|
let screenPatternFiles = null;
|
|
428
784
|
if (this.strictScreens) {
|
|
429
|
-
const matched = await
|
|
785
|
+
const matched = await globSorted(this.screenPatterns, {
|
|
430
786
|
cwd: this.config.rootDir,
|
|
431
787
|
ignore: exclude
|
|
432
788
|
});
|
|
433
|
-
screenPatternFiles = new Set(matched.map((f) =>
|
|
789
|
+
screenPatternFiles = new Set(matched.map((f) => path2__default.resolve(this.config.rootDir, f)));
|
|
434
790
|
}
|
|
435
791
|
const screens = [];
|
|
436
792
|
this.screensFilteredOut = 0;
|
|
437
793
|
for (const file of files) {
|
|
438
|
-
const filePath =
|
|
794
|
+
const filePath = path2__default.resolve(this.config.rootDir, file);
|
|
439
795
|
try {
|
|
440
796
|
const descriptor = await this.analyzeFile(filePath);
|
|
441
797
|
if (!descriptor) continue;
|
|
442
798
|
if (this.strictScreens) {
|
|
443
799
|
const hasRegisterScreen = descriptor.__hasRegisterScreen === true;
|
|
444
800
|
const matchesPattern = screenPatternFiles?.has(filePath) ?? false;
|
|
445
|
-
|
|
801
|
+
const isRouteTarget = this.routeTargetFiles.has(filePath);
|
|
802
|
+
if (!hasRegisterScreen && !matchesPattern && !isRouteTarget) {
|
|
446
803
|
this.screensFilteredOut++;
|
|
447
804
|
if (this.verbose) {
|
|
448
805
|
console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file}`);
|
|
@@ -498,6 +855,11 @@ var ScreenAnalyzer = class {
|
|
|
498
855
|
title: registerScreenMeta?.title,
|
|
499
856
|
description: registerScreenMeta?.description,
|
|
500
857
|
components,
|
|
858
|
+
controlCandidates: extractControlEvidence(
|
|
859
|
+
ast,
|
|
860
|
+
source,
|
|
861
|
+
path2__default.relative(this.config.rootDir, filePath)
|
|
862
|
+
),
|
|
501
863
|
forms,
|
|
502
864
|
actions,
|
|
503
865
|
navigationTargets,
|
|
@@ -508,6 +870,8 @@ var ScreenAnalyzer = class {
|
|
|
508
870
|
...permissionsFromJsDoc.isPii ? { isPii: true } : {}
|
|
509
871
|
} : {}
|
|
510
872
|
};
|
|
873
|
+
if (descriptor.controlCandidates?.length)
|
|
874
|
+
this.controlEvidenceFiles[path2__default.relative(this.config.rootDir, filePath).split(path2__default.sep).join("/")] = descriptor.controlCandidates;
|
|
511
875
|
descriptor.__hasRegisterScreen = hasRegisterScreenCall;
|
|
512
876
|
return descriptor;
|
|
513
877
|
}
|
|
@@ -517,7 +881,7 @@ var ScreenAnalyzer = class {
|
|
|
517
881
|
*/
|
|
518
882
|
detectRegisterScreenCall(ast) {
|
|
519
883
|
let found = false;
|
|
520
|
-
|
|
884
|
+
traverse5(ast, {
|
|
521
885
|
CallExpression: (nodePath) => {
|
|
522
886
|
if (found) return;
|
|
523
887
|
const callee = nodePath.node.callee;
|
|
@@ -534,7 +898,7 @@ var ScreenAnalyzer = class {
|
|
|
534
898
|
*/
|
|
535
899
|
extractRegisterScreenMetadata(ast) {
|
|
536
900
|
let metadata = null;
|
|
537
|
-
|
|
901
|
+
traverse5(ast, {
|
|
538
902
|
CallExpression: (nodePath) => {
|
|
539
903
|
const callee = nodePath.node.callee;
|
|
540
904
|
if (BabelTypes.isIdentifier(callee) && callee.name === "registerScreen" || BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.property) && callee.property.name === "registerScreen") {
|
|
@@ -650,6 +1014,8 @@ var ScreenAnalyzer = class {
|
|
|
650
1014
|
action.riskLevel = value.value;
|
|
651
1015
|
} else if (key === "nativeConfirmationExpected" && BabelTypes.isBooleanLiteral(value)) {
|
|
652
1016
|
action.nativeConfirmationExpected = value.value;
|
|
1017
|
+
} else if (key === "asyncBudgetMs" && BabelTypes.isNumericLiteral(value) && Number.isInteger(value.value) && value.value > 0) {
|
|
1018
|
+
action.asyncBudgetMs = value.value;
|
|
653
1019
|
} else if (key === "appilotsInferred" && BabelTypes.isObjectExpression(value)) {
|
|
654
1020
|
action.appilotsInferred = this.parseAppilotsInferredObject(value);
|
|
655
1021
|
}
|
|
@@ -829,7 +1195,7 @@ var ScreenAnalyzer = class {
|
|
|
829
1195
|
*/
|
|
830
1196
|
extractDefaultComponentName(ast) {
|
|
831
1197
|
let componentName = "";
|
|
832
|
-
|
|
1198
|
+
traverse5(ast, {
|
|
833
1199
|
ExportDefaultDeclaration: (nodePath) => {
|
|
834
1200
|
const declaration = nodePath.node.declaration;
|
|
835
1201
|
if (BabelTypes.isFunctionDeclaration(declaration) && declaration.id?.name) {
|
|
@@ -854,7 +1220,7 @@ var ScreenAnalyzer = class {
|
|
|
854
1220
|
*/
|
|
855
1221
|
extractNavigationTargets(ast) {
|
|
856
1222
|
const targets = /* @__PURE__ */ new Set();
|
|
857
|
-
|
|
1223
|
+
traverse5(ast, {
|
|
858
1224
|
CallExpression: (nodePath) => {
|
|
859
1225
|
const callee = nodePath.node.callee;
|
|
860
1226
|
if (BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.object) && callee.object.name === "navigation" && BabelTypes.isIdentifier(callee.property) && callee.property.name === "navigate") {
|
|
@@ -873,7 +1239,7 @@ var ScreenAnalyzer = class {
|
|
|
873
1239
|
extractForms(ast) {
|
|
874
1240
|
const forms = [];
|
|
875
1241
|
const fields = /* @__PURE__ */ new Map();
|
|
876
|
-
|
|
1242
|
+
traverse5(ast, {
|
|
877
1243
|
JSXOpeningElement: (nodePath) => {
|
|
878
1244
|
const element = nodePath.node;
|
|
879
1245
|
if (BabelTypes.isJSXIdentifier(element.name)) {
|
|
@@ -994,7 +1360,7 @@ var ScreenAnalyzer = class {
|
|
|
994
1360
|
extractComponents(ast) {
|
|
995
1361
|
const components = [];
|
|
996
1362
|
const seen = /* @__PURE__ */ new Set();
|
|
997
|
-
|
|
1363
|
+
traverse5(ast, {
|
|
998
1364
|
JSXOpeningElement: (nodePath) => {
|
|
999
1365
|
const element = nodePath.node;
|
|
1000
1366
|
if (BabelTypes.isJSXIdentifier(element.name)) {
|
|
@@ -1058,7 +1424,7 @@ var ScreenAnalyzer = class {
|
|
|
1058
1424
|
const actionLabels = new Map(
|
|
1059
1425
|
actions.filter((a) => a.label).map((a) => [this.normalizeLabel(a.label), a])
|
|
1060
1426
|
);
|
|
1061
|
-
|
|
1427
|
+
traverse5(ast, {
|
|
1062
1428
|
JSXOpeningElement: (nodePath) => {
|
|
1063
1429
|
const element = nodePath.node;
|
|
1064
1430
|
if (BabelTypes.isJSXIdentifier(element.name)) {
|
|
@@ -1127,7 +1493,7 @@ var ScreenAnalyzer = class {
|
|
|
1127
1493
|
}
|
|
1128
1494
|
collectButtonHandlersByLabel(ast) {
|
|
1129
1495
|
const out = /* @__PURE__ */ new Map();
|
|
1130
|
-
|
|
1496
|
+
traverse5(ast, {
|
|
1131
1497
|
JSXOpeningElement: (nodePath) => {
|
|
1132
1498
|
const element = nodePath.node;
|
|
1133
1499
|
if (!BabelTypes.isJSXIdentifier(element.name)) return;
|
|
@@ -1209,7 +1575,7 @@ var ScreenAnalyzer = class {
|
|
|
1209
1575
|
}
|
|
1210
1576
|
};
|
|
1211
1577
|
if (fn.body) {
|
|
1212
|
-
|
|
1578
|
+
traverse5(
|
|
1213
1579
|
fn.body,
|
|
1214
1580
|
{
|
|
1215
1581
|
noScope: true,
|
|
@@ -1406,7 +1772,7 @@ var ScreenAnalyzer = class {
|
|
|
1406
1772
|
extractCollections(ast) {
|
|
1407
1773
|
const renderItemFns = this.collectRenderItemFunctions(ast);
|
|
1408
1774
|
const collections = [];
|
|
1409
|
-
|
|
1775
|
+
traverse5(ast, {
|
|
1410
1776
|
JSXOpeningElement: (nodePath) => {
|
|
1411
1777
|
const element = nodePath.node;
|
|
1412
1778
|
if (!BabelTypes.isJSXIdentifier(element.name)) return;
|
|
@@ -1443,7 +1809,7 @@ var ScreenAnalyzer = class {
|
|
|
1443
1809
|
}
|
|
1444
1810
|
collectRenderItemFunctions(ast) {
|
|
1445
1811
|
const out = /* @__PURE__ */ new Map();
|
|
1446
|
-
|
|
1812
|
+
traverse5(ast, {
|
|
1447
1813
|
VariableDeclarator: (nodePath) => {
|
|
1448
1814
|
if (!BabelTypes.isIdentifier(nodePath.node.id)) return;
|
|
1449
1815
|
const init = nodePath.node.init;
|
|
@@ -1486,7 +1852,7 @@ var ScreenAnalyzer = class {
|
|
|
1486
1852
|
}
|
|
1487
1853
|
extractRowAction(fn) {
|
|
1488
1854
|
let action;
|
|
1489
|
-
|
|
1855
|
+
traverse5(
|
|
1490
1856
|
fn.body,
|
|
1491
1857
|
{
|
|
1492
1858
|
noScope: true,
|
|
@@ -1539,7 +1905,7 @@ var ScreenAnalyzer = class {
|
|
|
1539
1905
|
} else if (BabelTypes.isIdentifier(firstParam)) {
|
|
1540
1906
|
itemNames.add(firstParam.name);
|
|
1541
1907
|
}
|
|
1542
|
-
|
|
1908
|
+
traverse5(
|
|
1543
1909
|
fn.body,
|
|
1544
1910
|
{
|
|
1545
1911
|
noScope: true,
|
|
@@ -1581,7 +1947,7 @@ var ScreenAnalyzer = class {
|
|
|
1581
1947
|
inferSearchField(ast, dataSource) {
|
|
1582
1948
|
if (!dataSource) return void 0;
|
|
1583
1949
|
let queryBinding;
|
|
1584
|
-
|
|
1950
|
+
traverse5(ast, {
|
|
1585
1951
|
CallExpression: (nodePath) => {
|
|
1586
1952
|
const node = nodePath.node;
|
|
1587
1953
|
if (!BabelTypes.isMemberExpression(node.callee)) return;
|
|
@@ -1592,7 +1958,7 @@ var ScreenAnalyzer = class {
|
|
|
1592
1958
|
const fn = node.arguments[0];
|
|
1593
1959
|
if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn))
|
|
1594
1960
|
return;
|
|
1595
|
-
|
|
1961
|
+
traverse5(
|
|
1596
1962
|
fn.body,
|
|
1597
1963
|
{
|
|
1598
1964
|
noScope: true,
|
|
@@ -1629,43 +1995,871 @@ var ScreenAnalyzer = class {
|
|
|
1629
1995
|
* E.g., /src/screens/ItemListScreen.tsx -> ItemListScreen
|
|
1630
1996
|
*/
|
|
1631
1997
|
extractScreenName(filePath) {
|
|
1632
|
-
const basename2 =
|
|
1998
|
+
const basename2 = path2__default.basename(filePath);
|
|
1633
1999
|
return basename2.replace(/\.(tsx?|jsx?)$/, "");
|
|
1634
2000
|
}
|
|
1635
2001
|
};
|
|
2002
|
+
var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
|
|
2003
|
+
var MAX_HOPS2 = 8;
|
|
2004
|
+
var ModuleGraph = class {
|
|
2005
|
+
asts = /* @__PURE__ */ new Map();
|
|
2006
|
+
resolved = /* @__PURE__ */ new Map();
|
|
2007
|
+
/** `@src/*` → `<root>/src/*`, lido do tsconfig do app. */
|
|
2008
|
+
aliases;
|
|
2009
|
+
/** `uniswap` → `<repo>/packages/uniswap`, lido do workspace do monorepo. */
|
|
2010
|
+
workspacePackages;
|
|
2011
|
+
constructor(rootDir) {
|
|
2012
|
+
this.aliases = rootDir ? readTsconfigAliases(rootDir) : [];
|
|
2013
|
+
this.workspacePackages = rootDir ? readWorkspacePackages(rootDir) : [];
|
|
2014
|
+
}
|
|
2015
|
+
/** AST de um arquivo, memoizada. `null` quando não parseia. */
|
|
2016
|
+
parse(file) {
|
|
2017
|
+
const cached = this.asts.get(file);
|
|
2018
|
+
if (cached !== void 0) return cached;
|
|
2019
|
+
let ast = null;
|
|
2020
|
+
try {
|
|
2021
|
+
ast = parser.parse(readFileSync(file, "utf-8"), {
|
|
2022
|
+
sourceType: "module",
|
|
2023
|
+
plugins: ["jsx", "typescript"]
|
|
2024
|
+
});
|
|
2025
|
+
} catch {
|
|
2026
|
+
ast = null;
|
|
2027
|
+
}
|
|
2028
|
+
this.asts.set(file, ast);
|
|
2029
|
+
return ast;
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* `./account/Home` a partir de `src/navigation/index.tsx` → caminho absoluto.
|
|
2033
|
+
* Só resolve caminho relativo: import de pacote (`@react-navigation/native`)
|
|
2034
|
+
* é de terceiro e não tem tela nossa dentro.
|
|
2035
|
+
*/
|
|
2036
|
+
resolve(fromFile, spec) {
|
|
2037
|
+
const key = `${fromFile} ${spec}`;
|
|
2038
|
+
const cached = this.resolved.get(key);
|
|
2039
|
+
if (cached !== void 0) return cached;
|
|
2040
|
+
let base = null;
|
|
2041
|
+
if (spec.startsWith(".")) {
|
|
2042
|
+
base = path2__default.resolve(path2__default.dirname(fromFile), spec);
|
|
2043
|
+
} else {
|
|
2044
|
+
for (const { prefix, target } of this.aliases) {
|
|
2045
|
+
if (spec === prefix || spec.startsWith(prefix + "/")) {
|
|
2046
|
+
base = path2__default.join(target, spec.slice(prefix.length));
|
|
2047
|
+
break;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
if (!base) {
|
|
2051
|
+
for (const pkg of this.workspacePackages) {
|
|
2052
|
+
if (spec === pkg.name || spec.startsWith(pkg.name + "/")) {
|
|
2053
|
+
base = path2__default.join(pkg.dir, spec.slice(pkg.name.length));
|
|
2054
|
+
break;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
if (!base) {
|
|
2060
|
+
this.resolved.set(key, null);
|
|
2061
|
+
return null;
|
|
2062
|
+
}
|
|
2063
|
+
const candidates = [
|
|
2064
|
+
base,
|
|
2065
|
+
...EXTENSIONS.map((e) => base + e),
|
|
2066
|
+
...EXTENSIONS.map((e) => path2__default.join(base, "index" + e))
|
|
2067
|
+
];
|
|
2068
|
+
let found = null;
|
|
2069
|
+
for (const c of candidates) {
|
|
2070
|
+
try {
|
|
2071
|
+
if (existsSync(c) && statSync(c).isFile()) {
|
|
2072
|
+
found = c;
|
|
2073
|
+
break;
|
|
2074
|
+
}
|
|
2075
|
+
} catch {
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
this.resolved.set(key, found);
|
|
2079
|
+
return found;
|
|
2080
|
+
}
|
|
2081
|
+
/** `import`s do arquivo, por nome local. */
|
|
2082
|
+
imports(file) {
|
|
2083
|
+
const out = /* @__PURE__ */ new Map();
|
|
2084
|
+
const ast = this.parse(file);
|
|
2085
|
+
if (!ast) return out;
|
|
2086
|
+
for (const stmt of ast.program.body) {
|
|
2087
|
+
if (!BabelTypes.isImportDeclaration(stmt)) continue;
|
|
2088
|
+
const source = stmt.source.value;
|
|
2089
|
+
for (const spec of stmt.specifiers) {
|
|
2090
|
+
if (BabelTypes.isImportDefaultSpecifier(spec)) {
|
|
2091
|
+
out.set(spec.local.name, { source, imported: "default" });
|
|
2092
|
+
} else if (BabelTypes.isImportNamespaceSpecifier(spec)) {
|
|
2093
|
+
out.set(spec.local.name, { source, imported: "*" });
|
|
2094
|
+
} else if (BabelTypes.isImportSpecifier(spec)) {
|
|
2095
|
+
const imported = BabelTypes.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
|
|
2096
|
+
out.set(spec.local.name, { source, imported });
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
return out;
|
|
2101
|
+
}
|
|
2102
|
+
/** `const X = <init>` no topo do arquivo, incluindo `export const`. */
|
|
2103
|
+
topLevelInit(file, name) {
|
|
2104
|
+
const ast = this.parse(file);
|
|
2105
|
+
if (!ast) return null;
|
|
2106
|
+
for (const stmt of ast.program.body) {
|
|
2107
|
+
const decl = BabelTypes.isExportNamedDeclaration(stmt) ? stmt.declaration : stmt;
|
|
2108
|
+
if (!BabelTypes.isVariableDeclaration(decl)) continue;
|
|
2109
|
+
for (const d of decl.declarations) {
|
|
2110
|
+
if (BabelTypes.isIdentifier(d.id) && d.id.name === name && d.init) {
|
|
2111
|
+
return BabelTypes.isTSAsExpression(d.init) ? d.init.expression : d.init;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
return null;
|
|
2116
|
+
}
|
|
2117
|
+
/**
|
|
2118
|
+
* Onde `name` é DEFINIDO — segue import e reexport de barrel.
|
|
2119
|
+
* Devolve o arquivo e o nome sob o qual ele é definido lá.
|
|
2120
|
+
*/
|
|
2121
|
+
resolveBinding(file, name, hops = 0) {
|
|
2122
|
+
if (hops > MAX_HOPS2) return null;
|
|
2123
|
+
if (this.topLevelInit(file, name) !== null) return { file, name };
|
|
2124
|
+
const binding = this.imports(file).get(name);
|
|
2125
|
+
if (binding) {
|
|
2126
|
+
const target = this.resolve(file, binding.source);
|
|
2127
|
+
if (!target) return null;
|
|
2128
|
+
const next = binding.imported === "default" || binding.imported === "*" ? name : binding.imported;
|
|
2129
|
+
const deeper = this.resolveBinding(target, next, hops + 1);
|
|
2130
|
+
return deeper ?? { file: target, name: next };
|
|
2131
|
+
}
|
|
2132
|
+
const ast = this.parse(file);
|
|
2133
|
+
if (ast) {
|
|
2134
|
+
for (const stmt of ast.program.body) {
|
|
2135
|
+
if (!BabelTypes.isExportNamedDeclaration(stmt) || !stmt.source) continue;
|
|
2136
|
+
for (const spec of stmt.specifiers) {
|
|
2137
|
+
if (!BabelTypes.isExportSpecifier(spec)) continue;
|
|
2138
|
+
const exported = BabelTypes.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value;
|
|
2139
|
+
if (exported !== name) continue;
|
|
2140
|
+
const target = this.resolve(file, stmt.source.value);
|
|
2141
|
+
if (!target) return null;
|
|
2142
|
+
const local = spec.local.name;
|
|
2143
|
+
return this.resolveBinding(target, local, hops + 1) ?? { file: target, name: local };
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
if (ast) {
|
|
2148
|
+
for (const stmt of ast.program.body) {
|
|
2149
|
+
if (!BabelTypes.isExportAllDeclaration(stmt)) continue;
|
|
2150
|
+
const target = this.resolve(file, stmt.source.value);
|
|
2151
|
+
if (!target || target === file) continue;
|
|
2152
|
+
const deeper = this.resolveBinding(target, name, hops + 1);
|
|
2153
|
+
if (deeper) return deeper;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
if (ast) {
|
|
2157
|
+
for (const stmt of ast.program.body) {
|
|
2158
|
+
if (!BabelTypes.isExportDefaultDeclaration(stmt)) continue;
|
|
2159
|
+
if (BabelTypes.isIdentifier(stmt.declaration)) {
|
|
2160
|
+
const local = stmt.declaration.name;
|
|
2161
|
+
if (local === name) return null;
|
|
2162
|
+
return this.resolveBinding(file, local, hops + 1) ?? { file, name: local };
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
return null;
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* O valor string de uma expressão de nome de rota, ou `null`.
|
|
2170
|
+
*
|
|
2171
|
+
* Cobre `"Chat"`, `ROUTES.CHAT`, `ROUTES.ONBOARDING.SPLASH` e `SOME_CONST` —
|
|
2172
|
+
* seguindo import quando o objeto vem de outro arquivo. NÃO cobre template
|
|
2173
|
+
* com interpolação nem valor calculado, de propósito.
|
|
2174
|
+
*/
|
|
2175
|
+
stringConstant(file, node, hops = 0) {
|
|
2176
|
+
if (!node || hops > MAX_HOPS2) return null;
|
|
2177
|
+
if (BabelTypes.isStringLiteral(node)) return node.value;
|
|
2178
|
+
if (BabelTypes.isTemplateLiteral(node)) {
|
|
2179
|
+
return node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? null : null;
|
|
2180
|
+
}
|
|
2181
|
+
if (BabelTypes.isTSAsExpression(node)) return this.stringConstant(file, node.expression, hops + 1);
|
|
2182
|
+
const chain = memberChain(node);
|
|
2183
|
+
if (!chain) return null;
|
|
2184
|
+
const origin = this.resolveBinding(file, chain.root);
|
|
2185
|
+
if (!origin) return null;
|
|
2186
|
+
let current = this.topLevelInit(origin.file, origin.name);
|
|
2187
|
+
if (!current) return null;
|
|
2188
|
+
for (const key of chain.path) {
|
|
2189
|
+
if (!BabelTypes.isObjectExpression(current)) return null;
|
|
2190
|
+
const prop = objectProperty(current, key);
|
|
2191
|
+
if (!prop) return null;
|
|
2192
|
+
current = BabelTypes.isTSAsExpression(prop) ? prop.expression : prop;
|
|
2193
|
+
}
|
|
2194
|
+
return BabelTypes.isStringLiteral(current) ? current.value : null;
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* O ARQUIVO onde vive o componente de uma rota, ou `null`.
|
|
2198
|
+
*
|
|
2199
|
+
* Aceita as três formas que o corpus mostrou: identificador
|
|
2200
|
+
* (`component={Home}`), membro de barrel (`component={screens.AccountHome}`)
|
|
2201
|
+
* e componente embrulhado em HOC (`component={gestureHandlerRootHOC(Chat)}`,
|
|
2202
|
+
* que é como o `pocketpal` monta todas as telas do Drawer).
|
|
2203
|
+
*/
|
|
2204
|
+
componentFile(file, node, hops = 0) {
|
|
2205
|
+
if (!node || hops > MAX_HOPS2) return null;
|
|
2206
|
+
if (BabelTypes.isCallExpression(node)) {
|
|
2207
|
+
for (const arg of node.arguments) {
|
|
2208
|
+
if (BabelTypes.isIdentifier(arg) || BabelTypes.isMemberExpression(arg)) {
|
|
2209
|
+
const inner = this.componentFile(file, arg, hops + 1);
|
|
2210
|
+
if (inner) return inner;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
return null;
|
|
2214
|
+
}
|
|
2215
|
+
if (BabelTypes.isTSAsExpression(node)) return this.componentFile(file, node.expression, hops + 1);
|
|
2216
|
+
const chain = memberChain(node);
|
|
2217
|
+
if (!chain) return null;
|
|
2218
|
+
const origin = this.resolveBinding(file, chain.root);
|
|
2219
|
+
if (!origin) return null;
|
|
2220
|
+
if (chain.path.length === 0) return origin.file;
|
|
2221
|
+
const init = this.topLevelInit(origin.file, origin.name);
|
|
2222
|
+
if (init && BabelTypes.isObjectExpression(init)) {
|
|
2223
|
+
const prop = objectProperty(init, chain.path[0]);
|
|
2224
|
+
if (prop) return this.componentFile(origin.file, prop, hops + 1);
|
|
2225
|
+
}
|
|
2226
|
+
const viaExport = this.resolveBinding(origin.file, chain.path[0], hops + 1);
|
|
2227
|
+
return viaExport?.file ?? null;
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
2230
|
+
* Os arquivos DO PROJETO que este arquivo renderiza como JSX.
|
|
2231
|
+
*
|
|
2232
|
+
* `<ChatView …/>` em `ChatScreen.tsx` → `components/ChatView/ChatView.tsx`.
|
|
2233
|
+
* Import de pacote devolve `null` no `resolve` e fica de fora: componente de
|
|
2234
|
+
* terceiro não tem tela nossa dentro.
|
|
2235
|
+
*
|
|
2236
|
+
* Ignora `<X.Screen>` e `<X.Navigator>` de propósito — navegação é outro
|
|
2237
|
+
* extractor, e um navegador não é conteúdo de tela.
|
|
2238
|
+
*/
|
|
2239
|
+
renderedComponentFiles(file) {
|
|
2240
|
+
const ast = this.parse(file);
|
|
2241
|
+
if (!ast) return [];
|
|
2242
|
+
const names = /* @__PURE__ */ new Set();
|
|
2243
|
+
const visit = (node) => {
|
|
2244
|
+
if (!node || typeof node !== "object") return;
|
|
2245
|
+
if (BabelTypes.isJSXOpeningElement(node) && BabelTypes.isJSXIdentifier(node.name)) {
|
|
2246
|
+
const n = node.name.name;
|
|
2247
|
+
if (/^[A-Z]/.test(n)) names.add(n);
|
|
2248
|
+
}
|
|
2249
|
+
for (const key of Object.keys(node)) {
|
|
2250
|
+
const value = node[key];
|
|
2251
|
+
if (Array.isArray(value)) {
|
|
2252
|
+
for (const item of value) visit(item);
|
|
2253
|
+
} else if (value && typeof value === "object" && "type" in value) {
|
|
2254
|
+
visit(value);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
};
|
|
2258
|
+
visit(ast.program);
|
|
2259
|
+
const out = /* @__PURE__ */ new Set();
|
|
2260
|
+
for (const name of names) {
|
|
2261
|
+
const origin = this.resolveBinding(file, name);
|
|
2262
|
+
if (origin && origin.file !== file) out.add(origin.file);
|
|
2263
|
+
}
|
|
2264
|
+
return [...out];
|
|
2265
|
+
}
|
|
2266
|
+
};
|
|
2267
|
+
function memberChain(node) {
|
|
2268
|
+
const chain = [];
|
|
2269
|
+
let current = node;
|
|
2270
|
+
while (BabelTypes.isMemberExpression(current)) {
|
|
2271
|
+
if (current.computed) {
|
|
2272
|
+
if (!BabelTypes.isStringLiteral(current.property)) return null;
|
|
2273
|
+
chain.unshift(current.property.value);
|
|
2274
|
+
} else if (BabelTypes.isIdentifier(current.property)) {
|
|
2275
|
+
chain.unshift(current.property.name);
|
|
2276
|
+
} else {
|
|
2277
|
+
return null;
|
|
2278
|
+
}
|
|
2279
|
+
current = current.object;
|
|
2280
|
+
}
|
|
2281
|
+
return BabelTypes.isIdentifier(current) ? { root: current.name, path: chain } : null;
|
|
2282
|
+
}
|
|
2283
|
+
function objectProperty(obj, key) {
|
|
2284
|
+
for (const prop of obj.properties) {
|
|
2285
|
+
if (!BabelTypes.isObjectProperty(prop)) continue;
|
|
2286
|
+
const name = BabelTypes.isIdentifier(prop.key) ? prop.key.name : BabelTypes.isStringLiteral(prop.key) ? prop.key.value : null;
|
|
2287
|
+
if (name === key && BabelTypes.isExpression(prop.value)) return prop.value;
|
|
2288
|
+
}
|
|
2289
|
+
return null;
|
|
2290
|
+
}
|
|
2291
|
+
function readTsconfigAliases(rootDir) {
|
|
2292
|
+
const file = path2__default.join(rootDir, "tsconfig.json");
|
|
2293
|
+
if (!existsSync(file)) return [];
|
|
2294
|
+
let parsed;
|
|
2295
|
+
try {
|
|
2296
|
+
const raw = stripJsonComments(readFileSync(file, "utf-8")).replace(/,(\s*[}\]])/g, "$1");
|
|
2297
|
+
parsed = JSON.parse(raw);
|
|
2298
|
+
} catch {
|
|
2299
|
+
return [];
|
|
2300
|
+
}
|
|
2301
|
+
const paths = parsed.compilerOptions?.paths;
|
|
2302
|
+
if (!paths) return [];
|
|
2303
|
+
const baseUrl = path2__default.resolve(rootDir, parsed.compilerOptions?.baseUrl ?? ".");
|
|
2304
|
+
const out = [];
|
|
2305
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
2306
|
+
const first = targets[0];
|
|
2307
|
+
if (typeof first !== "string") continue;
|
|
2308
|
+
out.push({
|
|
2309
|
+
prefix: pattern.replace(/\/?\*$/, ""),
|
|
2310
|
+
target: path2__default.resolve(baseUrl, first.replace(/\/?\*$/, ""))
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
return out.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
2314
|
+
}
|
|
2315
|
+
function stripJsonComments(text) {
|
|
2316
|
+
let out = "";
|
|
2317
|
+
let inString = false;
|
|
2318
|
+
let escaped = false;
|
|
2319
|
+
for (let i = 0; i < text.length; i++) {
|
|
2320
|
+
const c = text[i];
|
|
2321
|
+
if (inString) {
|
|
2322
|
+
out += c;
|
|
2323
|
+
if (escaped) escaped = false;
|
|
2324
|
+
else if (c === "\\") escaped = true;
|
|
2325
|
+
else if (c === '"') inString = false;
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
if (c === '"') {
|
|
2329
|
+
inString = true;
|
|
2330
|
+
out += c;
|
|
2331
|
+
continue;
|
|
2332
|
+
}
|
|
2333
|
+
if (c === "/" && text[i + 1] === "/") {
|
|
2334
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
2335
|
+
out += "\n";
|
|
2336
|
+
continue;
|
|
2337
|
+
}
|
|
2338
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
2339
|
+
i += 2;
|
|
2340
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
2341
|
+
i++;
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
out += c;
|
|
2345
|
+
}
|
|
2346
|
+
return out;
|
|
2347
|
+
}
|
|
2348
|
+
function readWorkspacePackages(rootDir) {
|
|
2349
|
+
const start = path2__default.resolve(rootDir);
|
|
2350
|
+
let dir = start;
|
|
2351
|
+
for (let i = 0; i < 6; i++) {
|
|
2352
|
+
const globs = workspaceGlobs(dir);
|
|
2353
|
+
if (globs.length > 0) {
|
|
2354
|
+
const packages = expandWorkspaceGlobs(dir, globs);
|
|
2355
|
+
const containsApp = packages.some(
|
|
2356
|
+
(p) => start === p.dir || start.startsWith(p.dir + path2__default.sep)
|
|
2357
|
+
);
|
|
2358
|
+
return containsApp ? packages : [];
|
|
2359
|
+
}
|
|
2360
|
+
const parent = path2__default.dirname(dir);
|
|
2361
|
+
if (parent === dir) break;
|
|
2362
|
+
dir = parent;
|
|
2363
|
+
}
|
|
2364
|
+
return [];
|
|
2365
|
+
}
|
|
2366
|
+
function workspaceGlobs(dir) {
|
|
2367
|
+
const pnpm = path2__default.join(dir, "pnpm-workspace.yaml");
|
|
2368
|
+
if (existsSync(pnpm)) {
|
|
2369
|
+
try {
|
|
2370
|
+
const lines = readFileSync(pnpm, "utf-8").split(/\r?\n/);
|
|
2371
|
+
const out = [];
|
|
2372
|
+
let inPackages = false;
|
|
2373
|
+
for (const line of lines) {
|
|
2374
|
+
if (/^packages:/.test(line)) {
|
|
2375
|
+
inPackages = true;
|
|
2376
|
+
continue;
|
|
2377
|
+
}
|
|
2378
|
+
if (inPackages) {
|
|
2379
|
+
const m = /^\s*-\s*["']?([^"'#]+?)["']?\s*$/.exec(line);
|
|
2380
|
+
if (m) out.push(m[1].trim());
|
|
2381
|
+
else if (/^\S/.test(line)) break;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
if (out.length) return out;
|
|
2385
|
+
} catch {
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
const pkgPath = path2__default.join(dir, "package.json");
|
|
2389
|
+
if (!existsSync(pkgPath)) return [];
|
|
2390
|
+
try {
|
|
2391
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
2392
|
+
const ws = pkg.workspaces;
|
|
2393
|
+
if (Array.isArray(ws)) return ws;
|
|
2394
|
+
if (ws && Array.isArray(ws.packages)) return ws.packages;
|
|
2395
|
+
} catch {
|
|
2396
|
+
}
|
|
2397
|
+
return [];
|
|
2398
|
+
}
|
|
2399
|
+
function expandWorkspaceGlobs(root, globs) {
|
|
2400
|
+
const out = [];
|
|
2401
|
+
const add2 = (dir) => {
|
|
2402
|
+
const pkgPath = path2__default.join(dir, "package.json");
|
|
2403
|
+
if (!existsSync(pkgPath)) return;
|
|
2404
|
+
try {
|
|
2405
|
+
const name = JSON.parse(readFileSync(pkgPath, "utf-8")).name;
|
|
2406
|
+
if (name) out.push({ name, dir });
|
|
2407
|
+
} catch {
|
|
2408
|
+
}
|
|
2409
|
+
};
|
|
2410
|
+
for (const glob of globs) {
|
|
2411
|
+
if (glob.endsWith("/*")) {
|
|
2412
|
+
const parent = path2__default.join(root, glob.slice(0, -2));
|
|
2413
|
+
let entries = [];
|
|
2414
|
+
try {
|
|
2415
|
+
entries = readdirSync(parent);
|
|
2416
|
+
} catch {
|
|
2417
|
+
continue;
|
|
2418
|
+
}
|
|
2419
|
+
for (const entry of entries) {
|
|
2420
|
+
const dir = path2__default.join(parent, entry);
|
|
2421
|
+
try {
|
|
2422
|
+
if (statSync(dir).isDirectory()) add2(dir);
|
|
2423
|
+
} catch {
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
} else if (!glob.includes("*")) {
|
|
2427
|
+
add2(path2__default.join(root, glob));
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
return out.sort((a, b) => b.name.length - a.name.length);
|
|
2431
|
+
}
|
|
2432
|
+
var ROUTE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
2433
|
+
var PLATFORM_SUFFIXES = [".ios", ".android", ".native", ".web"];
|
|
2434
|
+
var ROUTE_DIR_CANDIDATES = ["app", "src/app"];
|
|
2435
|
+
function baseName(file) {
|
|
2436
|
+
let name = file;
|
|
2437
|
+
for (const ext of ROUTE_EXTENSIONS) {
|
|
2438
|
+
if (name.endsWith(ext)) {
|
|
2439
|
+
name = name.slice(0, -ext.length);
|
|
2440
|
+
break;
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
for (const suffix of PLATFORM_SUFFIXES) {
|
|
2444
|
+
if (name.endsWith(suffix)) return name.slice(0, -suffix.length);
|
|
2445
|
+
}
|
|
2446
|
+
return name;
|
|
2447
|
+
}
|
|
2448
|
+
function isRouteFile(file) {
|
|
2449
|
+
return ROUTE_EXTENSIONS.some((ext) => file.endsWith(ext));
|
|
2450
|
+
}
|
|
2451
|
+
function isSpecial(base) {
|
|
2452
|
+
if (base === "_layout") return true;
|
|
2453
|
+
if (base.startsWith("+")) return base !== "+not-found";
|
|
2454
|
+
return base.startsWith("_");
|
|
2455
|
+
}
|
|
2456
|
+
function layoutType(file) {
|
|
2457
|
+
try {
|
|
2458
|
+
const src = readFileSync(file, "utf-8");
|
|
2459
|
+
if (/<(?:Native)?Tabs\b/.test(src)) return "tab";
|
|
2460
|
+
if (/<Drawer\b/.test(src)) return "drawer";
|
|
2461
|
+
} catch {
|
|
2462
|
+
}
|
|
2463
|
+
return "stack";
|
|
2464
|
+
}
|
|
2465
|
+
function findLayout(dir) {
|
|
2466
|
+
for (const ext of ROUTE_EXTENSIONS) {
|
|
2467
|
+
const candidate = path2__default.join(dir, "_layout" + ext);
|
|
2468
|
+
if (existsSync(candidate)) return candidate;
|
|
2469
|
+
}
|
|
2470
|
+
return null;
|
|
2471
|
+
}
|
|
2472
|
+
function analyzeExpoRouter(appRoot) {
|
|
2473
|
+
let routeDir = null;
|
|
2474
|
+
for (const candidate of ROUTE_DIR_CANDIDATES) {
|
|
2475
|
+
const full = path2__default.join(appRoot, candidate);
|
|
2476
|
+
if (existsSync(full) && findLayout(full)) {
|
|
2477
|
+
routeDir = full;
|
|
2478
|
+
break;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
if (!routeDir) return null;
|
|
2482
|
+
const routes = [];
|
|
2483
|
+
const navigators = /* @__PURE__ */ new Map();
|
|
2484
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2485
|
+
const walk = (dir, segments, navigator) => {
|
|
2486
|
+
const layout = findLayout(dir);
|
|
2487
|
+
const current = layout ? segments.join("/") || "/" : navigator;
|
|
2488
|
+
if (layout && !navigators.has(current)) navigators.set(current, layoutType(layout));
|
|
2489
|
+
let entries;
|
|
2490
|
+
try {
|
|
2491
|
+
entries = readdirSync(dir);
|
|
2492
|
+
} catch {
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
for (const entry of entries.sort()) {
|
|
2496
|
+
const full = path2__default.join(dir, entry);
|
|
2497
|
+
let isDir = false;
|
|
2498
|
+
try {
|
|
2499
|
+
isDir = statSync(full).isDirectory();
|
|
2500
|
+
} catch {
|
|
2501
|
+
continue;
|
|
2502
|
+
}
|
|
2503
|
+
if (isDir) {
|
|
2504
|
+
if (entry === "node_modules" || entry.startsWith(".")) continue;
|
|
2505
|
+
walk(full, [...segments, entry], current);
|
|
2506
|
+
continue;
|
|
2507
|
+
}
|
|
2508
|
+
if (!isRouteFile(entry)) continue;
|
|
2509
|
+
const base = baseName(entry);
|
|
2510
|
+
if (isSpecial(base)) continue;
|
|
2511
|
+
const routeSegments = base === "index" ? segments : [...segments, base];
|
|
2512
|
+
const name = "/" + routeSegments.join("/");
|
|
2513
|
+
if (seen.has(name)) continue;
|
|
2514
|
+
seen.add(name);
|
|
2515
|
+
routes.push({
|
|
2516
|
+
name,
|
|
2517
|
+
componentFile: full,
|
|
2518
|
+
navigatorName: current,
|
|
2519
|
+
navigatorType: navigators.get(current) ?? "stack",
|
|
2520
|
+
params: routeSegments.filter((s) => s.startsWith("[") && s.endsWith("]")).map((s) => s.slice(1, -1).replace(/^\.\.\./, ""))
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
};
|
|
2524
|
+
walk(routeDir, [], "/");
|
|
2525
|
+
return {
|
|
2526
|
+
routeDir: path2__default.relative(appRoot, routeDir),
|
|
2527
|
+
routes,
|
|
2528
|
+
navigators: [...navigators.entries()].map(([name, type]) => ({ name, type }))
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// src/analyzers/NavigationAnalyzer.ts
|
|
2533
|
+
var NAVIGATOR_FACTORIES = {
|
|
2534
|
+
createStackNavigator: "stack",
|
|
2535
|
+
createNativeStackNavigator: "stack",
|
|
2536
|
+
createBottomTabNavigator: "tab",
|
|
2537
|
+
createTabNavigator: "tab",
|
|
2538
|
+
createMaterialTopTabNavigator: "tab",
|
|
2539
|
+
createMaterialBottomTabNavigator: "tab",
|
|
2540
|
+
createDrawerNavigator: "drawer"
|
|
2541
|
+
};
|
|
2542
|
+
var CUSTOM_FACTORY = /^create[A-Za-z0-9]*(Navigator|Stack|Tabs?|Drawer)$/;
|
|
2543
|
+
function unwrapStaticScreen(value) {
|
|
2544
|
+
if (BabelTypes.isCallExpression(value)) {
|
|
2545
|
+
const arg = value.arguments[0];
|
|
2546
|
+
return arg ? unwrapStaticScreen(arg) : null;
|
|
2547
|
+
}
|
|
2548
|
+
if (BabelTypes.isObjectExpression(value)) {
|
|
2549
|
+
for (const prop of value.properties) {
|
|
2550
|
+
if (!BabelTypes.isObjectProperty(prop)) continue;
|
|
2551
|
+
const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : null;
|
|
2552
|
+
if (key === "screen" && BabelTypes.isExpression(prop.value)) return prop.value;
|
|
2553
|
+
}
|
|
2554
|
+
return null;
|
|
2555
|
+
}
|
|
2556
|
+
return BabelTypes.isExpression(value) ? value : null;
|
|
2557
|
+
}
|
|
2558
|
+
function inferNavigatorType(factoryName) {
|
|
2559
|
+
if (/drawer/i.test(factoryName)) return "drawer";
|
|
2560
|
+
if (/tab/i.test(factoryName)) return "tab";
|
|
2561
|
+
return "stack";
|
|
2562
|
+
}
|
|
2563
|
+
var NAVIGATION_EVIDENCE = new RegExp(
|
|
2564
|
+
// `\.Navigator`/`\.Screen` entram porque o JSX pode estar num arquivo que
|
|
2565
|
+
// não menciona fábrica nenhuma: `comapeo` declara `RootStack` em
|
|
2566
|
+
// `Stack/RootStack.ts` e escreve todas as 120 rotas em `Stack/index.tsx`,
|
|
2567
|
+
// que não cita `createNativeStackNavigator` uma vez sequer. Sem isto a
|
|
2568
|
+
// junção entre arquivos nunca chega a ser tentada.
|
|
2569
|
+
// Sem `\\b` depois da palavra — `createNativeStackNavigatorWithAuth` não tem
|
|
2570
|
+
// fronteira ali, e exigi-la fecharia o portão para o arquivo que declara os
|
|
2571
|
+
// seis navegadores do `bluesky`.
|
|
2572
|
+
`create[A-Za-z0-9]*(?:Navigator|Stack|Tabs?|Drawer)|ParamList|\\.Navigator\\b|\\.Screen\\b`
|
|
2573
|
+
);
|
|
1636
2574
|
var NavigationAnalyzer = class {
|
|
1637
2575
|
config;
|
|
1638
2576
|
/** Extra glob patterns for navigation file discovery (added to defaults) */
|
|
1639
2577
|
navigationInclude;
|
|
1640
2578
|
/** Extra glob patterns to exclude from navigation analysis */
|
|
1641
2579
|
navigationExclude;
|
|
2580
|
+
/** Exposto para a composição reusar o cache de AST em vez de reparsear a árvore. */
|
|
2581
|
+
graph;
|
|
2582
|
+
/**
|
|
2583
|
+
* Rota → arquivo do componente que ela monta. É a única ligação entre o grafo
|
|
2584
|
+
* de navegação e a árvore de telas; sem ela as duas metades do documento
|
|
2585
|
+
* falam de coisas diferentes. Populada por `analyze()`.
|
|
2586
|
+
*/
|
|
2587
|
+
routeTargets = /* @__PURE__ */ new Map();
|
|
2588
|
+
/**
|
|
2589
|
+
* Arquivos que DECLARAM uma fábrica de navegador.
|
|
2590
|
+
*
|
|
2591
|
+
* Um navegador aninhado é montado como `component=` de uma rota — no
|
|
2592
|
+
* `apps/example-app`, `<Stack.Screen name="Main" component={MainTabNavigator} />`.
|
|
2593
|
+
* Promover esse arquivo a tela criaria seis "telas" sem uma única ação, que é
|
|
2594
|
+
* exatamente o falso positivo que o corpus existe para não repetir. Um
|
|
2595
|
+
* navegador é um contêiner de rotas; a tela está um nível abaixo.
|
|
2596
|
+
*
|
|
2597
|
+
* Entram aqui os dois lados: o arquivo que CHAMA a fábrica e o arquivo que
|
|
2598
|
+
* RENDERIZA `<X.Navigator>`. Não são o mesmo — `bluewallet` declara
|
|
2599
|
+
* `DetailViewStack` num arquivo e escreve o JSX em
|
|
2600
|
+
* `navigation/DetailViewScreensStack.tsx`, e checar só o primeiro deixava o
|
|
2601
|
+
* segundo entrar como tela.
|
|
2602
|
+
*/
|
|
2603
|
+
navigatorFiles = /* @__PURE__ */ new Set();
|
|
2604
|
+
/** Preenchido por `analyze()`. Vazio antes disso. */
|
|
2605
|
+
diagnostics = {
|
|
2606
|
+
filesScanned: 0,
|
|
2607
|
+
filesWithEvidence: 0,
|
|
2608
|
+
navigatorsDeclared: 0,
|
|
2609
|
+
navigatorsWithJsx: 0,
|
|
2610
|
+
routes: 0,
|
|
2611
|
+
routesLinkedToFile: 0
|
|
2612
|
+
};
|
|
1642
2613
|
constructor(config, options) {
|
|
1643
2614
|
this.config = config;
|
|
2615
|
+
this.graph = new ModuleGraph(config.rootDir);
|
|
1644
2616
|
this.navigationInclude = options?.navigationInclude ?? [];
|
|
1645
2617
|
this.navigationExclude = options?.navigationExclude ?? [];
|
|
1646
2618
|
}
|
|
1647
2619
|
/** Build the full navigation graph */
|
|
1648
2620
|
async analyze() {
|
|
1649
2621
|
const navigationFiles = await this.findNavigationFiles();
|
|
1650
|
-
const parsedNavigators = [];
|
|
1651
2622
|
const typeExports = [];
|
|
2623
|
+
this.routeTargets.clear();
|
|
2624
|
+
const declarations = /* @__PURE__ */ new Map();
|
|
2625
|
+
const evidenceFiles = [];
|
|
2626
|
+
this.navigatorFiles.clear();
|
|
1652
2627
|
for (const filePath of navigationFiles) {
|
|
1653
2628
|
try {
|
|
1654
2629
|
const content = await promises.readFile(filePath, "utf-8");
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
2630
|
+
if (!NAVIGATION_EVIDENCE.test(content)) continue;
|
|
2631
|
+
evidenceFiles.push(filePath);
|
|
2632
|
+
for (const decl of this.collectDeclarations(filePath)) {
|
|
2633
|
+
declarations.set(`${decl.file}#${decl.name}`, decl);
|
|
2634
|
+
if (decl.confident) this.navigatorFiles.add(decl.file);
|
|
2635
|
+
}
|
|
2636
|
+
typeExports.push(...this.parseParamTypes(content, filePath));
|
|
1659
2637
|
} catch (error) {
|
|
1660
2638
|
console.warn(`Failed to parse ${filePath}:`, error);
|
|
1661
2639
|
}
|
|
1662
2640
|
}
|
|
2641
|
+
const parsedNavigators = [];
|
|
2642
|
+
for (const filePath of evidenceFiles) {
|
|
2643
|
+
try {
|
|
2644
|
+
parsedNavigators.push(...this.parseNavigatorUsages(filePath, declarations));
|
|
2645
|
+
} catch (error) {
|
|
2646
|
+
console.warn(`Failed to parse navigators in ${filePath}:`, error);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
let detachedScreens = 0;
|
|
2650
|
+
for (const filePath of evidenceFiles) {
|
|
2651
|
+
try {
|
|
2652
|
+
for (const detached of this.parseDetachedScreens(filePath, declarations)) {
|
|
2653
|
+
const existing = parsedNavigators.find(
|
|
2654
|
+
(nav) => nav.name === detached.name && nav.type === detached.type
|
|
2655
|
+
);
|
|
2656
|
+
if (!existing) {
|
|
2657
|
+
parsedNavigators.push(detached);
|
|
2658
|
+
detachedScreens += detached.screens.length;
|
|
2659
|
+
continue;
|
|
2660
|
+
}
|
|
2661
|
+
const seen = new Set(existing.screens.map((screen) => screen.name));
|
|
2662
|
+
for (const screen of detached.screens) {
|
|
2663
|
+
if (seen.has(screen.name)) continue;
|
|
2664
|
+
seen.add(screen.name);
|
|
2665
|
+
existing.screens.push(screen);
|
|
2666
|
+
detachedScreens++;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
} catch (error) {
|
|
2670
|
+
console.warn(`Failed to parse detached screens in ${filePath}:`, error);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
const expo = analyzeExpoRouter(this.config.rootDir);
|
|
2674
|
+
if (expo) {
|
|
2675
|
+
console.log(
|
|
2676
|
+
`[NavigationAnalyzer] expo-router em "${expo.routeDir}": ${expo.routes.length} rotas, ${expo.navigators.length} layouts`
|
|
2677
|
+
);
|
|
2678
|
+
this.diagnostics.fileBasedRouter = {
|
|
2679
|
+
kind: "expo-router",
|
|
2680
|
+
routeDir: expo.routeDir,
|
|
2681
|
+
routes: expo.routes.length
|
|
2682
|
+
};
|
|
2683
|
+
const byNavigator = /* @__PURE__ */ new Map();
|
|
2684
|
+
for (const nav of expo.navigators) {
|
|
2685
|
+
byNavigator.set(nav.name, { name: nav.name, type: nav.type, screens: [] });
|
|
2686
|
+
}
|
|
2687
|
+
for (const route of expo.routes) {
|
|
2688
|
+
const navigator = byNavigator.get(route.navigatorName) ?? byNavigator.set(route.navigatorName, {
|
|
2689
|
+
name: route.navigatorName,
|
|
2690
|
+
type: route.navigatorType,
|
|
2691
|
+
screens: []
|
|
2692
|
+
}).get(route.navigatorName);
|
|
2693
|
+
navigator.screens.push({
|
|
2694
|
+
name: route.name,
|
|
2695
|
+
navigatorName: route.navigatorName,
|
|
2696
|
+
navigatorType: route.navigatorType,
|
|
2697
|
+
componentFile: route.componentFile,
|
|
2698
|
+
...route.params.length ? {
|
|
2699
|
+
params: route.params.map((name) => ({ name, type: "string", required: true }))
|
|
2700
|
+
} : {}
|
|
2701
|
+
});
|
|
2702
|
+
this.routeTargets.set(route.name, route.componentFile);
|
|
2703
|
+
}
|
|
2704
|
+
parsedNavigators.push(...byNavigator.values());
|
|
2705
|
+
}
|
|
2706
|
+
let staticOnly = 0;
|
|
2707
|
+
for (const decl of declarations.values()) {
|
|
2708
|
+
if (!decl.confident || !decl.staticScreens?.length) continue;
|
|
2709
|
+
const existing = parsedNavigators.find((n) => n.name === decl.name && n.type === decl.type);
|
|
2710
|
+
if (existing) {
|
|
2711
|
+
const seen = new Set(existing.screens.map((s) => s.name));
|
|
2712
|
+
for (const screen of decl.staticScreens) {
|
|
2713
|
+
if (!seen.has(screen.name)) existing.screens.push(screen);
|
|
2714
|
+
}
|
|
2715
|
+
continue;
|
|
2716
|
+
}
|
|
2717
|
+
parsedNavigators.push({ name: decl.name, type: decl.type, screens: decl.staticScreens });
|
|
2718
|
+
staticOnly++;
|
|
2719
|
+
}
|
|
2720
|
+
const confidentDeclarations = [...declarations.values()].filter((d) => d.confident).length;
|
|
2721
|
+
const uniqueRoutes = new Set(
|
|
2722
|
+
parsedNavigators.flatMap((nav) => nav.screens.map((screen) => screen.name))
|
|
2723
|
+
);
|
|
2724
|
+
this.diagnostics = {
|
|
2725
|
+
...this.diagnostics,
|
|
2726
|
+
filesScanned: navigationFiles.length,
|
|
2727
|
+
filesWithEvidence: evidenceFiles.length,
|
|
2728
|
+
navigatorsDeclared: confidentDeclarations,
|
|
2729
|
+
navigatorsWithJsx: parsedNavigators.length,
|
|
2730
|
+
routes: uniqueRoutes.size,
|
|
2731
|
+
// `routeTargets` é indexado por nome de rota, e uma rota pode ter sido
|
|
2732
|
+
// registrada por um navegador que não sobreviveu. O mínimo evita a
|
|
2733
|
+
// cobertura acima de 100% que a medição do corpus expôs no `coopcycle`.
|
|
2734
|
+
routesLinkedToFile: Math.min(this.routeTargets.size, uniqueRoutes.size)
|
|
2735
|
+
};
|
|
2736
|
+
console.log(
|
|
2737
|
+
`[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`
|
|
2738
|
+
);
|
|
1663
2739
|
this.attachParamsToNavigators(parsedNavigators, typeExports);
|
|
1664
2740
|
return this.buildNavigationGraph(parsedNavigators);
|
|
1665
2741
|
}
|
|
1666
|
-
/**
|
|
2742
|
+
/** Fase 1: as fábricas `create*Navigator()` atribuídas a um nome neste arquivo. */
|
|
2743
|
+
collectDeclarations(filePath) {
|
|
2744
|
+
const ast = this.graph.parse(filePath);
|
|
2745
|
+
if (!ast) return [];
|
|
2746
|
+
const out = /* @__PURE__ */ new Map();
|
|
2747
|
+
traverse5(ast, {
|
|
2748
|
+
CallExpression: (nodePath) => {
|
|
2749
|
+
const callee = nodePath.node.callee;
|
|
2750
|
+
if (!BabelTypes.isIdentifier(callee)) return;
|
|
2751
|
+
const known = NAVIGATOR_FACTORIES[callee.name] ?? (CUSTOM_FACTORY.test(callee.name) ? inferNavigatorType(callee.name) : null);
|
|
2752
|
+
const type = known ?? inferNavigatorType(callee.name);
|
|
2753
|
+
const confident = known !== null;
|
|
2754
|
+
let up = nodePath.parentPath;
|
|
2755
|
+
for (let i = 0; i < 4 && up; i++) {
|
|
2756
|
+
if (BabelTypes.isVariableDeclarator(up.node)) break;
|
|
2757
|
+
if (!BabelTypes.isMemberExpression(up.node) && !BabelTypes.isCallExpression(up.node)) {
|
|
2758
|
+
up = null;
|
|
2759
|
+
break;
|
|
2760
|
+
}
|
|
2761
|
+
up = up.parentPath;
|
|
2762
|
+
}
|
|
2763
|
+
const declarator = up?.node;
|
|
2764
|
+
if (!declarator || !BabelTypes.isVariableDeclarator(declarator) || !BabelTypes.isIdentifier(declarator.id)) {
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
const name = declarator.id.name;
|
|
2768
|
+
if (out.get(name)?.confident) return;
|
|
2769
|
+
const staticScreens = confident ? this.parseStaticScreens(filePath, nodePath.node.arguments[0], name, type) : [];
|
|
2770
|
+
out.set(name, {
|
|
2771
|
+
file: filePath,
|
|
2772
|
+
name,
|
|
2773
|
+
type,
|
|
2774
|
+
confident,
|
|
2775
|
+
...staticScreens.length ? { staticScreens } : {}
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
});
|
|
2779
|
+
return [...out.values()];
|
|
2780
|
+
}
|
|
2781
|
+
/**
|
|
2782
|
+
* As rotas de `createXNavigator({ screens: { … } })`.
|
|
2783
|
+
*
|
|
2784
|
+
* Quatro formas no corpus, todas em `rocketchat`:
|
|
2785
|
+
* `NewServerView` — shorthand, nome = componente
|
|
2786
|
+
* `LoginView: createNativeStackScreen({ screen })` — embrulho da própria lib
|
|
2787
|
+
* `SelectListView: SelectListViewScreen` — identificador direto
|
|
2788
|
+
* `ChatsStackNavigator: ChatsStack` — navegador aninhado
|
|
2789
|
+
*
|
|
2790
|
+
* `groups: { G: { screens: { … } } }` também entra: faz parte da API estática
|
|
2791
|
+
* e agrupar rotas não muda o que elas são.
|
|
2792
|
+
*/
|
|
2793
|
+
parseStaticScreens(filePath, config, navigatorName, navigatorType, depth = 0) {
|
|
2794
|
+
if (!config || !BabelTypes.isObjectExpression(config) || depth > 4) return [];
|
|
2795
|
+
const out = [];
|
|
2796
|
+
for (const prop of config.properties) {
|
|
2797
|
+
if (!BabelTypes.isObjectProperty(prop)) continue;
|
|
2798
|
+
const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : BabelTypes.isStringLiteral(prop.key) ? prop.key.value : null;
|
|
2799
|
+
if (!key) continue;
|
|
2800
|
+
if (key === "groups" && BabelTypes.isObjectExpression(prop.value)) {
|
|
2801
|
+
for (const group of prop.value.properties) {
|
|
2802
|
+
if (!BabelTypes.isObjectProperty(group) || !BabelTypes.isExpression(group.value)) continue;
|
|
2803
|
+
out.push(
|
|
2804
|
+
...this.parseStaticScreens(
|
|
2805
|
+
filePath,
|
|
2806
|
+
group.value,
|
|
2807
|
+
navigatorName,
|
|
2808
|
+
navigatorType,
|
|
2809
|
+
depth + 1
|
|
2810
|
+
)
|
|
2811
|
+
);
|
|
2812
|
+
}
|
|
2813
|
+
continue;
|
|
2814
|
+
}
|
|
2815
|
+
if (key !== "screens" || !BabelTypes.isObjectExpression(prop.value)) continue;
|
|
2816
|
+
for (const entry of prop.value.properties) {
|
|
2817
|
+
if (!BabelTypes.isObjectProperty(entry)) continue;
|
|
2818
|
+
const routeName = BabelTypes.isIdentifier(entry.key) ? entry.key.name : BabelTypes.isStringLiteral(entry.key) ? entry.key.value : null;
|
|
2819
|
+
if (!routeName) continue;
|
|
2820
|
+
const componentExpr = unwrapStaticScreen(entry.value);
|
|
2821
|
+
const componentFile = componentExpr ? this.graph.componentFile(filePath, componentExpr) : null;
|
|
2822
|
+
if (componentFile) this.routeTargets.set(routeName, componentFile);
|
|
2823
|
+
out.push({
|
|
2824
|
+
name: routeName,
|
|
2825
|
+
navigatorName,
|
|
2826
|
+
navigatorType,
|
|
2827
|
+
...componentFile ? { componentFile } : {}
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
return out;
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* Todo arquivo-fonte do projeto — não os que ficam num diretório com o nome
|
|
2835
|
+
* certo.
|
|
2836
|
+
*
|
|
2837
|
+
* POR QUE ISTO MUDOU. A lista anterior era `**\/navigation/**`,
|
|
2838
|
+
* `**\/navigator*` e `**\/routes*`, o que fazia da descoberta de navegação uma
|
|
2839
|
+
* convenção de caminho. Medido contra 20 apps React Native de terceiros
|
|
2840
|
+
* (`qa/eval-corpus`), o filtro errava por motivos que nada têm a ver com o
|
|
2841
|
+
* app não ter navegação:
|
|
2842
|
+
*
|
|
2843
|
+
* - `pocketpal` declara 4 navegadores em `App.tsx` e dentro de `src/screens/`;
|
|
2844
|
+
* - `comapeo` usa `src/frontend/Navigation/` — `N` maiúsculo, e o glob é
|
|
2845
|
+
* sensível a caixa;
|
|
2846
|
+
* - `abacus` usa `src/routes/index.tsx` — `routes` é o DIRETÓRIO, e o glob
|
|
2847
|
+
* pedia um arquivo chamado `routes*`;
|
|
2848
|
+
* - `discourse` declara em `js/Discourse.js` — o glob só aceitava `.ts`/`.tsx`;
|
|
2849
|
+
* - `rainbow` tem 11 arquivos com navegador e o glob alcançava 1.
|
|
2850
|
+
*
|
|
2851
|
+
* Varrer tudo não custa uma varredura nova: `ReactNativePlatformAnalyzer` já
|
|
2852
|
+
* globa e parseia a árvore inteira para `ComponentAnalyzer` e `FormAnalyzer`.
|
|
2853
|
+
* O que segura o custo aqui é o portão por evidência em `analyze()`, que só
|
|
2854
|
+
* parseia arquivo cujo texto menciona uma fábrica de navegador.
|
|
2855
|
+
*
|
|
2856
|
+
* Os globs antigos continuam na lista: se um projeto restringir `include`, o
|
|
2857
|
+
* que era encontrado antes continua sendo.
|
|
2858
|
+
*/
|
|
1667
2859
|
async findNavigationFiles() {
|
|
1668
2860
|
const patterns = [
|
|
2861
|
+
...this.config.include ?? ["**/*.{ts,tsx,js,jsx}"],
|
|
2862
|
+
// Piso de compatibilidade — nunca encontrar MENOS que a versão anterior.
|
|
1669
2863
|
"**/navigation/**/*.{ts,tsx}",
|
|
1670
2864
|
"**/navigator*.{ts,tsx}",
|
|
1671
2865
|
"**/routes*.{ts,tsx}",
|
|
@@ -1676,91 +2870,188 @@ var NavigationAnalyzer = class {
|
|
|
1676
2870
|
"**/node_modules/**",
|
|
1677
2871
|
"**/dist/**",
|
|
1678
2872
|
"**/build/**",
|
|
2873
|
+
// Um navegador declarado dentro de um teste é fixture, não a navegação
|
|
2874
|
+
// do app. Isto passou a importar quando a varredura deixou de ser por
|
|
2875
|
+
// caminho: em `expensify`, os únicos arquivos que o glob antigo
|
|
2876
|
+
// alcançava eram três de `tests/`.
|
|
2877
|
+
"**/__tests__/**",
|
|
2878
|
+
"**/test/**",
|
|
2879
|
+
"**/tests/**",
|
|
2880
|
+
"**/*.test.{ts,tsx,js,jsx}",
|
|
2881
|
+
"**/*.tests.{ts,tsx,js,jsx}",
|
|
2882
|
+
"**/*.spec.{ts,tsx,js,jsx}",
|
|
2883
|
+
// Sufixo `Test` sem ponto: `expensify` chama os fixtures dele de
|
|
2884
|
+
// `LegalNameStepTest.tsx`, e cada um monta um `<Stack.Screen>` próprio.
|
|
2885
|
+
// Auditando as 144 rotas lidas dele contra `SCREENS.ts`, 2 vinham daqui —
|
|
2886
|
+
// destinos que existem no teste e não no app, para os quais o agente
|
|
2887
|
+
// tentaria navegar.
|
|
2888
|
+
"**/*Test.{ts,tsx,js,jsx}",
|
|
2889
|
+
"**/*Tests.{ts,tsx,js,jsx}",
|
|
1679
2890
|
...this.config.exclude || [],
|
|
1680
2891
|
// §5: Add user-configured navigation excludes
|
|
1681
2892
|
...this.navigationExclude
|
|
1682
2893
|
];
|
|
1683
|
-
const files = await
|
|
2894
|
+
const files = await globSorted(patterns, {
|
|
1684
2895
|
cwd: this.config.rootDir,
|
|
1685
2896
|
ignore: excludePatterns
|
|
1686
2897
|
});
|
|
1687
|
-
return files.map((file) =>
|
|
2898
|
+
return files.map((file) => path2__default.join(this.config.rootDir, file));
|
|
1688
2899
|
}
|
|
1689
|
-
/**
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
traverse4(ast, {
|
|
1704
|
-
// Detect createStackNavigator / createTabNavigator / createDrawerNavigator calls
|
|
1705
|
-
CallExpression: (nodePath) => {
|
|
1706
|
-
const { node } = nodePath;
|
|
1707
|
-
const callee = node.callee;
|
|
1708
|
-
let navigatorType = null;
|
|
1709
|
-
if (BabelTypes.isIdentifier(callee) && callee.name === "createNativeStackNavigator") {
|
|
1710
|
-
navigatorType = "stack";
|
|
1711
|
-
} else if (BabelTypes.isIdentifier(callee) && callee.name === "createStackNavigator") {
|
|
1712
|
-
navigatorType = "stack";
|
|
1713
|
-
} else if (BabelTypes.isIdentifier(callee) && callee.name === "createBottomTabNavigator") {
|
|
1714
|
-
navigatorType = "tab";
|
|
1715
|
-
} else if (BabelTypes.isIdentifier(callee) && callee.name === "createTabNavigator") {
|
|
1716
|
-
navigatorType = "tab";
|
|
1717
|
-
} else if (BabelTypes.isIdentifier(callee) && callee.name === "createDrawerNavigator") {
|
|
1718
|
-
navigatorType = "drawer";
|
|
1719
|
-
}
|
|
1720
|
-
if (navigatorType) {
|
|
1721
|
-
const parent = nodePath.parent;
|
|
1722
|
-
if (BabelTypes.isVariableDeclarator(parent) && BabelTypes.isIdentifier(parent.id)) {
|
|
1723
|
-
const varName = parent.id.name;
|
|
1724
|
-
navigatorCalls.set(varName, {
|
|
1725
|
-
name: varName,
|
|
1726
|
-
type: navigatorType,
|
|
1727
|
-
screens: []
|
|
1728
|
-
});
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
},
|
|
1732
|
-
// Detect Stack.Navigator / Tab.Navigator JSX elements
|
|
1733
|
-
JSXElement: (nodePath) => {
|
|
1734
|
-
const { node } = nodePath;
|
|
1735
|
-
const openingElement = node.openingElement;
|
|
1736
|
-
if (BabelTypes.isJSXMemberExpression(openingElement.name) && BabelTypes.isJSXIdentifier(openingElement.name.object)) {
|
|
1737
|
-
const objectName = openingElement.name.object.name;
|
|
1738
|
-
const propertyName = BabelTypes.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
|
|
1739
|
-
if (propertyName === "Navigator") {
|
|
1740
|
-
const navigator = navigatorCalls.get(objectName);
|
|
1741
|
-
if (navigator) {
|
|
1742
|
-
const initialRouteAttr = openingElement.attributes.find(
|
|
1743
|
-
(attr) => BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
|
|
1744
|
-
);
|
|
1745
|
-
if (BabelTypes.isJSXAttribute(initialRouteAttr) && BabelTypes.isStringLiteral(initialRouteAttr.value)) {
|
|
1746
|
-
navigator.initialRouteName = initialRouteAttr.value.value;
|
|
1747
|
-
}
|
|
1748
|
-
const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
|
|
1749
|
-
screensByNavigator.set(objectName, screens);
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
2900
|
+
/**
|
|
2901
|
+
* Fase 2: o JSX `<X.Navigator>` deste arquivo, ligado à declaração de `X` —
|
|
2902
|
+
* que pode estar aqui ou em qualquer arquivo que este importe.
|
|
2903
|
+
*/
|
|
2904
|
+
parseNavigatorUsages(filePath, declarations) {
|
|
2905
|
+
const ast = this.graph.parse(filePath);
|
|
2906
|
+
if (!ast) return [];
|
|
2907
|
+
const found = /* @__PURE__ */ new Map();
|
|
2908
|
+
traverse5(ast, {
|
|
2909
|
+
JSXElement: (nodePath) => {
|
|
2910
|
+
const { node } = nodePath;
|
|
2911
|
+
const openingElement = node.openingElement;
|
|
2912
|
+
if (!BabelTypes.isJSXMemberExpression(openingElement.name) || !BabelTypes.isJSXIdentifier(openingElement.name.object)) {
|
|
2913
|
+
return;
|
|
1753
2914
|
}
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
2915
|
+
const objectName = openingElement.name.object.name;
|
|
2916
|
+
const propertyName = BabelTypes.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
|
|
2917
|
+
if (propertyName !== "Navigator") return;
|
|
2918
|
+
const decl = this.resolveNavigator(filePath, objectName, declarations);
|
|
2919
|
+
if (!decl) return;
|
|
2920
|
+
const navigator = found.get(objectName) ?? {
|
|
2921
|
+
name: objectName,
|
|
2922
|
+
type: decl.type,
|
|
2923
|
+
screens: []
|
|
2924
|
+
};
|
|
2925
|
+
const initialRouteAttr = openingElement.attributes.find(
|
|
2926
|
+
(attr) => BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
|
|
2927
|
+
);
|
|
2928
|
+
if (BabelTypes.isJSXAttribute(initialRouteAttr)) {
|
|
2929
|
+
const initial = this.attributeString(filePath, initialRouteAttr);
|
|
2930
|
+
if (initial) navigator.initialRouteName = initial;
|
|
2931
|
+
}
|
|
2932
|
+
this.navigatorFiles.add(filePath);
|
|
2933
|
+
this.navigatorFiles.add(decl.file);
|
|
2934
|
+
navigator.screens = this.extractScreensFromNavigator(filePath, node, objectName, decl.type);
|
|
2935
|
+
found.set(objectName, navigator);
|
|
2936
|
+
}
|
|
2937
|
+
});
|
|
2938
|
+
return [...found.values()];
|
|
2939
|
+
}
|
|
2940
|
+
/**
|
|
2941
|
+
* Rotas declaradas FORA de qualquer `<X.Navigator>`.
|
|
2942
|
+
*
|
|
2943
|
+
* O CASO. A fase 2 desce a partir do `<X.Navigator>` e lê as `<X.Screen>`
|
|
2944
|
+
* que estão DENTRO dele. Dois alvos do corpus não escrevem assim, e entre os
|
|
2945
|
+
* dois são 171 rotas invisíveis:
|
|
2946
|
+
*
|
|
2947
|
+
* `bluesky` — `function commonScreens(Stack: typeof Flat) { return (<>
|
|
2948
|
+
* <Stack.Screen name="NotFound" … /> … </>) }`, chamada de
|
|
2949
|
+
* dentro de seis navegadores diferentes. 70 rotas.
|
|
2950
|
+
* `comapeo` — `export const createAppScreens = ({intl}) => (<>
|
|
2951
|
+
* <RootStack.Group><RootStack.Screen … /></RootStack.Group></>)`,
|
|
2952
|
+
* num arquivo sem `<RootStack.Navigator>` nenhum. 101 rotas.
|
|
2953
|
+
*
|
|
2954
|
+
* A REGRA. Uma `<X.Screen name="…">` sem `<Y.Navigator>` ancestral é rota do
|
|
2955
|
+
* navegador ao qual `X` se resolve. Não há palpite em jogo: o nome da rota é
|
|
2956
|
+
* literal do fonte (ou constante que o resolvedor segue), e `X` precisa
|
|
2957
|
+
* chegar a uma declaração de navegador que já existe. O que não resolve não
|
|
2958
|
+
* vira nada.
|
|
2959
|
+
*
|
|
2960
|
+
* O ancestral é o que evita contar duas vezes — dentro do `<X.Navigator>` a
|
|
2961
|
+
* fase 2 já leu, e somar aqui duplicaria cada rota do corpus inteiro.
|
|
2962
|
+
*
|
|
2963
|
+
* DUAS FORMAS DE RESOLVER `X`, e a segunda é o que o `bluesky` exige. A
|
|
2964
|
+
* primeira é a de sempre (declaração local, ou `import` seguido até a
|
|
2965
|
+
* origem) e resolve o `comapeo`. A segunda lê a ANOTAÇÃO DE TIPO do
|
|
2966
|
+
* parâmetro: em `commonScreens(Stack: typeof Flat)`, quem diz que `Stack` é
|
|
2967
|
+
* o navegador `Flat` é o próprio app, no fonte.
|
|
2968
|
+
*
|
|
2969
|
+
* O QUE ISTO NÃO FAZ. As 70 do `bluesky` ficam atribuídas a `Flat` — que as
|
|
2970
|
+
* contém de fato (`{commonScreens(Flat, numUnread)}`) — e não aos outros
|
|
2971
|
+
* cinco stacks que também chamam a mesma função. Seguir os seis pontos de
|
|
2972
|
+
* chamada é análise interprocedural, e o ganho seria só de atribuição: o nome
|
|
2973
|
+
* da rota e o arquivo da tela, que é o que o agente usa, já saem certos.
|
|
2974
|
+
*/
|
|
2975
|
+
parseDetachedScreens(filePath, declarations) {
|
|
2976
|
+
const ast = this.graph.parse(filePath);
|
|
2977
|
+
if (!ast) return [];
|
|
2978
|
+
const byNavigator = /* @__PURE__ */ new Map();
|
|
2979
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
2980
|
+
traverse5(ast, {
|
|
2981
|
+
JSXElement: (nodePath) => {
|
|
2982
|
+
const name = nodePath.node.openingElement.name;
|
|
2983
|
+
if (!BabelTypes.isJSXMemberExpression(name) || !BabelTypes.isJSXIdentifier(name.object)) return;
|
|
2984
|
+
if (!BabelTypes.isJSXIdentifier(name.property) || name.property.name !== "Screen") return;
|
|
2985
|
+
const insideNavigator = nodePath.findParent((parent) => {
|
|
2986
|
+
if (!parent.isJSXElement()) return false;
|
|
2987
|
+
const parentName = parent.node.openingElement.name;
|
|
2988
|
+
return BabelTypes.isJSXMemberExpression(parentName) && BabelTypes.isJSXIdentifier(parentName.property) && parentName.property.name === "Navigator";
|
|
2989
|
+
});
|
|
2990
|
+
if (insideNavigator) return;
|
|
2991
|
+
const local = name.object.name;
|
|
2992
|
+
if (!resolved.has(local)) {
|
|
2993
|
+
resolved.set(
|
|
2994
|
+
local,
|
|
2995
|
+
this.resolveDetachedNavigator(filePath, local, nodePath, declarations)
|
|
2996
|
+
);
|
|
2997
|
+
}
|
|
2998
|
+
const decl = resolved.get(local);
|
|
2999
|
+
if (!decl) return;
|
|
3000
|
+
const screen = this.parseScreenElement(filePath, nodePath.node, decl.name, decl.type);
|
|
3001
|
+
if (!screen) return;
|
|
3002
|
+
const nav = byNavigator.get(decl.name) ?? {
|
|
3003
|
+
name: decl.name,
|
|
3004
|
+
type: decl.type,
|
|
3005
|
+
screens: []
|
|
3006
|
+
};
|
|
3007
|
+
if (!nav.screens.some((existing) => existing.name === screen.name))
|
|
3008
|
+
nav.screens.push(screen);
|
|
3009
|
+
byNavigator.set(decl.name, nav);
|
|
3010
|
+
}
|
|
3011
|
+
});
|
|
3012
|
+
return [...byNavigator.values()];
|
|
3013
|
+
}
|
|
3014
|
+
/**
|
|
3015
|
+
* De um `X` usado como `<X.Screen>` até a declaração do navegador.
|
|
3016
|
+
*
|
|
3017
|
+
* Além do caminho normal — declaração local ou `import` seguido até a origem
|
|
3018
|
+
* — aceita `X` como PARÂMETRO anotado com `typeof Y`. É o idioma do
|
|
3019
|
+
* `bluesky`, e a anotação é declaração do app: nada aqui é inferido do nome.
|
|
3020
|
+
*/
|
|
3021
|
+
resolveDetachedNavigator(filePath, localName, nodePath, declarations) {
|
|
3022
|
+
const direct = this.resolveNavigator(filePath, localName, declarations);
|
|
3023
|
+
if (direct) return direct;
|
|
3024
|
+
const binding = nodePath.scope.getBinding(localName);
|
|
3025
|
+
if (!binding || binding.kind !== "param") return null;
|
|
3026
|
+
const param = binding.path.node;
|
|
3027
|
+
if (!BabelTypes.isIdentifier(param) || !BabelTypes.isTSTypeAnnotation(param.typeAnnotation)) return null;
|
|
3028
|
+
const annotation = param.typeAnnotation.typeAnnotation;
|
|
3029
|
+
if (!BabelTypes.isTSTypeQuery(annotation) || !BabelTypes.isIdentifier(annotation.exprName)) return null;
|
|
3030
|
+
return this.resolveNavigator(filePath, annotation.exprName.name, declarations);
|
|
3031
|
+
}
|
|
3032
|
+
/**
|
|
3033
|
+
* De `<X.Navigator>` até a declaração de `X`.
|
|
3034
|
+
*
|
|
3035
|
+
* Primeiro no próprio arquivo; se não estiver, segue o `import` até onde `X`
|
|
3036
|
+
* é definido. Resolver pelo import é o que torna a junção entre arquivos
|
|
3037
|
+
* segura: dois `Stack` de arquivos diferentes nunca colidem, porque a chave
|
|
3038
|
+
* é o arquivo de DEFINIÇÃO.
|
|
3039
|
+
*/
|
|
3040
|
+
resolveNavigator(filePath, localName, declarations) {
|
|
3041
|
+
const local = declarations.get(`${filePath}#${localName}`);
|
|
3042
|
+
if (local) return local;
|
|
3043
|
+
const origin = this.graph.resolveBinding(filePath, localName);
|
|
3044
|
+
if (!origin) return null;
|
|
3045
|
+
return declarations.get(`${origin.file}#${origin.name}`) ?? null;
|
|
3046
|
+
}
|
|
3047
|
+
/** O valor string de um atributo JSX, resolvendo constante importada. */
|
|
3048
|
+
attributeString(filePath, attr) {
|
|
3049
|
+
const value = attr.value;
|
|
3050
|
+
if (BabelTypes.isStringLiteral(value)) return value.value;
|
|
3051
|
+
if (BabelTypes.isJSXExpressionContainer(value) && BabelTypes.isExpression(value.expression)) {
|
|
3052
|
+
return this.graph.stringConstant(filePath, value.expression);
|
|
1762
3053
|
}
|
|
1763
|
-
return
|
|
3054
|
+
return null;
|
|
1764
3055
|
}
|
|
1765
3056
|
/**
|
|
1766
3057
|
* Is this JSX element `<navigatorVarName.MEMBER …>`?
|
|
@@ -1855,22 +3146,48 @@ var NavigationAnalyzer = class {
|
|
|
1855
3146
|
return found;
|
|
1856
3147
|
}
|
|
1857
3148
|
/** Extract screens from a navigator JSX element */
|
|
1858
|
-
extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
|
|
3149
|
+
extractScreensFromNavigator(filePath, navigatorElement, navigatorVarName, navigatorType) {
|
|
1859
3150
|
const screens = [];
|
|
1860
3151
|
if (!navigatorElement.children) return screens;
|
|
1861
3152
|
const seen = /* @__PURE__ */ new Set();
|
|
1862
3153
|
for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
|
|
1863
|
-
const
|
|
1864
|
-
if (!
|
|
1865
|
-
seen.add(
|
|
1866
|
-
screens.push(
|
|
1867
|
-
name: screenName,
|
|
1868
|
-
navigatorName: navigatorVarName,
|
|
1869
|
-
navigatorType
|
|
1870
|
-
});
|
|
3154
|
+
const screen = this.parseScreenElement(filePath, element, navigatorVarName, navigatorType);
|
|
3155
|
+
if (!screen || seen.has(screen.name)) continue;
|
|
3156
|
+
seen.add(screen.name);
|
|
3157
|
+
screens.push(screen);
|
|
1871
3158
|
}
|
|
1872
3159
|
return screens;
|
|
1873
3160
|
}
|
|
3161
|
+
/**
|
|
3162
|
+
* Uma `<X.Screen>` isolada até a rota que ela declara.
|
|
3163
|
+
*
|
|
3164
|
+
* Separado de `extractScreensFromNavigator` porque a MESMA leitura serve para
|
|
3165
|
+
* a `<X.Screen>` que mora dentro do `<X.Navigator>` e para a que mora fora
|
|
3166
|
+
* dele — só a forma de chegar até o elemento muda.
|
|
3167
|
+
*/
|
|
3168
|
+
parseScreenElement(filePath, element, navigatorName, navigatorType) {
|
|
3169
|
+
const attrs = element.openingElement.attributes;
|
|
3170
|
+
const nameAttr = attrs.find(
|
|
3171
|
+
(a) => BabelTypes.isJSXAttribute(a) && BabelTypes.isJSXIdentifier(a.name) && a.name.name === "name"
|
|
3172
|
+
);
|
|
3173
|
+
const screenName = BabelTypes.isJSXAttribute(nameAttr) ? this.attributeString(filePath, nameAttr) : null;
|
|
3174
|
+
if (!screenName) return null;
|
|
3175
|
+
const componentAttr = attrs.find(
|
|
3176
|
+
(a) => BabelTypes.isJSXAttribute(a) && BabelTypes.isJSXIdentifier(a.name) && a.name.name === "component"
|
|
3177
|
+
);
|
|
3178
|
+
let componentFile = null;
|
|
3179
|
+
if (BabelTypes.isJSXAttribute(componentAttr) && BabelTypes.isJSXExpressionContainer(componentAttr.value)) {
|
|
3180
|
+
const expr = componentAttr.value.expression;
|
|
3181
|
+
if (BabelTypes.isExpression(expr)) componentFile = this.graph.componentFile(filePath, expr);
|
|
3182
|
+
}
|
|
3183
|
+
if (componentFile) this.routeTargets.set(screenName, componentFile);
|
|
3184
|
+
return {
|
|
3185
|
+
name: screenName,
|
|
3186
|
+
navigatorName,
|
|
3187
|
+
navigatorType,
|
|
3188
|
+
...componentFile ? { componentFile } : {}
|
|
3189
|
+
};
|
|
3190
|
+
}
|
|
1874
3191
|
/** Extract string attribute value from JSX attributes */
|
|
1875
3192
|
extractAttributeValue(attributes, attrName) {
|
|
1876
3193
|
const attr = attributes.find(
|
|
@@ -1893,7 +3210,7 @@ var NavigationAnalyzer = class {
|
|
|
1893
3210
|
...this.config.parserPlugins || []
|
|
1894
3211
|
]
|
|
1895
3212
|
});
|
|
1896
|
-
|
|
3213
|
+
traverse5(ast, {
|
|
1897
3214
|
TSTypeAliasDeclaration: (nodePath) => {
|
|
1898
3215
|
const { node } = nodePath;
|
|
1899
3216
|
const typeName = node.id.name;
|
|
@@ -1966,7 +3283,7 @@ var NavigationAnalyzer = class {
|
|
|
1966
3283
|
if (type.type === "TSUndefinedKeyword") return "undefined";
|
|
1967
3284
|
if (type.type === "TSNullKeyword") return "null";
|
|
1968
3285
|
if (type.type === "TSUnionType") {
|
|
1969
|
-
return type.types.map((
|
|
3286
|
+
return type.types.map((t15) => this.typeToString(t15)).join(" | ");
|
|
1970
3287
|
}
|
|
1971
3288
|
if (type.type === "TSTypeLiteral") {
|
|
1972
3289
|
return "object";
|
|
@@ -1987,7 +3304,7 @@ var NavigationAnalyzer = class {
|
|
|
1987
3304
|
/** Attach parsed type params to navigator screens */
|
|
1988
3305
|
attachParamsToNavigators(navigators, types) {
|
|
1989
3306
|
for (const navigator of navigators) {
|
|
1990
|
-
const matchingType = types.find((
|
|
3307
|
+
const matchingType = types.find((t15) => t15.type === navigator.type);
|
|
1991
3308
|
if (matchingType) {
|
|
1992
3309
|
for (const screen of navigator.screens) {
|
|
1993
3310
|
const screenParams = matchingType.paramEntries.get(screen.name);
|
|
@@ -2076,9 +3393,9 @@ var ComponentAnalyzer = class {
|
|
|
2076
3393
|
plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
|
|
2077
3394
|
});
|
|
2078
3395
|
const components = [];
|
|
2079
|
-
|
|
2080
|
-
JSXElement: (
|
|
2081
|
-
const component = this.extractComponentFromJSXElement(
|
|
3396
|
+
traverse5(ast, {
|
|
3397
|
+
JSXElement: (path11) => {
|
|
3398
|
+
const component = this.extractComponentFromJSXElement(path11.node);
|
|
2082
3399
|
if (component) {
|
|
2083
3400
|
components.push(component);
|
|
2084
3401
|
}
|
|
@@ -2204,14 +3521,14 @@ var FormAnalyzer = class {
|
|
|
2204
3521
|
this.stateVariables.clear();
|
|
2205
3522
|
this.inputElements = [];
|
|
2206
3523
|
this.submitButtons = [];
|
|
2207
|
-
|
|
2208
|
-
CallExpression: (
|
|
2209
|
-
this.extractStateVariables(
|
|
3524
|
+
traverse5(ast, {
|
|
3525
|
+
CallExpression: (path11) => {
|
|
3526
|
+
this.extractStateVariables(path11.node);
|
|
2210
3527
|
}
|
|
2211
3528
|
});
|
|
2212
|
-
|
|
2213
|
-
JSXElement: (
|
|
2214
|
-
this.extractFormElements(
|
|
3529
|
+
traverse5(ast, {
|
|
3530
|
+
JSXElement: (path11) => {
|
|
3531
|
+
this.extractFormElements(path11.node);
|
|
2215
3532
|
}
|
|
2216
3533
|
});
|
|
2217
3534
|
const validationRules = this.extractValidationRules(ast);
|
|
@@ -2245,23 +3562,23 @@ var FormAnalyzer = class {
|
|
|
2245
3562
|
for (const attr of openingElement.attributes) {
|
|
2246
3563
|
if (BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name)) {
|
|
2247
3564
|
const propName = attr.name.name;
|
|
2248
|
-
const
|
|
3565
|
+
const propValue2 = this.extractAttributeValue(attr.value);
|
|
2249
3566
|
switch (propName) {
|
|
2250
3567
|
case "label":
|
|
2251
|
-
info.label =
|
|
3568
|
+
info.label = propValue2;
|
|
2252
3569
|
break;
|
|
2253
3570
|
case "placeholder":
|
|
2254
|
-
info.placeholder =
|
|
3571
|
+
info.placeholder = propValue2;
|
|
2255
3572
|
break;
|
|
2256
3573
|
case "keyboardType":
|
|
2257
|
-
info.keyboardType =
|
|
3574
|
+
info.keyboardType = propValue2;
|
|
2258
3575
|
break;
|
|
2259
3576
|
case "testID":
|
|
2260
|
-
info.testID =
|
|
3577
|
+
info.testID = propValue2;
|
|
2261
3578
|
break;
|
|
2262
3579
|
case "appilotsId":
|
|
2263
|
-
info.appilotsId =
|
|
2264
|
-
if (
|
|
3580
|
+
info.appilotsId = propValue2;
|
|
3581
|
+
if (propValue2) info.varName = propValue2;
|
|
2265
3582
|
break;
|
|
2266
3583
|
case "value":
|
|
2267
3584
|
if (attr.value && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
|
|
@@ -2310,9 +3627,9 @@ var FormAnalyzer = class {
|
|
|
2310
3627
|
}
|
|
2311
3628
|
extractValidationRules(ast) {
|
|
2312
3629
|
const rules = {};
|
|
2313
|
-
|
|
2314
|
-
IfStatement: (
|
|
2315
|
-
const test =
|
|
3630
|
+
traverse5(ast, {
|
|
3631
|
+
IfStatement: (path11) => {
|
|
3632
|
+
const test = path11.node.test;
|
|
2316
3633
|
const rule = this.extractRuleFromCondition(test);
|
|
2317
3634
|
if (rule) {
|
|
2318
3635
|
const { field, description } = rule;
|
|
@@ -2366,7 +3683,7 @@ var FormAnalyzer = class {
|
|
|
2366
3683
|
}
|
|
2367
3684
|
buildForms(filePath, validationRules) {
|
|
2368
3685
|
if (this.inputElements.length === 0) return [];
|
|
2369
|
-
const fileName =
|
|
3686
|
+
const fileName = path2.basename(filePath, path2.extname(filePath));
|
|
2370
3687
|
const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
|
|
2371
3688
|
const fields = this.inputElements.map((input) => {
|
|
2372
3689
|
const fieldType = this.inferFieldType(input);
|
|
@@ -2415,13 +3732,105 @@ var FormAnalyzer = class {
|
|
|
2415
3732
|
return rule !== void 0 && rule.includes("required");
|
|
2416
3733
|
}
|
|
2417
3734
|
};
|
|
3735
|
+
|
|
3736
|
+
// src/pipeline/composition.ts
|
|
3737
|
+
var MAX_DEPTH = 3;
|
|
3738
|
+
var MAX_VISITED_PER_SCREEN = 60;
|
|
3739
|
+
var MAX_FAN_IN = 3;
|
|
3740
|
+
var ADDRESSABLE = /* @__PURE__ */ new Set(["testID", "appilotsId", "accessibilityLabel", "label"]);
|
|
3741
|
+
function descendants(graph, from, screenFiles) {
|
|
3742
|
+
const seen = /* @__PURE__ */ new Set([from]);
|
|
3743
|
+
const out = [];
|
|
3744
|
+
let frontier = [from];
|
|
3745
|
+
for (let depth = 0; depth < MAX_DEPTH && frontier.length > 0; depth++) {
|
|
3746
|
+
const next = [];
|
|
3747
|
+
for (const file of frontier) {
|
|
3748
|
+
for (const child of graph.renderedComponentFiles(file)) {
|
|
3749
|
+
if (seen.has(child)) continue;
|
|
3750
|
+
seen.add(child);
|
|
3751
|
+
if (screenFiles.has(child)) continue;
|
|
3752
|
+
out.push(child);
|
|
3753
|
+
next.push(child);
|
|
3754
|
+
if (out.length >= MAX_VISITED_PER_SCREEN) return out;
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
frontier = next;
|
|
3758
|
+
}
|
|
3759
|
+
return out;
|
|
3760
|
+
}
|
|
3761
|
+
function addressable(locator) {
|
|
3762
|
+
if (!locator || typeof locator !== "object") return false;
|
|
3763
|
+
const l = locator;
|
|
3764
|
+
if (typeof l["source"] === "string" && ADDRESSABLE.has(l["source"])) return true;
|
|
3765
|
+
return typeof l["testID"] === "string" && l["testID"].length > 0;
|
|
3766
|
+
}
|
|
3767
|
+
async function composeAffordances(options) {
|
|
3768
|
+
const { screens, graph, analyzeFile, screenFiles } = options;
|
|
3769
|
+
const byScreen = /* @__PURE__ */ new Map();
|
|
3770
|
+
const fanIn = /* @__PURE__ */ new Map();
|
|
3771
|
+
for (const screen of screens) {
|
|
3772
|
+
if (!screen.filePath) continue;
|
|
3773
|
+
const children = descendants(graph, screen.filePath, screenFiles);
|
|
3774
|
+
byScreen.set(screen, children);
|
|
3775
|
+
for (const child of children) fanIn.set(child, (fanIn.get(child) ?? 0) + 1);
|
|
3776
|
+
}
|
|
3777
|
+
const analyzed = /* @__PURE__ */ new Map();
|
|
3778
|
+
let screensEnriched = 0;
|
|
3779
|
+
let filesMerged = 0;
|
|
3780
|
+
for (const [screen, children] of byScreen) {
|
|
3781
|
+
const exclusive = children.filter((c) => (fanIn.get(c) ?? 0) <= MAX_FAN_IN);
|
|
3782
|
+
if (exclusive.length === 0) continue;
|
|
3783
|
+
const actionIds = new Set(screen.actions.map((a) => a.id));
|
|
3784
|
+
const targetIds = new Set((screen.targets ?? []).map((t15) => t15.id));
|
|
3785
|
+
const formIds = new Set(screen.forms.map((f) => f.id));
|
|
3786
|
+
let gained = false;
|
|
3787
|
+
for (const file of exclusive) {
|
|
3788
|
+
if (!analyzed.has(file)) {
|
|
3789
|
+
try {
|
|
3790
|
+
analyzed.set(file, await analyzeFile(file));
|
|
3791
|
+
} catch {
|
|
3792
|
+
analyzed.set(file, null);
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
const child = analyzed.get(file);
|
|
3796
|
+
if (!child) continue;
|
|
3797
|
+
let merged = false;
|
|
3798
|
+
for (const action of child.actions) {
|
|
3799
|
+
if (!addressable(action.locator)) continue;
|
|
3800
|
+
if (actionIds.has(action.id)) continue;
|
|
3801
|
+
actionIds.add(action.id);
|
|
3802
|
+
screen.actions.push(action);
|
|
3803
|
+
merged = true;
|
|
3804
|
+
}
|
|
3805
|
+
for (const target of child.targets ?? []) {
|
|
3806
|
+
if (!addressable(target.locator)) continue;
|
|
3807
|
+
if (targetIds.has(target.id)) continue;
|
|
3808
|
+
targetIds.add(target.id);
|
|
3809
|
+
(screen.targets ??= []).push(target);
|
|
3810
|
+
merged = true;
|
|
3811
|
+
}
|
|
3812
|
+
for (const form of child.forms) {
|
|
3813
|
+
if (form.fields.length === 0) continue;
|
|
3814
|
+
const id = formIds.has(form.id) ? `${form.id}:${child.name}` : form.id;
|
|
3815
|
+
if (formIds.has(id)) continue;
|
|
3816
|
+
formIds.add(id);
|
|
3817
|
+
screen.forms.push({ ...form, id });
|
|
3818
|
+
merged = true;
|
|
3819
|
+
}
|
|
3820
|
+
if (merged) {
|
|
3821
|
+
filesMerged++;
|
|
3822
|
+
gained = true;
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
if (gained) screensEnriched++;
|
|
3826
|
+
}
|
|
3827
|
+
return { screens, screensEnriched, filesMerged };
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
// src/analyzers/ReactNativePlatformAnalyzer.ts
|
|
2418
3831
|
var ReactNativePlatformAnalyzer = class {
|
|
2419
3832
|
platform = "react-native";
|
|
2420
3833
|
async analyze(config, options) {
|
|
2421
|
-
const screenAnalyzer = new ScreenAnalyzer(config, {
|
|
2422
|
-
strictScreens: options.strictScreens ?? true,
|
|
2423
|
-
screenPatterns: options.screenPatterns
|
|
2424
|
-
});
|
|
2425
3834
|
const navigationAnalyzer = new NavigationAnalyzer(config, {
|
|
2426
3835
|
navigationInclude: options.navigationInclude,
|
|
2427
3836
|
navigationExclude: options.navigationExclude
|
|
@@ -2429,14 +3838,31 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2429
3838
|
const componentAnalyzer = new ComponentAnalyzer(config);
|
|
2430
3839
|
const formAnalyzer = new FormAnalyzer(config);
|
|
2431
3840
|
console.log("[ReactNativePlatformAnalyzer] Running analyzers...");
|
|
2432
|
-
const
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
3841
|
+
const navigation = await navigationAnalyzer.analyze();
|
|
3842
|
+
const screenAnalyzer = new ScreenAnalyzer(config, {
|
|
3843
|
+
strictScreens: options.strictScreens ?? true,
|
|
3844
|
+
screenPatterns: options.screenPatterns,
|
|
3845
|
+
// Menos os arquivos que declaram navegador: ver `navigatorFiles`.
|
|
3846
|
+
routeTargetFiles: new Set(
|
|
3847
|
+
[...navigationAnalyzer.routeTargets.values()].filter(
|
|
3848
|
+
(file) => !navigationAnalyzer.navigatorFiles.has(file)
|
|
3849
|
+
)
|
|
3850
|
+
)
|
|
3851
|
+
});
|
|
3852
|
+
const screens = await screenAnalyzer.analyze();
|
|
3853
|
+
const composed = await composeAffordances({
|
|
3854
|
+
screens,
|
|
3855
|
+
graph: navigationAnalyzer.graph,
|
|
3856
|
+
analyzeFile: (file) => screenAnalyzer.analyzeFile(file),
|
|
3857
|
+
screenFiles: new Set(screens.map((s) => s.filePath).filter(Boolean))
|
|
3858
|
+
});
|
|
3859
|
+
console.log(
|
|
3860
|
+
`[ReactNativePlatformAnalyzer] Composi\xE7\xE3o: ${composed.filesMerged} componente(s) exclusivo(s) fundido(s) em ${composed.screensEnriched} tela(s)`
|
|
3861
|
+
);
|
|
2436
3862
|
console.log(
|
|
2437
3863
|
`[ReactNativePlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens`
|
|
2438
3864
|
);
|
|
2439
|
-
const screenFiles = await
|
|
3865
|
+
const screenFiles = await globSorted(config.include || ["**/*.tsx", "**/*.ts"], {
|
|
2440
3866
|
cwd: config.rootDir,
|
|
2441
3867
|
ignore: config.exclude || ["**/node_modules/**"]
|
|
2442
3868
|
});
|
|
@@ -2444,7 +3870,7 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2444
3870
|
`[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
|
|
2445
3871
|
);
|
|
2446
3872
|
const enrichmentPromises = screenFiles.map(async (file) => {
|
|
2447
|
-
const filePath =
|
|
3873
|
+
const filePath = path2__default.resolve(config.rootDir, file);
|
|
2448
3874
|
try {
|
|
2449
3875
|
const [components, forms] = await Promise.all([
|
|
2450
3876
|
componentAnalyzer.analyzeFile(filePath),
|
|
@@ -2497,9 +3923,11 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2497
3923
|
});
|
|
2498
3924
|
return {
|
|
2499
3925
|
screens: enrichedScreens,
|
|
3926
|
+
controlEvidenceFiles: screenAnalyzer.controlEvidenceFiles,
|
|
2500
3927
|
navigation,
|
|
2501
3928
|
analyzedFiles: screenFiles.length,
|
|
2502
|
-
...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
|
|
3929
|
+
...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {},
|
|
3930
|
+
diagnostics: { navigation: navigationAnalyzer.diagnostics }
|
|
2503
3931
|
};
|
|
2504
3932
|
}
|
|
2505
3933
|
mergeForm(target, source) {
|
|
@@ -2517,9 +3945,12 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2517
3945
|
findEquivalentField(fields, incoming) {
|
|
2518
3946
|
return fields.find((field) => {
|
|
2519
3947
|
if (field.name && incoming.name && field.name === incoming.name) return true;
|
|
2520
|
-
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
2521
|
-
|
|
2522
|
-
if (field.
|
|
3948
|
+
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
3949
|
+
return true;
|
|
3950
|
+
if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
|
|
3951
|
+
return true;
|
|
3952
|
+
if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
|
|
3953
|
+
return true;
|
|
2523
3954
|
if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
|
|
2524
3955
|
return true;
|
|
2525
3956
|
}
|
|
@@ -2552,7 +3983,9 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2552
3983
|
if (incoming.fields.length === 0) return void 0;
|
|
2553
3984
|
let best;
|
|
2554
3985
|
for (const form of forms) {
|
|
2555
|
-
const overlap = incoming.fields.filter(
|
|
3986
|
+
const overlap = incoming.fields.filter(
|
|
3987
|
+
(field) => this.findEquivalentField(form.fields, field)
|
|
3988
|
+
).length;
|
|
2556
3989
|
if (overlap > 0 && (!best || overlap > best.overlap)) {
|
|
2557
3990
|
best = { form, overlap };
|
|
2558
3991
|
}
|
|
@@ -2706,13 +4139,13 @@ function staticRoutePath(node) {
|
|
|
2706
4139
|
if (!node) return void 0;
|
|
2707
4140
|
if (BabelTypes.isStringLiteral(node)) return node.value;
|
|
2708
4141
|
if (BabelTypes.isTemplateLiteral(node)) {
|
|
2709
|
-
let
|
|
4142
|
+
let path11 = "";
|
|
2710
4143
|
node.quasis.forEach((quasi, index) => {
|
|
2711
|
-
|
|
4144
|
+
path11 += quasi.value.cooked ?? quasi.value.raw;
|
|
2712
4145
|
const expr = node.expressions[index];
|
|
2713
|
-
if (expr)
|
|
4146
|
+
if (expr) path11 += `:${paramNameOf(expr)}`;
|
|
2714
4147
|
});
|
|
2715
|
-
return
|
|
4148
|
+
return path11;
|
|
2716
4149
|
}
|
|
2717
4150
|
return void 0;
|
|
2718
4151
|
}
|
|
@@ -2755,7 +4188,7 @@ function extractWebNavigationCalls(ast) {
|
|
|
2755
4188
|
calls.push({ method: "navigate", targetPath: node.right.value });
|
|
2756
4189
|
}
|
|
2757
4190
|
};
|
|
2758
|
-
|
|
4191
|
+
traverse5(ast, {
|
|
2759
4192
|
noScope: !BabelTypes.isFile(ast),
|
|
2760
4193
|
enter: (nodePath) => inspect(nodePath.node)
|
|
2761
4194
|
});
|
|
@@ -2797,17 +4230,17 @@ var WebScreenAnalyzer = class {
|
|
|
2797
4230
|
include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
|
|
2798
4231
|
exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
2799
4232
|
} = this.config;
|
|
2800
|
-
const files = (await
|
|
4233
|
+
const files = (await globSorted(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
|
|
2801
4234
|
(file) => !file.endsWith(".d.ts")
|
|
2802
4235
|
);
|
|
2803
|
-
const patternMatches = await
|
|
4236
|
+
const patternMatches = await globSorted(this.screenPatterns, {
|
|
2804
4237
|
cwd: this.config.rootDir,
|
|
2805
4238
|
ignore: exclude
|
|
2806
4239
|
});
|
|
2807
|
-
const patternSet = new Set(patternMatches.map((f) =>
|
|
4240
|
+
const patternSet = new Set(patternMatches.map((f) => path2__default.resolve(this.config.rootDir, f)));
|
|
2808
4241
|
const candidates = [];
|
|
2809
4242
|
for (const file of files) {
|
|
2810
|
-
const filePath =
|
|
4243
|
+
const filePath = path2__default.resolve(this.config.rootDir, file);
|
|
2811
4244
|
try {
|
|
2812
4245
|
const candidate = await this.analyzeFile(filePath);
|
|
2813
4246
|
if (!candidate) continue;
|
|
@@ -2851,7 +4284,7 @@ var WebScreenAnalyzer = class {
|
|
|
2851
4284
|
}
|
|
2852
4285
|
}
|
|
2853
4286
|
}
|
|
2854
|
-
const name = registerScreenMeta?.name || componentName ||
|
|
4287
|
+
const name = registerScreenMeta?.name || componentName || path2__default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
|
|
2855
4288
|
const descriptor = {
|
|
2856
4289
|
name,
|
|
2857
4290
|
filePath,
|
|
@@ -2877,7 +4310,7 @@ var WebScreenAnalyzer = class {
|
|
|
2877
4310
|
// ── registerScreen ────────────────────────────────────────────────
|
|
2878
4311
|
detectRegisterScreenCall(ast) {
|
|
2879
4312
|
let found = false;
|
|
2880
|
-
|
|
4313
|
+
traverse5(ast, {
|
|
2881
4314
|
CallExpression: (nodePath) => {
|
|
2882
4315
|
if (found) return;
|
|
2883
4316
|
if (isRegisterScreenCallee(nodePath.node.callee)) {
|
|
@@ -2890,7 +4323,7 @@ var WebScreenAnalyzer = class {
|
|
|
2890
4323
|
}
|
|
2891
4324
|
extractRegisterScreenMetadata(ast) {
|
|
2892
4325
|
let plain = null;
|
|
2893
|
-
|
|
4326
|
+
traverse5(ast, {
|
|
2894
4327
|
CallExpression: (nodePath) => {
|
|
2895
4328
|
if (!isRegisterScreenCallee(nodePath.node.callee)) return;
|
|
2896
4329
|
const arg = nodePath.node.arguments[0];
|
|
@@ -2938,7 +4371,7 @@ var WebScreenAnalyzer = class {
|
|
|
2938
4371
|
extractComponentName(ast) {
|
|
2939
4372
|
let defaultName = "";
|
|
2940
4373
|
let firstExported = "";
|
|
2941
|
-
|
|
4374
|
+
traverse5(ast, {
|
|
2942
4375
|
ExportDefaultDeclaration: (nodePath) => {
|
|
2943
4376
|
const declaration = nodePath.node.declaration;
|
|
2944
4377
|
if (BabelTypes.isFunctionDeclaration(declaration) && declaration.id?.name) {
|
|
@@ -2967,7 +4400,7 @@ var WebScreenAnalyzer = class {
|
|
|
2967
4400
|
extractComponents(ast) {
|
|
2968
4401
|
const components = [];
|
|
2969
4402
|
const seen = /* @__PURE__ */ new Set();
|
|
2970
|
-
|
|
4403
|
+
traverse5(ast, {
|
|
2971
4404
|
JSXOpeningElement: (nodePath) => {
|
|
2972
4405
|
const element = nodePath.node;
|
|
2973
4406
|
const name = getJsxElementName(element);
|
|
@@ -2988,7 +4421,7 @@ var WebScreenAnalyzer = class {
|
|
|
2988
4421
|
// ── <label htmlFor> association ───────────────────────────────────
|
|
2989
4422
|
collectHtmlForLabels(ast) {
|
|
2990
4423
|
const labels = /* @__PURE__ */ new Map();
|
|
2991
|
-
|
|
4424
|
+
traverse5(ast, {
|
|
2992
4425
|
JSXElement: (nodePath) => {
|
|
2993
4426
|
const element = nodePath.node;
|
|
2994
4427
|
if (getJsxElementName(element.openingElement) !== "label") return;
|
|
@@ -3031,7 +4464,7 @@ var WebScreenAnalyzer = class {
|
|
|
3031
4464
|
formBuckets.set(formElement, bucket);
|
|
3032
4465
|
return bucket;
|
|
3033
4466
|
};
|
|
3034
|
-
|
|
4467
|
+
traverse5(ast, {
|
|
3035
4468
|
JSXElement: (nodePath) => {
|
|
3036
4469
|
const element = nodePath.node;
|
|
3037
4470
|
const name = getJsxElementName(element.openingElement);
|
|
@@ -3048,7 +4481,7 @@ var WebScreenAnalyzer = class {
|
|
|
3048
4481
|
if (!bucket.fields.has(field.name)) bucket.fields.set(field.name, field);
|
|
3049
4482
|
}
|
|
3050
4483
|
});
|
|
3051
|
-
|
|
4484
|
+
traverse5(ast, {
|
|
3052
4485
|
JSXElement: (nodePath) => {
|
|
3053
4486
|
const element = nodePath.node;
|
|
3054
4487
|
const name = getJsxElementName(element.openingElement);
|
|
@@ -3226,7 +4659,7 @@ var WebScreenAnalyzer = class {
|
|
|
3226
4659
|
const actionLabels = new Map(
|
|
3227
4660
|
actions.filter((a) => a.label).map((a) => [normalizeLabel(a.label), a])
|
|
3228
4661
|
);
|
|
3229
|
-
|
|
4662
|
+
traverse5(ast, {
|
|
3230
4663
|
JSXElement: (nodePath) => {
|
|
3231
4664
|
const element = nodePath.node;
|
|
3232
4665
|
const name = getJsxElementName(element.openingElement);
|
|
@@ -3457,7 +4890,7 @@ var WebScreenAnalyzer = class {
|
|
|
3457
4890
|
for (const call of extractWebNavigationCalls(ast)) {
|
|
3458
4891
|
if (call.targetPath && call.targetPath.startsWith("/")) targets.add(call.targetPath);
|
|
3459
4892
|
}
|
|
3460
|
-
|
|
4893
|
+
traverse5(ast, {
|
|
3461
4894
|
JSXOpeningElement: (nodePath) => {
|
|
3462
4895
|
const element = nodePath.node;
|
|
3463
4896
|
const name = getJsxElementName(element);
|
|
@@ -3477,7 +4910,7 @@ var WebScreenAnalyzer = class {
|
|
|
3477
4910
|
extractCollections(ast) {
|
|
3478
4911
|
const collections = [];
|
|
3479
4912
|
const seen = /* @__PURE__ */ new Set();
|
|
3480
|
-
|
|
4913
|
+
traverse5(ast, {
|
|
3481
4914
|
JSXExpressionContainer: (nodePath) => {
|
|
3482
4915
|
const expr = nodePath.node.expression;
|
|
3483
4916
|
if (!BabelTypes.isCallExpression(expr) || !BabelTypes.isMemberExpression(expr.callee) || !BabelTypes.isIdentifier(expr.callee.object) || !BabelTypes.isIdentifier(expr.callee.property) || expr.callee.property.name !== "map") {
|
|
@@ -3589,7 +5022,7 @@ function firstCalledFunctionName(node) {
|
|
|
3589
5022
|
}
|
|
3590
5023
|
function containsWindowConfirm(body) {
|
|
3591
5024
|
let found = false;
|
|
3592
|
-
|
|
5025
|
+
traverse5(
|
|
3593
5026
|
body,
|
|
3594
5027
|
{
|
|
3595
5028
|
noScope: true,
|
|
@@ -3641,7 +5074,7 @@ function collectionItemNames(callback) {
|
|
|
3641
5074
|
function collectionDisplayFields(callback, itemNames) {
|
|
3642
5075
|
const fields = /* @__PURE__ */ new Set();
|
|
3643
5076
|
if (!callback.body) return [];
|
|
3644
|
-
|
|
5077
|
+
traverse5(
|
|
3645
5078
|
callback.body,
|
|
3646
5079
|
{
|
|
3647
5080
|
noScope: true,
|
|
@@ -3658,7 +5091,7 @@ function collectionDisplayFields(callback, itemNames) {
|
|
|
3658
5091
|
function collectionKeyField(callback, itemNames) {
|
|
3659
5092
|
let keyField;
|
|
3660
5093
|
if (!callback.body) return void 0;
|
|
3661
|
-
|
|
5094
|
+
traverse5(
|
|
3662
5095
|
callback.body,
|
|
3663
5096
|
{
|
|
3664
5097
|
noScope: true,
|
|
@@ -3740,7 +5173,9 @@ var WebNavigationAnalyzer = class {
|
|
|
3740
5173
|
for (const filePath of files) {
|
|
3741
5174
|
try {
|
|
3742
5175
|
const content = await promises.readFile(filePath, "utf-8");
|
|
3743
|
-
if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
|
|
5176
|
+
if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
|
|
5177
|
+
content
|
|
5178
|
+
)) {
|
|
3744
5179
|
continue;
|
|
3745
5180
|
}
|
|
3746
5181
|
const ast = parseSource(content, this.config.parserPlugins);
|
|
@@ -3770,8 +5205,8 @@ var WebNavigationAnalyzer = class {
|
|
|
3770
5205
|
...this.config.exclude || [],
|
|
3771
5206
|
...this.navigationExclude
|
|
3772
5207
|
];
|
|
3773
|
-
const files = await
|
|
3774
|
-
return files.map((file) =>
|
|
5208
|
+
const files = await globSorted(patterns, { cwd: this.config.rootDir, ignore });
|
|
5209
|
+
return files.map((file) => path2__default.join(this.config.rootDir, file));
|
|
3775
5210
|
}
|
|
3776
5211
|
// ── JSX <Route> style ────────────────────────────────────────────
|
|
3777
5212
|
extractJsxRoutes(ast) {
|
|
@@ -3799,7 +5234,7 @@ var WebNavigationAnalyzer = class {
|
|
|
3799
5234
|
if (BabelTypes.isJSXElement(child)) visitRoute(child, fullPath);
|
|
3800
5235
|
}
|
|
3801
5236
|
};
|
|
3802
|
-
|
|
5237
|
+
traverse5(ast, {
|
|
3803
5238
|
JSXElement: (nodePath) => {
|
|
3804
5239
|
const name = getJsxElementName(nodePath.node.openingElement);
|
|
3805
5240
|
if (name !== "Routes" && name !== "Route") return;
|
|
@@ -3838,7 +5273,7 @@ var WebNavigationAnalyzer = class {
|
|
|
3838
5273
|
"createMemoryRouter",
|
|
3839
5274
|
"useRoutes"
|
|
3840
5275
|
]);
|
|
3841
|
-
|
|
5276
|
+
traverse5(ast, {
|
|
3842
5277
|
CallExpression: (nodePath) => {
|
|
3843
5278
|
const callee = nodePath.node.callee;
|
|
3844
5279
|
if (!BabelTypes.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
|
|
@@ -3943,11 +5378,13 @@ var WebNavigationAnalyzer = class {
|
|
|
3943
5378
|
return {
|
|
3944
5379
|
screens,
|
|
3945
5380
|
initialScreen: initialRoute?.screenName ?? "",
|
|
3946
|
-
navigators: routes.length > 0 ? [
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
5381
|
+
navigators: routes.length > 0 ? [
|
|
5382
|
+
{
|
|
5383
|
+
name: navigatorName,
|
|
5384
|
+
type: WEB_NAVIGATOR_TYPE,
|
|
5385
|
+
screens: screenNames
|
|
5386
|
+
}
|
|
5387
|
+
] : []
|
|
3951
5388
|
};
|
|
3952
5389
|
}
|
|
3953
5390
|
};
|
|
@@ -4269,8 +5706,8 @@ var ZodParsedType = util.arrayToEnum([
|
|
|
4269
5706
|
"set"
|
|
4270
5707
|
]);
|
|
4271
5708
|
var getParsedType = (data) => {
|
|
4272
|
-
const
|
|
4273
|
-
switch (
|
|
5709
|
+
const t15 = typeof data;
|
|
5710
|
+
switch (t15) {
|
|
4274
5711
|
case "undefined":
|
|
4275
5712
|
return ZodParsedType.undefined;
|
|
4276
5713
|
case "string":
|
|
@@ -4542,8 +5979,8 @@ function getErrorMap() {
|
|
|
4542
5979
|
|
|
4543
5980
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
4544
5981
|
var makeIssue = (params) => {
|
|
4545
|
-
const { data, path:
|
|
4546
|
-
const fullPath = [...
|
|
5982
|
+
const { data, path: path11, errorMaps, issueData } = params;
|
|
5983
|
+
const fullPath = [...path11, ...issueData.path || []];
|
|
4547
5984
|
const fullIssue = {
|
|
4548
5985
|
...issueData,
|
|
4549
5986
|
path: fullPath
|
|
@@ -4659,11 +6096,11 @@ var errorUtil;
|
|
|
4659
6096
|
|
|
4660
6097
|
// ../../node_modules/zod/v3/types.js
|
|
4661
6098
|
var ParseInputLazyPath = class {
|
|
4662
|
-
constructor(parent, value,
|
|
6099
|
+
constructor(parent, value, path11, key) {
|
|
4663
6100
|
this._cachedPath = [];
|
|
4664
6101
|
this.parent = parent;
|
|
4665
6102
|
this.data = value;
|
|
4666
|
-
this._path =
|
|
6103
|
+
this._path = path11;
|
|
4667
6104
|
this._key = key;
|
|
4668
6105
|
}
|
|
4669
6106
|
get path() {
|
|
@@ -8104,7 +9541,36 @@ var coerce = {
|
|
|
8104
9541
|
};
|
|
8105
9542
|
var NEVER = INVALID;
|
|
8106
9543
|
|
|
8107
|
-
// ../shared/dist/chunk-
|
|
9544
|
+
// ../shared/dist/chunk-SWQOZWHP.mjs
|
|
9545
|
+
var SAFE_IMAGE_URL_RE = /^(?:https?:\/\/|data:image\/(?:png|jpe?g|gif|webp);base64,)/i;
|
|
9546
|
+
var HAS_SCHEME_RE = /^[\s\u0000-\u001f]*[a-z][a-z0-9+.-]*:/i;
|
|
9547
|
+
function isSafeImageUrl(value) {
|
|
9548
|
+
if (value === null || value === void 0) return true;
|
|
9549
|
+
const trimmed = value.trim();
|
|
9550
|
+
if (trimmed === "") return true;
|
|
9551
|
+
if (!HAS_SCHEME_RE.test(trimmed)) return true;
|
|
9552
|
+
return SAFE_IMAGE_URL_RE.test(trimmed);
|
|
9553
|
+
}
|
|
9554
|
+
|
|
9555
|
+
// ../shared/dist/chunk-KUMPS6RH.mjs
|
|
9556
|
+
var SUPPORTED_APPILOTS_LOCALES = ["pt-BR", "en", "es", "fr"];
|
|
9557
|
+
|
|
9558
|
+
// ../shared/dist/chunk-2GTRKNPJ.mjs
|
|
9559
|
+
var symbols = external_exports.array(external_exports.string().max(240)).max(12);
|
|
9560
|
+
var controlEvidenceSchema = external_exports.object({
|
|
9561
|
+
version: external_exports.literal(1),
|
|
9562
|
+
siteId: external_exports.string().regex(/^[a-f0-9]{20}$/),
|
|
9563
|
+
component: external_exports.string().max(240),
|
|
9564
|
+
icons: symbols,
|
|
9565
|
+
handler: external_exports.string().max(240).optional(),
|
|
9566
|
+
calls: symbols,
|
|
9567
|
+
argumentBindings: symbols,
|
|
9568
|
+
conditions: symbols,
|
|
9569
|
+
nativeConfirmation: external_exports.object({
|
|
9570
|
+
title: external_exports.string().max(240).optional(),
|
|
9571
|
+
destructiveOption: external_exports.boolean()
|
|
9572
|
+
}).optional()
|
|
9573
|
+
});
|
|
8108
9574
|
var locatorSourceSchema = external_exports.enum([
|
|
8109
9575
|
"appilotsId",
|
|
8110
9576
|
"testID",
|
|
@@ -8283,6 +9749,23 @@ var actionDescriptorSchema = external_exports.object({
|
|
|
8283
9749
|
effect: external_exports.enum(["read", "write", "destructive"]).or(external_exports.string()).optional(),
|
|
8284
9750
|
riskLevel: external_exports.enum(["low", "medium", "high"]).or(external_exports.string()).optional(),
|
|
8285
9751
|
requiresConfirmation: external_exports.boolean().optional(),
|
|
9752
|
+
/**
|
|
9753
|
+
* How long this action's work is expected to take, in milliseconds,
|
|
9754
|
+
* DECLARED by the app — not inferred (that is `appilotsInferred`).
|
|
9755
|
+
*
|
|
9756
|
+
* The SDK's post-action wait was a constant: 6s, or 10s when the
|
|
9757
|
+
* generator could prove the handler awaits something. No constant
|
|
9758
|
+
* fits, because app operations run from ~100ms to minutes, and the
|
|
9759
|
+
* failure is silent in both directions — too short and the agent
|
|
9760
|
+
* photographs a loading screen with no controls on it, too long and
|
|
9761
|
+
* every fast press pays for the slowest one. Only the app knows.
|
|
9762
|
+
*
|
|
9763
|
+
* Absent means absent: the SDK keeps its current defaults, so a
|
|
9764
|
+
* document generated before this field existed behaves exactly as it
|
|
9765
|
+
* did. The SDK also clamps the value to its own safety ceiling — a
|
|
9766
|
+
* declared budget is a request, not a licence to hang the session.
|
|
9767
|
+
*/
|
|
9768
|
+
asyncBudgetMs: external_exports.number().int().positive().optional(),
|
|
8286
9769
|
appilotsInferred: appilotsInferredActionSchema.optional()
|
|
8287
9770
|
}).passthrough();
|
|
8288
9771
|
var screenPermissionDescriptorSchema = external_exports.object({
|
|
@@ -8465,7 +9948,7 @@ external_exports.object({
|
|
|
8465
9948
|
version: external_exports.string().default("1.0"),
|
|
8466
9949
|
content: external_exports.record(external_exports.unknown())
|
|
8467
9950
|
});
|
|
8468
|
-
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
|
|
9951
|
+
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator", "publish"]);
|
|
8469
9952
|
var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
|
|
8470
9953
|
external_exports.object({
|
|
8471
9954
|
name: external_exports.string().min(1).max(100),
|
|
@@ -8476,8 +9959,8 @@ external_exports.object({
|
|
|
8476
9959
|
scope: apiKeyScopeSchema.default("sdk"),
|
|
8477
9960
|
environment: apiKeyEnvironmentSchema.optional(),
|
|
8478
9961
|
expiresAt: external_exports.string().datetime().optional()
|
|
8479
|
-
}).refine((v) => v.scope
|
|
8480
|
-
message:
|
|
9962
|
+
}).refine((v) => v.scope === "operator" || !!v.projectId, {
|
|
9963
|
+
message: "projectId is required for SDK and publishing keys",
|
|
8481
9964
|
path: ["projectId"]
|
|
8482
9965
|
});
|
|
8483
9966
|
var boundedString = (max) => external_exports.string().max(max);
|
|
@@ -8502,10 +9985,21 @@ var snapshotInputSchema = external_exports.object({
|
|
|
8502
9985
|
type: boundedString(40).optional(),
|
|
8503
9986
|
required: external_exports.boolean().optional(),
|
|
8504
9987
|
invalid: external_exports.boolean().optional(),
|
|
9988
|
+
/**
|
|
9989
|
+
* Whether the field holds anything, as its own fact rather than an
|
|
9990
|
+
* inference over `value`. The value is the user's and is the first
|
|
9991
|
+
* thing a privacy policy withholds; the existence of a value is the
|
|
9992
|
+
* agent's and is what stops it filling the same field twice.
|
|
9993
|
+
*/
|
|
9994
|
+
filled: external_exports.boolean().optional(),
|
|
9995
|
+
/** This input has keyboard focus right now. */
|
|
9996
|
+
focused: external_exports.boolean().optional(),
|
|
8505
9997
|
inModal: external_exports.boolean().optional()
|
|
8506
9998
|
}).passthrough();
|
|
8507
9999
|
var snapshotButtonSchema = external_exports.object({
|
|
10000
|
+
controlEvidence: controlEvidenceSchema.optional(),
|
|
8508
10001
|
id: boundedString(160).optional(),
|
|
10002
|
+
dispatchable: external_exports.boolean().optional(),
|
|
8509
10003
|
provenance: identityProvenanceSchema.optional(),
|
|
8510
10004
|
/**
|
|
8511
10005
|
* False when the control is mounted but currently OUTSIDE the window —
|
|
@@ -8566,7 +10060,36 @@ var snapshotListSchema = external_exports.object({
|
|
|
8566
10060
|
*/
|
|
8567
10061
|
source: boundedString(40).optional(),
|
|
8568
10062
|
itemCount: external_exports.number().int().optional(),
|
|
10063
|
+
/**
|
|
10064
|
+
* Size of the whole collection when the app declared it, for a list
|
|
10065
|
+
* that is a WINDOW onto more data than it holds.
|
|
10066
|
+
*
|
|
10067
|
+
* `itemCount` is how many rows the list is rendering from and
|
|
10068
|
+
* `visibleItemCount` how many of those are mounted; neither can
|
|
10069
|
+
* express "there are 36 and you are looking at the first 20",
|
|
10070
|
+
* because a paginated list's `data` is the page. Absent means
|
|
10071
|
+
* unknown — never "same as itemCount".
|
|
10072
|
+
*/
|
|
10073
|
+
totalItemCount: external_exports.number().int().optional(),
|
|
8569
10074
|
visibleItemCount: external_exports.number().int().optional(),
|
|
10075
|
+
viewportItemCount: external_exports.number().int().nonnegative().optional(),
|
|
10076
|
+
exploration: external_exports.object({
|
|
10077
|
+
revision: external_exports.number().int().nonnegative(),
|
|
10078
|
+
observedItemCount: external_exports.number().int().min(0).max(5e3),
|
|
10079
|
+
observedRanges: external_exports.array(
|
|
10080
|
+
external_exports.object({
|
|
10081
|
+
start: external_exports.number().int().nonnegative(),
|
|
10082
|
+
end: external_exports.number().int().nonnegative()
|
|
10083
|
+
})
|
|
10084
|
+
).max(32),
|
|
10085
|
+
rangesTruncated: external_exports.boolean(),
|
|
10086
|
+
coverage: external_exports.enum(["partial", "all-loaded"]),
|
|
10087
|
+
pagination: external_exports.enum(["possible", "not-declared"]),
|
|
10088
|
+
scrollSteps: external_exports.number().int().nonnegative(),
|
|
10089
|
+
remainingScrollSteps: external_exports.number().int().nonnegative(),
|
|
10090
|
+
consecutiveNoProgress: external_exports.number().int().nonnegative(),
|
|
10091
|
+
lastScroll: external_exports.enum(["moved", "no-progress", "boundary", "unverified"]).optional()
|
|
10092
|
+
}).optional(),
|
|
8570
10093
|
refreshing: external_exports.boolean().optional(),
|
|
8571
10094
|
empty: external_exports.boolean().optional(),
|
|
8572
10095
|
label: boundedString(300).optional(),
|
|
@@ -8614,6 +10137,7 @@ var snapshotChoiceGroupSchema = external_exports.object({
|
|
|
8614
10137
|
}).passthrough();
|
|
8615
10138
|
var snapshotElementSchema = external_exports.object({
|
|
8616
10139
|
id: boundedString(160).optional(),
|
|
10140
|
+
dispatchable: external_exports.boolean().optional(),
|
|
8617
10141
|
role: boundedString(40).optional(),
|
|
8618
10142
|
label: boundedString(300).optional(),
|
|
8619
10143
|
texts: external_exports.array(boundedString(500)).max(50).optional(),
|
|
@@ -8657,6 +10181,34 @@ var agentSnapshotSchema = external_exports.object({
|
|
|
8657
10181
|
* it. Optional: older SDKs never clamp and never send it.
|
|
8658
10182
|
*/
|
|
8659
10183
|
truncated: external_exports.boolean().optional(),
|
|
10184
|
+
/**
|
|
10185
|
+
* What moved since the previous observation, computed on the device
|
|
10186
|
+
* because that is the only side holding both snapshots.
|
|
10187
|
+
*
|
|
10188
|
+
* Shape only — counts, booleans, and ids the app declared itself — so
|
|
10189
|
+
* it survives an observation whose content was withheld. ABSENT means
|
|
10190
|
+
* there was no previous observation to compare against; `unchanged:
|
|
10191
|
+
* true` means we compared and nothing moved, which is the strongest
|
|
10192
|
+
* evidence available that an action did nothing. The two must not be
|
|
10193
|
+
* collapsed, for the same reason `truncated` exists.
|
|
10194
|
+
*/
|
|
10195
|
+
delta: external_exports.object({
|
|
10196
|
+
routeChanged: external_exports.boolean().optional(),
|
|
10197
|
+
modalOpened: external_exports.boolean().optional(),
|
|
10198
|
+
modalClosed: external_exports.boolean().optional(),
|
|
10199
|
+
loadingStarted: external_exports.boolean().optional(),
|
|
10200
|
+
loadingFinished: external_exports.boolean().optional(),
|
|
10201
|
+
textsAdded: external_exports.number().int().nonnegative().optional(),
|
|
10202
|
+
textsRemoved: external_exports.number().int().nonnegative().optional(),
|
|
10203
|
+
buttonsAddedIndices: external_exports.array(external_exports.number().int().nonnegative()).max(12).optional(),
|
|
10204
|
+
buttonsRemoved: external_exports.number().int().nonnegative().optional(),
|
|
10205
|
+
visibleRowsDelta: external_exports.number().int().optional(),
|
|
10206
|
+
totalRowsDelta: external_exports.number().int().optional(),
|
|
10207
|
+
fieldsNewlyFilled: external_exports.array(boundedString(160)).max(12).optional(),
|
|
10208
|
+
fieldsCleared: external_exports.array(boundedString(160)).max(12).optional(),
|
|
10209
|
+
invalidAppeared: external_exports.boolean().optional(),
|
|
10210
|
+
unchanged: external_exports.boolean().optional()
|
|
10211
|
+
}).passthrough().optional(),
|
|
8660
10212
|
/**
|
|
8661
10213
|
* How many of this screen's controls the client could name, split by
|
|
8662
10214
|
* `identityProvenance`. Diagnostic only — the relay never grounds an
|
|
@@ -8675,6 +10227,10 @@ var agentSnapshotSchema = external_exports.object({
|
|
|
8675
10227
|
}).passthrough().optional()
|
|
8676
10228
|
}).passthrough();
|
|
8677
10229
|
var agentContextSchema = external_exports.object({
|
|
10230
|
+
missionProtocol: external_exports.literal(1).optional(),
|
|
10231
|
+
missionId: boundedString(160).optional(),
|
|
10232
|
+
/** Preferred supported device language, reported by the SDK independently of map/UI labels. */
|
|
10233
|
+
deviceLocale: external_exports.enum(SUPPORTED_APPILOTS_LOCALES).optional(),
|
|
8678
10234
|
/**
|
|
8679
10235
|
* Client platform this observation was captured on. Optional and
|
|
8680
10236
|
* additive (see `clientPlatformSchema`) — absent means
|
|
@@ -8690,7 +10246,19 @@ var agentContextSchema = external_exports.object({
|
|
|
8690
10246
|
rootRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
|
|
8691
10247
|
currentRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
|
|
8692
10248
|
routeNames: external_exports.array(boundedString(200)).max(500).optional(),
|
|
8693
|
-
canGoBack: external_exports.boolean().optional()
|
|
10249
|
+
canGoBack: external_exports.boolean().optional(),
|
|
10250
|
+
/**
|
|
10251
|
+
* The stack a back press pops through, oldest first, ending on
|
|
10252
|
+
* the current screen. `canGoBack` says a back exists; this says
|
|
10253
|
+
* where it goes.
|
|
10254
|
+
*/
|
|
10255
|
+
backStack: external_exports.array(boundedString(200)).max(50).optional(),
|
|
10256
|
+
/**
|
|
10257
|
+
* Screens the session has been on, oldest first. Names only —
|
|
10258
|
+
* params carry record ids and often personal data, and a route
|
|
10259
|
+
* name is structure.
|
|
10260
|
+
*/
|
|
10261
|
+
visited: external_exports.array(boundedString(200)).max(8).optional()
|
|
8694
10262
|
}).passthrough().optional(),
|
|
8695
10263
|
screenMetadata: external_exports.object({
|
|
8696
10264
|
name: boundedString(200).optional(),
|
|
@@ -8796,7 +10364,7 @@ var formFillPayloadSchema = external_exports.object({
|
|
|
8796
10364
|
submitAfterFill: external_exports.boolean().optional()
|
|
8797
10365
|
}).passthrough();
|
|
8798
10366
|
var uiInteractionPayloadSchema = external_exports.object({
|
|
8799
|
-
action: external_exports.enum(["press", "longPress", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
|
|
10367
|
+
action: external_exports.enum(["press", "longPress", "toggle", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
|
|
8800
10368
|
// `targetId` is the canonical server/LLM field. SDK runtimes still
|
|
8801
10369
|
// accept `componentId` as a compatibility alias.
|
|
8802
10370
|
targetId: external_exports.string().min(1),
|
|
@@ -8913,6 +10481,12 @@ var actionDiagnoseSchema = external_exports.object({
|
|
|
8913
10481
|
requiresUserInput: external_exports.boolean().optional()
|
|
8914
10482
|
});
|
|
8915
10483
|
var actionResultSchema = external_exports.object({
|
|
10484
|
+
nativeConfirmation: external_exports.object({
|
|
10485
|
+
title: external_exports.string().max(300),
|
|
10486
|
+
message: external_exports.string().max(1e3).optional(),
|
|
10487
|
+
buttonLabel: external_exports.string().max(160).optional(),
|
|
10488
|
+
handlerCompleted: external_exports.boolean()
|
|
10489
|
+
}).optional(),
|
|
8916
10490
|
actionId: external_exports.string(),
|
|
8917
10491
|
type: external_exports.string(),
|
|
8918
10492
|
success: external_exports.boolean(),
|
|
@@ -8964,8 +10538,15 @@ external_exports.object({
|
|
|
8964
10538
|
// issue #169 — the observation the server grounds targets against, now
|
|
8965
10539
|
// validated + bounded at the border instead of z.record(z.unknown()).
|
|
8966
10540
|
context: agentContextSchema.optional(),
|
|
8967
|
-
/**
|
|
8968
|
-
|
|
10541
|
+
/**
|
|
10542
|
+
* Hop counter — server enforces a cap to prevent runaway loops.
|
|
10543
|
+
* The max here must stay ABOVE the server's `MAX_AGENT_HOPS` (10 since
|
|
10544
|
+
* #503, apps/api/src/modules/agents/routes.ts): the first over-budget
|
|
10545
|
+
* hop has to get through validation so the route can answer it with
|
|
10546
|
+
* the friendly automation-limit message instead of a 422. The +3
|
|
10547
|
+
* headroom mirrors what 7/10 was before the recalibration.
|
|
10548
|
+
*/
|
|
10549
|
+
hop: external_exports.number().int().min(1).max(13).optional()
|
|
8969
10550
|
});
|
|
8970
10551
|
var agentAccessLevelSchema = external_exports.enum(["read", "write", "none"]);
|
|
8971
10552
|
var screenPermissionSchema = external_exports.object({
|
|
@@ -9033,7 +10614,7 @@ external_exports.object({
|
|
|
9033
10614
|
/** null = remove webhook, undefined = leave unchanged. */
|
|
9034
10615
|
budgetWebhookUrl: external_exports.string().url().nullable().optional()
|
|
9035
10616
|
}).strict();
|
|
9036
|
-
var localeSchema = external_exports.enum(
|
|
10617
|
+
var localeSchema = external_exports.enum(SUPPORTED_APPILOTS_LOCALES);
|
|
9037
10618
|
var hexColorSchema = external_exports.string().regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
|
|
9038
10619
|
message: "Must be a hex color like #6366f1"
|
|
9039
10620
|
});
|
|
@@ -9071,15 +10652,18 @@ var themeTokensSchema = external_exports.object({
|
|
|
9071
10652
|
*/
|
|
9072
10653
|
mode: external_exports.enum(["auto", "light", "dark"]).optional()
|
|
9073
10654
|
}).strict();
|
|
10655
|
+
var imageSourceField = external_exports.string().max(500).refine(isSafeImageUrl, {
|
|
10656
|
+
message: "Only http(s) URLs, base64 image data URIs, emoji or asset ids are allowed here"
|
|
10657
|
+
});
|
|
9074
10658
|
external_exports.object({
|
|
9075
10659
|
/** Tone / persona instructions appended to the system prompt. */
|
|
9076
10660
|
personaPrompt: external_exports.string().max(2048).nullable(),
|
|
9077
10661
|
/** Display name in the chat header (e.g. "Aria"). */
|
|
9078
10662
|
assistantName: external_exports.string().max(60).nullable(),
|
|
9079
10663
|
/** URL or remote asset id for the avatar shown next to assistant turns. */
|
|
9080
|
-
assistantAvatar:
|
|
10664
|
+
assistantAvatar: imageSourceField.nullable(),
|
|
9081
10665
|
/** Emoji or image URL for the empty-state icon. Auto-detected by prefix. */
|
|
9082
|
-
emptyStateIcon:
|
|
10666
|
+
emptyStateIcon: imageSourceField.nullable(),
|
|
9083
10667
|
/** First-message text shown in the empty state. */
|
|
9084
10668
|
welcomeMessage: external_exports.string().max(500).nullable(),
|
|
9085
10669
|
/** Title rendered at the top of the chat. */
|
|
@@ -9093,20 +10677,20 @@ external_exports.object({
|
|
|
9093
10677
|
/** Chat-open FAB background color. Null falls back to theme.colors.primary. */
|
|
9094
10678
|
triggerButtonColor: hexColorSchema.nullable(),
|
|
9095
10679
|
/** Image URL rendered inside the FAB instead of the default chat-bubble icon. */
|
|
9096
|
-
triggerButtonImageUrl:
|
|
10680
|
+
triggerButtonImageUrl: imageSourceField.nullable()
|
|
9097
10681
|
});
|
|
9098
10682
|
external_exports.object({
|
|
9099
10683
|
personaPrompt: external_exports.string().max(2048).nullable().optional(),
|
|
9100
10684
|
assistantName: external_exports.string().max(60).nullable().optional(),
|
|
9101
|
-
assistantAvatar:
|
|
9102
|
-
emptyStateIcon:
|
|
10685
|
+
assistantAvatar: imageSourceField.nullable().optional(),
|
|
10686
|
+
emptyStateIcon: imageSourceField.nullable().optional(),
|
|
9103
10687
|
welcomeMessage: external_exports.string().max(500).nullable().optional(),
|
|
9104
10688
|
chatTitle: external_exports.string().max(60).nullable().optional(),
|
|
9105
10689
|
poweredByVisible: external_exports.boolean().optional(),
|
|
9106
10690
|
theme: themeTokensSchema.nullable().optional(),
|
|
9107
10691
|
defaultLocale: localeSchema.nullable().optional(),
|
|
9108
10692
|
triggerButtonColor: hexColorSchema.nullable().optional(),
|
|
9109
|
-
triggerButtonImageUrl:
|
|
10693
|
+
triggerButtonImageUrl: imageSourceField.nullable().optional()
|
|
9110
10694
|
}).strict();
|
|
9111
10695
|
var sandboxObservationSchema = external_exports.object({
|
|
9112
10696
|
route: external_exports.string().max(200).optional(),
|
|
@@ -9126,7 +10710,19 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
9126
10710
|
type: external_exports.string().max(40).optional(),
|
|
9127
10711
|
placeholder: external_exports.string().max(200).optional(),
|
|
9128
10712
|
/** Inline validation state (SDK snapshot field). */
|
|
9129
|
-
invalid: external_exports.boolean().optional()
|
|
10713
|
+
invalid: external_exports.boolean().optional(),
|
|
10714
|
+
/**
|
|
10715
|
+
* Whether the field holds anything, as its own fact rather than
|
|
10716
|
+
* an inference over `value`.
|
|
10717
|
+
*
|
|
10718
|
+
* Here because the eval posts through this schema: a fact the
|
|
10719
|
+
* SDK produces and this contract has no word for is a fact the
|
|
10720
|
+
* corpus can never grade the agent on. `invalid` and `required`
|
|
10721
|
+
* were already here; these two were the half that was missing.
|
|
10722
|
+
*/
|
|
10723
|
+
filled: external_exports.boolean().optional(),
|
|
10724
|
+
/** This input has keyboard focus right now. */
|
|
10725
|
+
focused: external_exports.boolean().optional()
|
|
9130
10726
|
})
|
|
9131
10727
|
).max(100).optional(),
|
|
9132
10728
|
buttons: external_exports.array(
|
|
@@ -9605,7 +11201,7 @@ var manifestSchema = external_exports.object({
|
|
|
9605
11201
|
navigation: navigationGraphSchema.partial().optional()
|
|
9606
11202
|
}).passthrough();
|
|
9607
11203
|
async function loadManifest(rootDir, manifestPath) {
|
|
9608
|
-
const resolvedPath =
|
|
11204
|
+
const resolvedPath = path2__default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
|
|
9609
11205
|
let raw;
|
|
9610
11206
|
try {
|
|
9611
11207
|
raw = await readFile(resolvedPath, "utf-8");
|
|
@@ -9688,6 +11284,12 @@ function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
|
|
|
9688
11284
|
};
|
|
9689
11285
|
}
|
|
9690
11286
|
|
|
11287
|
+
// src/generators/checksum.ts
|
|
11288
|
+
function serializeForChecksum(document) {
|
|
11289
|
+
const { generatedAt: _generatedAt, ...content } = document;
|
|
11290
|
+
return JSON.stringify(content, null, 2);
|
|
11291
|
+
}
|
|
11292
|
+
|
|
9691
11293
|
// src/pipeline/enrichment.ts
|
|
9692
11294
|
function enrichScreenForAgent(screen) {
|
|
9693
11295
|
const targets = mergeTargets([
|
|
@@ -9764,7 +11366,10 @@ function mergeTargets(targets) {
|
|
|
9764
11366
|
for (const target of targets) {
|
|
9765
11367
|
if (!target.id) continue;
|
|
9766
11368
|
const existing = byId.get(target.id);
|
|
9767
|
-
byId.set(
|
|
11369
|
+
byId.set(
|
|
11370
|
+
target.id,
|
|
11371
|
+
existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target
|
|
11372
|
+
);
|
|
9768
11373
|
}
|
|
9769
11374
|
return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
9770
11375
|
}
|
|
@@ -9809,7 +11414,10 @@ function synthesizeFlows(screen, targets) {
|
|
|
9809
11414
|
intent: "destructive_action",
|
|
9810
11415
|
steps: [
|
|
9811
11416
|
{ type: "press", target: action.id, label: action.label },
|
|
9812
|
-
{
|
|
11417
|
+
{
|
|
11418
|
+
type: "confirm",
|
|
11419
|
+
description: "Wait for native or custom confirmation before continuing"
|
|
11420
|
+
},
|
|
9813
11421
|
{ type: "wait", description: describeWait(action) }
|
|
9814
11422
|
],
|
|
9815
11423
|
waitPolicy: waitPolicyForAction(action),
|
|
@@ -9824,10 +11432,19 @@ function synthesizeFlows(screen, targets) {
|
|
|
9824
11432
|
title: `Act on an item in ${collection.id}`,
|
|
9825
11433
|
intent: "list_action",
|
|
9826
11434
|
steps: [
|
|
9827
|
-
{
|
|
9828
|
-
|
|
11435
|
+
{
|
|
11436
|
+
type: "choose-list-item",
|
|
11437
|
+
target: collection.id,
|
|
11438
|
+
description: "Resolve the user reference to a visible or searchable row"
|
|
11439
|
+
},
|
|
11440
|
+
{
|
|
11441
|
+
type: "press",
|
|
11442
|
+
description: collection.rowAction?.description ?? "Open the row action"
|
|
11443
|
+
}
|
|
9829
11444
|
],
|
|
9830
|
-
waitPolicy: {
|
|
11445
|
+
waitPolicy: {
|
|
11446
|
+
expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback"
|
|
11447
|
+
}
|
|
9831
11448
|
});
|
|
9832
11449
|
}
|
|
9833
11450
|
}
|
|
@@ -9835,25 +11452,32 @@ function synthesizeFlows(screen, targets) {
|
|
|
9835
11452
|
}
|
|
9836
11453
|
function waitPolicyForAction(action) {
|
|
9837
11454
|
const expectedOutcome = action.successSignal?.type === "goBack" ? "goBack" : action.appilotsInferred?.expectedOutcome ?? (action.targetScreen ? "navigation" : void 0);
|
|
9838
|
-
const signals = [action.successSignal, action.failureSignal].filter(
|
|
11455
|
+
const signals = [action.successSignal, action.failureSignal].filter(
|
|
11456
|
+
Boolean
|
|
11457
|
+
);
|
|
11458
|
+
const maxMs = action.asyncBudgetMs ?? (action.appilotsInferred?.isAsyncTrigger ? 1e4 : void 0);
|
|
9839
11459
|
return {
|
|
9840
11460
|
...expectedOutcome ? { expectedOutcome } : {},
|
|
9841
11461
|
...signals && signals.length > 0 ? { signals } : {},
|
|
9842
|
-
...
|
|
11462
|
+
...maxMs !== void 0 ? { maxMs } : {}
|
|
9843
11463
|
};
|
|
9844
11464
|
}
|
|
9845
11465
|
function describeWait(action) {
|
|
9846
11466
|
if (action.successSignal?.description) return action.successSignal.description;
|
|
9847
|
-
if (action.successSignal?.type === "goBack")
|
|
11467
|
+
if (action.successSignal?.type === "goBack")
|
|
11468
|
+
return "Wait for the app to return to the previous screen";
|
|
9848
11469
|
if (action.targetScreen) return `Wait for navigation to ${action.targetScreen}`;
|
|
9849
|
-
if (action.appilotsInferred?.expectedOutcome)
|
|
11470
|
+
if (action.appilotsInferred?.expectedOutcome)
|
|
11471
|
+
return `Wait for ${action.appilotsInferred.expectedOutcome}`;
|
|
9850
11472
|
return "Wait for the UI to settle";
|
|
9851
11473
|
}
|
|
9852
11474
|
function synthesizeAgentHints(screen, targets, flows) {
|
|
9853
11475
|
const preferredTargets = targets.filter((target) => ["submit", "button", "list"].includes(target.role)).slice(0, 8).map((target) => target.id);
|
|
9854
11476
|
const commonTasks = flows.slice(0, 6).map((flow) => flow.title);
|
|
9855
11477
|
const safetyNotes = screen.actions.filter((action) => action.destructive || action.requiresConfirmation).map((action) => `${action.id} requires confirmation`);
|
|
9856
|
-
const firstAsyncAction = screen.actions.find(
|
|
11478
|
+
const firstAsyncAction = screen.actions.find(
|
|
11479
|
+
(action) => action.appilotsInferred?.isAsyncTrigger || action.asyncBudgetMs !== void 0
|
|
11480
|
+
);
|
|
9857
11481
|
const hints = {
|
|
9858
11482
|
primaryGoal: synthesizePrimaryGoal(screen),
|
|
9859
11483
|
commonTasks,
|
|
@@ -9880,7 +11504,9 @@ function synthesizePrimaryGoal(screen) {
|
|
|
9880
11504
|
}
|
|
9881
11505
|
const fieldCount = uniqueFields.size;
|
|
9882
11506
|
if (fieldCount > 0) {
|
|
9883
|
-
const requiredCount = Array.from(uniqueFields.values()).filter(
|
|
11507
|
+
const requiredCount = Array.from(uniqueFields.values()).filter(
|
|
11508
|
+
(field) => field.required
|
|
11509
|
+
).length;
|
|
9884
11510
|
const fieldLabel = fieldCount === 1 ? "field" : "fields";
|
|
9885
11511
|
goals.push(
|
|
9886
11512
|
requiredCount > 0 ? `Complete a form with ${fieldCount} ${fieldLabel} (${requiredCount} required)` : `Complete a form with ${fieldCount} ${fieldLabel}`
|
|
@@ -9894,7 +11520,9 @@ function synthesizePrimaryGoal(screen) {
|
|
|
9894
11520
|
if (submitCount > 0) {
|
|
9895
11521
|
goals.push(`Submit ${submitCount === 1 ? "the primary form" : `${submitCount} forms/actions`}`);
|
|
9896
11522
|
}
|
|
9897
|
-
const asyncCount = screen.actions.filter(
|
|
11523
|
+
const asyncCount = screen.actions.filter(
|
|
11524
|
+
(action) => action.appilotsInferred?.isAsyncTrigger
|
|
11525
|
+
).length;
|
|
9898
11526
|
if (asyncCount > 0) {
|
|
9899
11527
|
goals.push(`Wait for ${asyncCount === 1 ? "async feedback" : "async action feedback"}`);
|
|
9900
11528
|
}
|
|
@@ -9969,7 +11597,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
9969
11597
|
const agentReadyScreens = mergedScreens.map(
|
|
9970
11598
|
(screen) => enrichScreenForAgent({
|
|
9971
11599
|
...screen,
|
|
9972
|
-
filePath: screen.filePath ?
|
|
11600
|
+
filePath: screen.filePath ? path2__default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
|
|
9973
11601
|
})
|
|
9974
11602
|
);
|
|
9975
11603
|
const projectInfo = await this.getProjectInfo();
|
|
@@ -9993,19 +11621,33 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
9993
11621
|
}
|
|
9994
11622
|
};
|
|
9995
11623
|
const serialized = JSON.stringify(document, null, 2);
|
|
9996
|
-
const checksum = this.calculateChecksum(
|
|
9997
|
-
const filePath =
|
|
11624
|
+
const checksum = this.calculateChecksum(serializeForChecksum(document));
|
|
11625
|
+
const filePath = path2__default.resolve(outputDir, `mcp-document.${this.options.format}`);
|
|
9998
11626
|
await writeFile(filePath, serialized, "utf-8");
|
|
9999
11627
|
console.log(`[MCPGenerator] Document written to: ${filePath}`);
|
|
10000
|
-
const
|
|
11628
|
+
const controlFiles = analyzed.controlEvidenceFiles ?? {};
|
|
11629
|
+
await writeFile(
|
|
11630
|
+
path2__default.resolve(outputDir, "control-evidence.json"),
|
|
11631
|
+
JSON.stringify({ version: 1, files: controlFiles }, null, 2),
|
|
11632
|
+
"utf-8"
|
|
11633
|
+
);
|
|
11634
|
+
const checksumFilePath = path2__default.resolve(outputDir, ".appilots-checksum");
|
|
10001
11635
|
await writeFile(checksumFilePath, checksum, "utf-8");
|
|
10002
11636
|
console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
|
|
11637
|
+
const evidenceCount = Object.values(controlFiles).reduce(
|
|
11638
|
+
(sum, entries) => sum + entries.length,
|
|
11639
|
+
0
|
|
11640
|
+
);
|
|
11641
|
+
console.log(
|
|
11642
|
+
`[MCPGenerator] Source evidence: ${evidenceCount} icon controls across ${Object.keys(controlFiles).length} files (runtime binding required)`
|
|
11643
|
+
);
|
|
10003
11644
|
console.log("[MCPGenerator] Generation complete!");
|
|
10004
11645
|
return {
|
|
10005
11646
|
document,
|
|
10006
11647
|
filePath,
|
|
10007
11648
|
format: this.options.format,
|
|
10008
|
-
checksum
|
|
11649
|
+
checksum,
|
|
11650
|
+
...analyzed.diagnostics ? { diagnostics: analyzed.diagnostics } : {}
|
|
10009
11651
|
};
|
|
10010
11652
|
}
|
|
10011
11653
|
/**
|
|
@@ -10017,7 +11659,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10017
11659
|
* after calling `generate()`.
|
|
10018
11660
|
*/
|
|
10019
11661
|
static async readPreviousChecksum(outputDir) {
|
|
10020
|
-
const checksumFilePath =
|
|
11662
|
+
const checksumFilePath = path2__default.resolve(outputDir, ".appilots-checksum");
|
|
10021
11663
|
try {
|
|
10022
11664
|
const content = await readFile(checksumFilePath, "utf-8");
|
|
10023
11665
|
return content.trim() || null;
|
|
@@ -10036,7 +11678,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10036
11678
|
*/
|
|
10037
11679
|
async getProjectInfo() {
|
|
10038
11680
|
try {
|
|
10039
|
-
const packageJsonPath =
|
|
11681
|
+
const packageJsonPath = path2__default.resolve(this.analyzerConfig.rootDir, "package.json");
|
|
10040
11682
|
const packageJsonContent = await readFile(packageJsonPath, "utf-8");
|
|
10041
11683
|
const packageJson = JSON.parse(packageJsonContent);
|
|
10042
11684
|
return {
|
|
@@ -10064,7 +11706,8 @@ var KNOWN_CONFIG_KEYS = [
|
|
|
10064
11706
|
"navigationExclude",
|
|
10065
11707
|
"platform",
|
|
10066
11708
|
"manifestPath",
|
|
10067
|
-
"eval"
|
|
11709
|
+
"eval",
|
|
11710
|
+
"knowledge"
|
|
10068
11711
|
];
|
|
10069
11712
|
var KEY_ALIASES = {
|
|
10070
11713
|
apiUrl: "serverUrl",
|
|
@@ -10119,12 +11762,12 @@ function getEnvOverrides(env = process.env) {
|
|
|
10119
11762
|
return trimmed ? trimmed : void 0;
|
|
10120
11763
|
};
|
|
10121
11764
|
return {
|
|
10122
|
-
apiKey: clean(env.APPILOTS_API_KEY),
|
|
11765
|
+
apiKey: clean(env.APPILOTS_PUBLISH_KEY) ?? clean(env.APPILOTS_API_KEY),
|
|
10123
11766
|
projectId: clean(env.APPILOTS_PROJECT_ID),
|
|
10124
11767
|
serverUrl: clean(env.APPILOTS_SERVER_URL)
|
|
10125
11768
|
};
|
|
10126
11769
|
}
|
|
10127
|
-
function loadConfig(onWarn) {
|
|
11770
|
+
function loadConfig(onWarn, options = {}) {
|
|
10128
11771
|
const configPath = getConfigPath();
|
|
10129
11772
|
const env = getEnvOverrides();
|
|
10130
11773
|
let fileConfig = {};
|
|
@@ -10163,6 +11806,10 @@ function loadConfig(onWarn) {
|
|
|
10163
11806
|
};
|
|
10164
11807
|
const validation = validateConfig(merged);
|
|
10165
11808
|
validation.warnings.unshift(...legacyWarnings);
|
|
11809
|
+
if (options.requireApiKey === false) {
|
|
11810
|
+
validation.errors = validation.errors.filter((e) => !e.startsWith("apiKey"));
|
|
11811
|
+
validation.valid = validation.errors.length === 0;
|
|
11812
|
+
}
|
|
10166
11813
|
if (fileConfig.serverUrl === void 0 && env.serverUrl === void 0) {
|
|
10167
11814
|
for (const key of Object.keys(fileConfig)) {
|
|
10168
11815
|
if (KNOWN_CONFIG_KEYS.includes(key)) continue;
|
|
@@ -10208,7 +11855,8 @@ function saveConfig(dir, config) {
|
|
|
10208
11855
|
navigationExclude: config.navigationExclude || existingConfig?.navigationExclude,
|
|
10209
11856
|
platform: config.platform || existingConfig?.platform,
|
|
10210
11857
|
manifestPath: config.manifestPath || existingConfig?.manifestPath,
|
|
10211
|
-
eval: config.eval || existingConfig?.eval
|
|
11858
|
+
eval: config.eval || existingConfig?.eval,
|
|
11859
|
+
knowledge: config.knowledge || existingConfig?.knowledge
|
|
10212
11860
|
};
|
|
10213
11861
|
try {
|
|
10214
11862
|
writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
|
|
@@ -10301,6 +11949,19 @@ function validateConfig(config) {
|
|
|
10301
11949
|
if (config.manifestPath !== void 0 && typeof config.manifestPath !== "string") {
|
|
10302
11950
|
errors.push("manifestPath must be a string");
|
|
10303
11951
|
}
|
|
11952
|
+
if (config.knowledge !== void 0) {
|
|
11953
|
+
if (typeof config.knowledge !== "object" || config.knowledge === null || Array.isArray(config.knowledge)) {
|
|
11954
|
+
errors.push("knowledge must be an object");
|
|
11955
|
+
} else {
|
|
11956
|
+
const kc = config.knowledge;
|
|
11957
|
+
for (const field of ["sources", "exclude"]) {
|
|
11958
|
+
const value = kc[field];
|
|
11959
|
+
if (value !== void 0 && (!Array.isArray(value) || !value.every((item) => typeof item === "string"))) {
|
|
11960
|
+
errors.push(`knowledge.${field} must be an array of strings`);
|
|
11961
|
+
}
|
|
11962
|
+
}
|
|
11963
|
+
}
|
|
11964
|
+
}
|
|
10304
11965
|
if (config.eval !== void 0) {
|
|
10305
11966
|
if (typeof config.eval !== "object" || config.eval === null || Array.isArray(config.eval)) {
|
|
10306
11967
|
errors.push("eval must be an object");
|
|
@@ -10577,6 +12238,48 @@ var AppilotsAPIClient = class {
|
|
|
10577
12238
|
};
|
|
10578
12239
|
}
|
|
10579
12240
|
}
|
|
12241
|
+
/**
|
|
12242
|
+
* Syncs knowledge documents with the Appilots backend.
|
|
12243
|
+
*
|
|
12244
|
+
* @param documents Array of documents to sync (filename, base64 content, mimeType, checksum)
|
|
12245
|
+
* @returns KnowledgeSyncResult with counts of uploaded, skipped, and errored docs
|
|
12246
|
+
*/
|
|
12247
|
+
async knowledgeSync(documents) {
|
|
12248
|
+
try {
|
|
12249
|
+
const response = await this.request(`${this.baseUrl}/cli/knowledge/sync`, {
|
|
12250
|
+
method: "POST",
|
|
12251
|
+
headers: {
|
|
12252
|
+
"Content-Type": "application/json",
|
|
12253
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
12254
|
+
},
|
|
12255
|
+
body: JSON.stringify({ documents })
|
|
12256
|
+
});
|
|
12257
|
+
if (!response.ok) {
|
|
12258
|
+
const errorData = await response.json().catch(() => ({}));
|
|
12259
|
+
return {
|
|
12260
|
+
success: false,
|
|
12261
|
+
uploaded: 0,
|
|
12262
|
+
skipped: 0,
|
|
12263
|
+
errors: 0,
|
|
12264
|
+
error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`)
|
|
12265
|
+
};
|
|
12266
|
+
}
|
|
12267
|
+
const json = await response.json();
|
|
12268
|
+
const inner = json.data ?? json;
|
|
12269
|
+
return {
|
|
12270
|
+
success: true,
|
|
12271
|
+
...inner
|
|
12272
|
+
};
|
|
12273
|
+
} catch (error) {
|
|
12274
|
+
return {
|
|
12275
|
+
success: false,
|
|
12276
|
+
uploaded: 0,
|
|
12277
|
+
skipped: 0,
|
|
12278
|
+
errors: 0,
|
|
12279
|
+
error: error instanceof Error ? error.message : "Failed to sync knowledge with Appilots API"
|
|
12280
|
+
};
|
|
12281
|
+
}
|
|
12282
|
+
}
|
|
10580
12283
|
/**
|
|
10581
12284
|
* Checks if the Appilots API server is healthy
|
|
10582
12285
|
*
|
|
@@ -10595,7 +12298,7 @@ var AppilotsAPIClient = class {
|
|
|
10595
12298
|
};
|
|
10596
12299
|
|
|
10597
12300
|
// src/version.ts
|
|
10598
|
-
var CLI_VERSION = "0.
|
|
12301
|
+
var CLI_VERSION = "0.13.0";
|
|
10599
12302
|
|
|
10600
12303
|
export { AppilotsAPIClient, CLI_VERSION, ComponentAnalyzer, DEFAULT_MANIFEST_FILENAME, DEFAULT_WEB_SCREEN_PATTERNS, FormAnalyzer, GenericPlatformAnalyzer, MCPGenerator, NavigationAnalyzer, ReactNativePlatformAnalyzer, ReactWebPlatformAnalyzer, ScreenAnalyzer, WebNavigationAnalyzer, WebScreenAnalyzer, formatMetadataWarnings, getConfigPath, getEnvOverrides, lintActionMetadata, loadConfig, loadManifest, mergeManifestNavigation, mergeManifestScreens, resolvePathToScreen, saveConfig, screenNameFromPath, validateConfig };
|
|
10601
12304
|
//# sourceMappingURL=index.mjs.map
|