@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/cli/index.js
CHANGED
|
@@ -28,10 +28,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
));
|
|
29
29
|
|
|
30
30
|
// src/cli/index.ts
|
|
31
|
-
var
|
|
31
|
+
var import_commander9 = require("commander");
|
|
32
32
|
|
|
33
33
|
// src/version.ts
|
|
34
|
-
var CLI_VERSION = "0.
|
|
34
|
+
var CLI_VERSION = "0.13.0";
|
|
35
35
|
|
|
36
36
|
// src/config/index.ts
|
|
37
37
|
var import_fs = require("fs");
|
|
@@ -52,7 +52,8 @@ var KNOWN_CONFIG_KEYS = [
|
|
|
52
52
|
"navigationExclude",
|
|
53
53
|
"platform",
|
|
54
54
|
"manifestPath",
|
|
55
|
-
"eval"
|
|
55
|
+
"eval",
|
|
56
|
+
"knowledge"
|
|
56
57
|
];
|
|
57
58
|
var KEY_ALIASES = {
|
|
58
59
|
apiUrl: "serverUrl",
|
|
@@ -107,12 +108,12 @@ function getEnvOverrides(env = process.env) {
|
|
|
107
108
|
return trimmed ? trimmed : void 0;
|
|
108
109
|
};
|
|
109
110
|
return {
|
|
110
|
-
apiKey: clean(env.APPILOTS_API_KEY),
|
|
111
|
+
apiKey: clean(env.APPILOTS_PUBLISH_KEY) ?? clean(env.APPILOTS_API_KEY),
|
|
111
112
|
projectId: clean(env.APPILOTS_PROJECT_ID),
|
|
112
113
|
serverUrl: clean(env.APPILOTS_SERVER_URL)
|
|
113
114
|
};
|
|
114
115
|
}
|
|
115
|
-
function loadConfig(onWarn) {
|
|
116
|
+
function loadConfig(onWarn, options = {}) {
|
|
116
117
|
const configPath = getConfigPath();
|
|
117
118
|
const env = getEnvOverrides();
|
|
118
119
|
let fileConfig = {};
|
|
@@ -151,6 +152,10 @@ function loadConfig(onWarn) {
|
|
|
151
152
|
};
|
|
152
153
|
const validation = validateConfig(merged);
|
|
153
154
|
validation.warnings.unshift(...legacyWarnings);
|
|
155
|
+
if (options.requireApiKey === false) {
|
|
156
|
+
validation.errors = validation.errors.filter((e) => !e.startsWith("apiKey"));
|
|
157
|
+
validation.valid = validation.errors.length === 0;
|
|
158
|
+
}
|
|
154
159
|
if (fileConfig.serverUrl === void 0 && env.serverUrl === void 0) {
|
|
155
160
|
for (const key of Object.keys(fileConfig)) {
|
|
156
161
|
if (KNOWN_CONFIG_KEYS.includes(key)) continue;
|
|
@@ -196,7 +201,8 @@ function saveConfig(dir, config) {
|
|
|
196
201
|
navigationExclude: config.navigationExclude || existingConfig?.navigationExclude,
|
|
197
202
|
platform: config.platform || existingConfig?.platform,
|
|
198
203
|
manifestPath: config.manifestPath || existingConfig?.manifestPath,
|
|
199
|
-
eval: config.eval || existingConfig?.eval
|
|
204
|
+
eval: config.eval || existingConfig?.eval,
|
|
205
|
+
knowledge: config.knowledge || existingConfig?.knowledge
|
|
200
206
|
};
|
|
201
207
|
try {
|
|
202
208
|
(0, import_fs.writeFileSync)(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
|
|
@@ -289,6 +295,19 @@ function validateConfig(config) {
|
|
|
289
295
|
if (config.manifestPath !== void 0 && typeof config.manifestPath !== "string") {
|
|
290
296
|
errors.push("manifestPath must be a string");
|
|
291
297
|
}
|
|
298
|
+
if (config.knowledge !== void 0) {
|
|
299
|
+
if (typeof config.knowledge !== "object" || config.knowledge === null || Array.isArray(config.knowledge)) {
|
|
300
|
+
errors.push("knowledge must be an object");
|
|
301
|
+
} else {
|
|
302
|
+
const kc = config.knowledge;
|
|
303
|
+
for (const field of ["sources", "exclude"]) {
|
|
304
|
+
const value = kc[field];
|
|
305
|
+
if (value !== void 0 && (!Array.isArray(value) || !value.every((item) => typeof item === "string"))) {
|
|
306
|
+
errors.push(`knowledge.${field} must be an array of strings`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
292
311
|
if (config.eval !== void 0) {
|
|
293
312
|
if (typeof config.eval !== "object" || config.eval === null || Array.isArray(config.eval)) {
|
|
294
313
|
errors.push("eval must be an object");
|
|
@@ -630,6 +649,48 @@ var AppilotsAPIClient = class {
|
|
|
630
649
|
};
|
|
631
650
|
}
|
|
632
651
|
}
|
|
652
|
+
/**
|
|
653
|
+
* Syncs knowledge documents with the Appilots backend.
|
|
654
|
+
*
|
|
655
|
+
* @param documents Array of documents to sync (filename, base64 content, mimeType, checksum)
|
|
656
|
+
* @returns KnowledgeSyncResult with counts of uploaded, skipped, and errored docs
|
|
657
|
+
*/
|
|
658
|
+
async knowledgeSync(documents) {
|
|
659
|
+
try {
|
|
660
|
+
const response = await this.request(`${this.baseUrl}/cli/knowledge/sync`, {
|
|
661
|
+
method: "POST",
|
|
662
|
+
headers: {
|
|
663
|
+
"Content-Type": "application/json",
|
|
664
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
665
|
+
},
|
|
666
|
+
body: JSON.stringify({ documents })
|
|
667
|
+
});
|
|
668
|
+
if (!response.ok) {
|
|
669
|
+
const errorData = await response.json().catch(() => ({}));
|
|
670
|
+
return {
|
|
671
|
+
success: false,
|
|
672
|
+
uploaded: 0,
|
|
673
|
+
skipped: 0,
|
|
674
|
+
errors: 0,
|
|
675
|
+
error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`)
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
const json = await response.json();
|
|
679
|
+
const inner = json.data ?? json;
|
|
680
|
+
return {
|
|
681
|
+
success: true,
|
|
682
|
+
...inner
|
|
683
|
+
};
|
|
684
|
+
} catch (error2) {
|
|
685
|
+
return {
|
|
686
|
+
success: false,
|
|
687
|
+
uploaded: 0,
|
|
688
|
+
skipped: 0,
|
|
689
|
+
errors: 0,
|
|
690
|
+
error: error2 instanceof Error ? error2.message : "Failed to sync knowledge with Appilots API"
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
}
|
|
633
694
|
/**
|
|
634
695
|
* Checks if the Appilots API server is healthy
|
|
635
696
|
*
|
|
@@ -763,12 +824,18 @@ function initCommand() {
|
|
|
763
824
|
// src/cli/commands/sync.ts
|
|
764
825
|
var import_commander2 = require("commander");
|
|
765
826
|
var import_promises5 = require("fs/promises");
|
|
766
|
-
var
|
|
827
|
+
var import_node_path7 = require("path");
|
|
767
828
|
|
|
768
829
|
// src/generators/MCPGenerator.ts
|
|
769
830
|
var import_promises4 = require("fs/promises");
|
|
770
|
-
var
|
|
771
|
-
var
|
|
831
|
+
var import_node_crypto2 = require("crypto");
|
|
832
|
+
var import_node_path5 = __toESM(require("path"));
|
|
833
|
+
|
|
834
|
+
// src/generators/checksum.ts
|
|
835
|
+
function serializeForChecksum(document) {
|
|
836
|
+
const { generatedAt: _generatedAt, ...content } = document;
|
|
837
|
+
return JSON.stringify(content, null, 2);
|
|
838
|
+
}
|
|
772
839
|
|
|
773
840
|
// src/pipeline/enrichment.ts
|
|
774
841
|
function enrichScreenForAgent(screen) {
|
|
@@ -846,7 +913,10 @@ function mergeTargets(targets) {
|
|
|
846
913
|
for (const target of targets) {
|
|
847
914
|
if (!target.id) continue;
|
|
848
915
|
const existing = byId.get(target.id);
|
|
849
|
-
byId.set(
|
|
916
|
+
byId.set(
|
|
917
|
+
target.id,
|
|
918
|
+
existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target
|
|
919
|
+
);
|
|
850
920
|
}
|
|
851
921
|
return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
852
922
|
}
|
|
@@ -891,7 +961,10 @@ function synthesizeFlows(screen, targets) {
|
|
|
891
961
|
intent: "destructive_action",
|
|
892
962
|
steps: [
|
|
893
963
|
{ type: "press", target: action.id, label: action.label },
|
|
894
|
-
{
|
|
964
|
+
{
|
|
965
|
+
type: "confirm",
|
|
966
|
+
description: "Wait for native or custom confirmation before continuing"
|
|
967
|
+
},
|
|
895
968
|
{ type: "wait", description: describeWait(action) }
|
|
896
969
|
],
|
|
897
970
|
waitPolicy: waitPolicyForAction(action),
|
|
@@ -906,10 +979,19 @@ function synthesizeFlows(screen, targets) {
|
|
|
906
979
|
title: `Act on an item in ${collection.id}`,
|
|
907
980
|
intent: "list_action",
|
|
908
981
|
steps: [
|
|
909
|
-
{
|
|
910
|
-
|
|
982
|
+
{
|
|
983
|
+
type: "choose-list-item",
|
|
984
|
+
target: collection.id,
|
|
985
|
+
description: "Resolve the user reference to a visible or searchable row"
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
type: "press",
|
|
989
|
+
description: collection.rowAction?.description ?? "Open the row action"
|
|
990
|
+
}
|
|
911
991
|
],
|
|
912
|
-
waitPolicy: {
|
|
992
|
+
waitPolicy: {
|
|
993
|
+
expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback"
|
|
994
|
+
}
|
|
913
995
|
});
|
|
914
996
|
}
|
|
915
997
|
}
|
|
@@ -917,25 +999,32 @@ function synthesizeFlows(screen, targets) {
|
|
|
917
999
|
}
|
|
918
1000
|
function waitPolicyForAction(action) {
|
|
919
1001
|
const expectedOutcome = action.successSignal?.type === "goBack" ? "goBack" : action.appilotsInferred?.expectedOutcome ?? (action.targetScreen ? "navigation" : void 0);
|
|
920
|
-
const signals = [action.successSignal, action.failureSignal].filter(
|
|
1002
|
+
const signals = [action.successSignal, action.failureSignal].filter(
|
|
1003
|
+
Boolean
|
|
1004
|
+
);
|
|
1005
|
+
const maxMs = action.asyncBudgetMs ?? (action.appilotsInferred?.isAsyncTrigger ? 1e4 : void 0);
|
|
921
1006
|
return {
|
|
922
1007
|
...expectedOutcome ? { expectedOutcome } : {},
|
|
923
1008
|
...signals && signals.length > 0 ? { signals } : {},
|
|
924
|
-
...
|
|
1009
|
+
...maxMs !== void 0 ? { maxMs } : {}
|
|
925
1010
|
};
|
|
926
1011
|
}
|
|
927
1012
|
function describeWait(action) {
|
|
928
1013
|
if (action.successSignal?.description) return action.successSignal.description;
|
|
929
|
-
if (action.successSignal?.type === "goBack")
|
|
1014
|
+
if (action.successSignal?.type === "goBack")
|
|
1015
|
+
return "Wait for the app to return to the previous screen";
|
|
930
1016
|
if (action.targetScreen) return `Wait for navigation to ${action.targetScreen}`;
|
|
931
|
-
if (action.appilotsInferred?.expectedOutcome)
|
|
1017
|
+
if (action.appilotsInferred?.expectedOutcome)
|
|
1018
|
+
return `Wait for ${action.appilotsInferred.expectedOutcome}`;
|
|
932
1019
|
return "Wait for the UI to settle";
|
|
933
1020
|
}
|
|
934
1021
|
function synthesizeAgentHints(screen, targets, flows) {
|
|
935
1022
|
const preferredTargets = targets.filter((target) => ["submit", "button", "list"].includes(target.role)).slice(0, 8).map((target) => target.id);
|
|
936
1023
|
const commonTasks = flows.slice(0, 6).map((flow) => flow.title);
|
|
937
1024
|
const safetyNotes = screen.actions.filter((action) => action.destructive || action.requiresConfirmation).map((action) => `${action.id} requires confirmation`);
|
|
938
|
-
const firstAsyncAction = screen.actions.find(
|
|
1025
|
+
const firstAsyncAction = screen.actions.find(
|
|
1026
|
+
(action) => action.appilotsInferred?.isAsyncTrigger || action.asyncBudgetMs !== void 0
|
|
1027
|
+
);
|
|
939
1028
|
const hints = {
|
|
940
1029
|
primaryGoal: synthesizePrimaryGoal(screen),
|
|
941
1030
|
commonTasks,
|
|
@@ -962,7 +1051,9 @@ function synthesizePrimaryGoal(screen) {
|
|
|
962
1051
|
}
|
|
963
1052
|
const fieldCount = uniqueFields.size;
|
|
964
1053
|
if (fieldCount > 0) {
|
|
965
|
-
const requiredCount = Array.from(uniqueFields.values()).filter(
|
|
1054
|
+
const requiredCount = Array.from(uniqueFields.values()).filter(
|
|
1055
|
+
(field) => field.required
|
|
1056
|
+
).length;
|
|
966
1057
|
const fieldLabel = fieldCount === 1 ? "field" : "fields";
|
|
967
1058
|
goals.push(
|
|
968
1059
|
requiredCount > 0 ? `Complete a form with ${fieldCount} ${fieldLabel} (${requiredCount} required)` : `Complete a form with ${fieldCount} ${fieldLabel}`
|
|
@@ -976,7 +1067,9 @@ function synthesizePrimaryGoal(screen) {
|
|
|
976
1067
|
if (submitCount > 0) {
|
|
977
1068
|
goals.push(`Submit ${submitCount === 1 ? "the primary form" : `${submitCount} forms/actions`}`);
|
|
978
1069
|
}
|
|
979
|
-
const asyncCount = screen.actions.filter(
|
|
1070
|
+
const asyncCount = screen.actions.filter(
|
|
1071
|
+
(action) => action.appilotsInferred?.isAsyncTrigger
|
|
1072
|
+
).length;
|
|
980
1073
|
if (asyncCount > 0) {
|
|
981
1074
|
goals.push(`Wait for ${asyncCount === 1 ? "async feedback" : "async action feedback"}`);
|
|
982
1075
|
}
|
|
@@ -986,15 +1079,370 @@ function synthesizePrimaryGoal(screen) {
|
|
|
986
1079
|
}
|
|
987
1080
|
|
|
988
1081
|
// src/analyzers/ReactNativePlatformAnalyzer.ts
|
|
989
|
-
var
|
|
990
|
-
|
|
1082
|
+
var import_node_path3 = __toESM(require("path"));
|
|
1083
|
+
|
|
1084
|
+
// src/utils/glob.ts
|
|
1085
|
+
var import_fast_glob = __toESM(require("fast-glob"));
|
|
1086
|
+
function byCodeUnit(a, b) {
|
|
1087
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
1088
|
+
}
|
|
1089
|
+
async function globSorted(patterns, options) {
|
|
1090
|
+
const files = await (0, import_fast_glob.default)(patterns, options);
|
|
1091
|
+
return files.sort(byCodeUnit);
|
|
1092
|
+
}
|
|
991
1093
|
|
|
992
1094
|
// src/analyzers/ScreenAnalyzer.ts
|
|
993
1095
|
var import_promises = __toESM(require("fs/promises"));
|
|
1096
|
+
|
|
1097
|
+
// src/extractors/control-evidence.ts
|
|
1098
|
+
var import_node_crypto = require("crypto");
|
|
1099
|
+
var import_traverse = __toESM(require("@babel/traverse"));
|
|
1100
|
+
var t2 = __toESM(require("@babel/types"));
|
|
1101
|
+
|
|
1102
|
+
// src/extractors/control-evidence-bindings.ts
|
|
1103
|
+
var t = __toESM(require("@babel/types"));
|
|
1104
|
+
var MAX_HOPS = 8;
|
|
1105
|
+
function unwrap(path13) {
|
|
1106
|
+
while (path13.isTSAsExpression() || path13.isTSTypeAssertion() || path13.isTSNonNullExpression() || path13.isTSSatisfiesExpression() || path13.isParenthesizedExpression())
|
|
1107
|
+
path13 = path13.get("expression");
|
|
1108
|
+
return path13;
|
|
1109
|
+
}
|
|
1110
|
+
function constantValue(path13, depth = 0) {
|
|
1111
|
+
if (depth > MAX_HOPS) return void 0;
|
|
1112
|
+
path13 = unwrap(path13);
|
|
1113
|
+
if (!path13.isIdentifier()) return path13;
|
|
1114
|
+
const binding = path13.scope.getBinding(path13.node.name);
|
|
1115
|
+
if (!binding?.constant || !binding.path.isVariableDeclarator()) return path13;
|
|
1116
|
+
const init = binding.path.get("init");
|
|
1117
|
+
const resolved = init.node ? constantValue(init, depth + 1) : void 0;
|
|
1118
|
+
if (resolved?.isObjectExpression() && binding.referencePaths.some(
|
|
1119
|
+
(reference) => !reference.parentPath?.isJSXSpreadAttribute() && !reference.parentPath?.isSpreadElement()
|
|
1120
|
+
))
|
|
1121
|
+
return void 0;
|
|
1122
|
+
return resolved;
|
|
1123
|
+
}
|
|
1124
|
+
function propValue(path13, name) {
|
|
1125
|
+
const readObject = (path14) => {
|
|
1126
|
+
const resolved = constantValue(path14);
|
|
1127
|
+
if (!resolved?.isObjectExpression()) return { blocked: true };
|
|
1128
|
+
for (const property of [...resolved.get("properties")].reverse()) {
|
|
1129
|
+
if (property.isSpreadElement()) {
|
|
1130
|
+
const found = readObjectBounded(property.get("argument"));
|
|
1131
|
+
if (found.value || found.blocked) return found;
|
|
1132
|
+
} else if (property.isObjectProperty() || property.isObjectMethod()) {
|
|
1133
|
+
if (property.node.computed) return { blocked: true };
|
|
1134
|
+
const key = property.node.key;
|
|
1135
|
+
if ((t.isIdentifier(key) ? key.name : t.isStringLiteral(key) ? key.value : "") === name)
|
|
1136
|
+
return property.isObjectProperty() ? { value: property.get("value") } : { value: property };
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return {};
|
|
1140
|
+
};
|
|
1141
|
+
let objectBudget = MAX_HOPS;
|
|
1142
|
+
const readObjectBounded = (path14) => objectBudget-- > 0 ? readObject(path14) : { blocked: true };
|
|
1143
|
+
for (const attr of [...path13.get("attributes")].reverse()) {
|
|
1144
|
+
if (attr.isJSXAttribute() && t.isJSXIdentifier(attr.node.name, { name })) {
|
|
1145
|
+
const value = attr.get("value");
|
|
1146
|
+
return value.isJSXExpressionContainer() ? unwrap(value.get("expression")) : value.node ? value : void 0;
|
|
1147
|
+
}
|
|
1148
|
+
if (attr.isJSXSpreadAttribute()) {
|
|
1149
|
+
const found = readObjectBounded(attr.get("argument"));
|
|
1150
|
+
if (found.value || found.blocked) return found.value;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return void 0;
|
|
1154
|
+
}
|
|
1155
|
+
function importIdentity(path13, depth = 0) {
|
|
1156
|
+
if (depth > MAX_HOPS) return void 0;
|
|
1157
|
+
path13 = unwrap(path13);
|
|
1158
|
+
if (path13.isIdentifier() || path13.isJSXIdentifier()) {
|
|
1159
|
+
const binding = path13.scope.getBinding(path13.node.name);
|
|
1160
|
+
if (!binding?.constant) return void 0;
|
|
1161
|
+
const declaration = binding.path;
|
|
1162
|
+
if (declaration.parentPath?.isImportDeclaration()) {
|
|
1163
|
+
const module2 = declaration.parentPath.node.source.value;
|
|
1164
|
+
if (declaration.isImportSpecifier()) {
|
|
1165
|
+
const imported = declaration.node.imported;
|
|
1166
|
+
return { module: module2, imported: t.isIdentifier(imported) ? imported.name : imported.value };
|
|
1167
|
+
}
|
|
1168
|
+
if (declaration.isImportDefaultSpecifier()) return { module: module2, imported: "default" };
|
|
1169
|
+
if (declaration.isImportNamespaceSpecifier()) return { module: module2, imported: "*" };
|
|
1170
|
+
}
|
|
1171
|
+
if (declaration.isVariableDeclarator() && declaration.get("init").node)
|
|
1172
|
+
return importIdentity(declaration.get("init"), depth + 1);
|
|
1173
|
+
}
|
|
1174
|
+
if (path13.isJSXMemberExpression() || path13.isMemberExpression() && !path13.node.computed) {
|
|
1175
|
+
const origin = importIdentity(path13.get("object"), depth + 1);
|
|
1176
|
+
const key = path13.node.property;
|
|
1177
|
+
if (origin && (t.isIdentifier(key) || t.isJSXIdentifier(key)) && (origin.imported === "*" || origin.module === "react" && origin.imported === "default"))
|
|
1178
|
+
return { module: origin.module, imported: key.name };
|
|
1179
|
+
}
|
|
1180
|
+
return void 0;
|
|
1181
|
+
}
|
|
1182
|
+
function handlerFunction(path13, depth = 0) {
|
|
1183
|
+
if (depth > MAX_HOPS) return void 0;
|
|
1184
|
+
path13 = unwrap(path13);
|
|
1185
|
+
if (path13.isFunction()) return path13;
|
|
1186
|
+
if (path13.isIdentifier()) {
|
|
1187
|
+
const binding = path13.scope.getBinding(path13.node.name);
|
|
1188
|
+
if (!binding?.constant) return void 0;
|
|
1189
|
+
if (binding.path.isFunctionDeclaration()) return binding.path;
|
|
1190
|
+
if (binding.path.isVariableDeclarator() && binding.path.get("init").node)
|
|
1191
|
+
return handlerFunction(binding.path.get("init"), depth + 1);
|
|
1192
|
+
}
|
|
1193
|
+
if (path13.isCallExpression()) {
|
|
1194
|
+
const origin = importIdentity(path13.get("callee"));
|
|
1195
|
+
if (origin?.module === "react" && origin.imported === "useCallback") {
|
|
1196
|
+
const callback = path13.get("arguments")[0];
|
|
1197
|
+
if (callback) return handlerFunction(callback, depth + 1);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
if (path13.isMemberExpression() && !path13.node.computed && t.isThisExpression(path13.node.object)) {
|
|
1201
|
+
const key = path13.node.property;
|
|
1202
|
+
if (!t.isIdentifier(key)) return void 0;
|
|
1203
|
+
const owner = path13.findParent((p) => p.isClassDeclaration() || p.isClassExpression());
|
|
1204
|
+
if (!owner || !(owner.isClassDeclaration() || owner.isClassExpression())) return void 0;
|
|
1205
|
+
for (const member of owner.get("body").get("body")) {
|
|
1206
|
+
if (!(member.isClassMethod() || member.isClassProperty()) || member.node.computed || member.node.static)
|
|
1207
|
+
continue;
|
|
1208
|
+
if (!t.isIdentifier(member.node.key, { name: key.name })) continue;
|
|
1209
|
+
if (member.isClassMethod() && member.node.kind === "method") return member;
|
|
1210
|
+
if (member.isClassProperty() && member.get("value").node)
|
|
1211
|
+
return handlerFunction(member.get("value"), depth + 1);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return void 0;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/extractors/control-evidence.ts
|
|
1218
|
+
function symbol(node) {
|
|
1219
|
+
if (t2.isTSAsExpression(node) || t2.isTSTypeAssertion(node) || t2.isTSNonNullExpression(node) || t2.isTSSatisfiesExpression(node))
|
|
1220
|
+
return symbol(node.expression);
|
|
1221
|
+
if (t2.isIdentifier(node) || t2.isJSXIdentifier(node)) return node.name;
|
|
1222
|
+
if (t2.isThisExpression(node)) return "this";
|
|
1223
|
+
if (t2.isMemberExpression(node) && !node.computed || t2.isJSXMemberExpression(node)) {
|
|
1224
|
+
const object = symbol(node.object), property = symbol(node.property);
|
|
1225
|
+
return object && property ? `${object}.${property}` : void 0;
|
|
1226
|
+
}
|
|
1227
|
+
if (t2.isUnaryExpression(node) && node.operator === "!") {
|
|
1228
|
+
const value = symbol(node.argument);
|
|
1229
|
+
return value ? `!${value}` : void 0;
|
|
1230
|
+
}
|
|
1231
|
+
return void 0;
|
|
1232
|
+
}
|
|
1233
|
+
function attribute(node, name) {
|
|
1234
|
+
return node.attributes.find(
|
|
1235
|
+
(a) => t2.isJSXAttribute(a) && t2.isJSXIdentifier(a.name, { name })
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
var ICON_LIBRARIES = [
|
|
1239
|
+
"lucide-react-native",
|
|
1240
|
+
"lucide-react",
|
|
1241
|
+
"@tamagui/lucide-icons",
|
|
1242
|
+
"@expo/vector-icons",
|
|
1243
|
+
"@react-native-vector-icons/",
|
|
1244
|
+
"react-native-vector-icons/"
|
|
1245
|
+
];
|
|
1246
|
+
function iconLibrary(module2) {
|
|
1247
|
+
return ICON_LIBRARIES.some(
|
|
1248
|
+
(name) => name.endsWith("/") ? module2.startsWith(name) : module2 === name || module2.startsWith(name + "/")
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
function add(values, value, max = 12) {
|
|
1252
|
+
if (value && value.length <= 240 && values.length < max && !values.includes(value))
|
|
1253
|
+
values.push(value);
|
|
1254
|
+
}
|
|
1255
|
+
function iconName(path13, explicitIcon = false) {
|
|
1256
|
+
const origin = importIdentity(path13);
|
|
1257
|
+
if (origin && iconLibrary(origin.module) && origin.imported !== "*")
|
|
1258
|
+
return origin.imported === "default" ? origin.module : `${origin.module}:${origin.imported}`;
|
|
1259
|
+
const name = symbol(path13.node);
|
|
1260
|
+
return name && (explicitIcon || /icon/i.test(name)) ? name : void 0;
|
|
1261
|
+
}
|
|
1262
|
+
function iconsInElement(path13, explicitIcon = false) {
|
|
1263
|
+
const name = iconName(path13.get("name"), explicitIcon);
|
|
1264
|
+
if (!name) return void 0;
|
|
1265
|
+
const glyph = propValue(path13, "name");
|
|
1266
|
+
const value = glyph && constantValue(glyph);
|
|
1267
|
+
return value?.isStringLiteral() ? `${name}:${value.node.value}` : name;
|
|
1268
|
+
}
|
|
1269
|
+
function hasInteraction(path13) {
|
|
1270
|
+
return ["onPress", "onLongPress", "onClick"].some(
|
|
1271
|
+
(name) => attribute(path13.node, name) || propValue(path13, name)
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
function collectPresentationIcons(path13, icons) {
|
|
1275
|
+
const namedIconProps = [
|
|
1276
|
+
"icon",
|
|
1277
|
+
"prefix",
|
|
1278
|
+
"suffix",
|
|
1279
|
+
"left",
|
|
1280
|
+
"right",
|
|
1281
|
+
"leadingIcon",
|
|
1282
|
+
"trailingIcon",
|
|
1283
|
+
"startIcon",
|
|
1284
|
+
"endIcon",
|
|
1285
|
+
"renderIcon"
|
|
1286
|
+
];
|
|
1287
|
+
const props = new Set(namedIconProps);
|
|
1288
|
+
for (const attr of path13.node.attributes)
|
|
1289
|
+
if (t2.isJSXAttribute(attr) && t2.isJSXIdentifier(attr.name) && !/^on[A-Z]/.test(attr.name.name))
|
|
1290
|
+
props.add(attr.name.name);
|
|
1291
|
+
for (const prop of props) {
|
|
1292
|
+
const icon = propValue(path13, prop);
|
|
1293
|
+
if (icon) {
|
|
1294
|
+
const value = constantValue(icon);
|
|
1295
|
+
if (value?.isStringLiteral()) {
|
|
1296
|
+
if (prop.toLowerCase().includes("icon")) add(icons, value.node.value, 8);
|
|
1297
|
+
} else {
|
|
1298
|
+
const explicitIcon = prop.toLowerCase() === "icon";
|
|
1299
|
+
if (icon.isJSXElement()) {
|
|
1300
|
+
if (hasInteraction(icon.get("openingElement"))) continue;
|
|
1301
|
+
add(icons, iconsInElement(icon.get("openingElement"), explicitIcon), 8);
|
|
1302
|
+
}
|
|
1303
|
+
icon.traverse({
|
|
1304
|
+
JSXAttribute(attr) {
|
|
1305
|
+
if (t2.isJSXIdentifier(attr.node.name) && /^on[A-Z]/.test(attr.node.name.name))
|
|
1306
|
+
attr.skip();
|
|
1307
|
+
},
|
|
1308
|
+
JSXElement(child) {
|
|
1309
|
+
const opening = child.get("openingElement");
|
|
1310
|
+
if (hasInteraction(opening)) {
|
|
1311
|
+
child.skip();
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
add(icons, iconsInElement(opening, explicitIcon), 8);
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
if (!icons.length && (namedIconProps.includes(prop) || explicitIcon))
|
|
1318
|
+
add(icons, iconName(icon, explicitIcon), 8);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
function collectIcons(path13) {
|
|
1324
|
+
const icons = [];
|
|
1325
|
+
collectPresentationIcons(path13, icons);
|
|
1326
|
+
const jsx = path13.parentPath;
|
|
1327
|
+
if (jsx.isJSXElement())
|
|
1328
|
+
jsx.traverse({
|
|
1329
|
+
// JSX mentioned inside an event callback is not a rendered child icon.
|
|
1330
|
+
JSXAttribute(attr) {
|
|
1331
|
+
attr.skip();
|
|
1332
|
+
},
|
|
1333
|
+
JSXElement(child) {
|
|
1334
|
+
const opening = child.get("openingElement");
|
|
1335
|
+
if (hasInteraction(opening)) {
|
|
1336
|
+
child.skip();
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
add(icons, iconsInElement(opening), 8);
|
|
1340
|
+
collectPresentationIcons(opening, icons);
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
return icons;
|
|
1344
|
+
}
|
|
1345
|
+
function conditionsAt(path13) {
|
|
1346
|
+
const conditions = [];
|
|
1347
|
+
let child = path13;
|
|
1348
|
+
for (let parent = child.parentPath; parent && !parent.isFunction(); child = parent, parent = parent.parentPath) {
|
|
1349
|
+
let test;
|
|
1350
|
+
let negated = false;
|
|
1351
|
+
if (parent.isLogicalExpression() && parent.node.right === child.node) {
|
|
1352
|
+
if (parent.node.operator === "&&") test = parent.node.left;
|
|
1353
|
+
if (parent.node.operator === "||") {
|
|
1354
|
+
test = parent.node.left;
|
|
1355
|
+
negated = true;
|
|
1356
|
+
}
|
|
1357
|
+
} else if (parent.isConditionalExpression() && parent.node.test !== child.node) {
|
|
1358
|
+
test = parent.node.test;
|
|
1359
|
+
negated = parent.node.alternate === child.node;
|
|
1360
|
+
} else if (parent.isIfStatement() && parent.node.test !== child.node) {
|
|
1361
|
+
test = parent.node.test;
|
|
1362
|
+
negated = parent.node.alternate === child.node;
|
|
1363
|
+
}
|
|
1364
|
+
const name = symbol(test);
|
|
1365
|
+
if (name) add(conditions, negated ? name.startsWith("!") ? name.slice(1) : `!${name}` : name);
|
|
1366
|
+
}
|
|
1367
|
+
return conditions;
|
|
1368
|
+
}
|
|
1369
|
+
function collectHandlerEvidence(expression, evidence) {
|
|
1370
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1371
|
+
let budget = 100;
|
|
1372
|
+
const visit = (path13, depth) => {
|
|
1373
|
+
if (depth > 4 || budget-- <= 0) return;
|
|
1374
|
+
const fn = handlerFunction(path13);
|
|
1375
|
+
if (!fn || seen.has(fn.node)) return;
|
|
1376
|
+
seen.add(fn.node);
|
|
1377
|
+
fn.traverse({
|
|
1378
|
+
// Ignore uncalled helper definitions; inline callbacks remain source evidence.
|
|
1379
|
+
Function(nested) {
|
|
1380
|
+
if (!nested.parentPath.isCallExpression() && !nested.parentPath.isObjectProperty())
|
|
1381
|
+
nested.skip();
|
|
1382
|
+
},
|
|
1383
|
+
CallExpression(call) {
|
|
1384
|
+
if (budget-- <= 0) {
|
|
1385
|
+
call.skip();
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
add(evidence.calls, symbol(call.node.callee));
|
|
1389
|
+
for (const arg of call.node.arguments) add(evidence.argumentBindings, symbol(arg));
|
|
1390
|
+
visit(call.get("callee"), depth + 1);
|
|
1391
|
+
if (!t2.isMemberExpression(call.node.callee) || call.node.callee.computed || !t2.isIdentifier(call.node.callee.property, { name: "alert" }))
|
|
1392
|
+
return;
|
|
1393
|
+
const callee = call.get("callee");
|
|
1394
|
+
if (!callee.isMemberExpression()) return;
|
|
1395
|
+
const origin = importIdentity(callee.get("object"));
|
|
1396
|
+
if (origin?.module !== "react-native" || origin.imported !== "Alert") return;
|
|
1397
|
+
const options = call.node.arguments[2];
|
|
1398
|
+
const destructiveOption = t2.isArrayExpression(options) && options.elements.some(
|
|
1399
|
+
(option) => t2.isObjectExpression(option) && option.properties.some(
|
|
1400
|
+
(p) => t2.isObjectProperty(p) && !p.computed && symbol(p.key) === "style" && t2.isStringLiteral(p.value, { value: "destructive" })
|
|
1401
|
+
)
|
|
1402
|
+
);
|
|
1403
|
+
if (destructiveOption)
|
|
1404
|
+
evidence.nativeConfirmation = {
|
|
1405
|
+
title: symbol(call.node.arguments[0]),
|
|
1406
|
+
destructiveOption
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
};
|
|
1411
|
+
visit(unwrap(expression), 0);
|
|
1412
|
+
}
|
|
1413
|
+
function extractControlEvidence(ast, source, file2) {
|
|
1414
|
+
const candidates = [];
|
|
1415
|
+
const sourceHash = (0, import_node_crypto.createHash)("sha256").update(source).digest("hex");
|
|
1416
|
+
(0, import_traverse.default)(ast, {
|
|
1417
|
+
JSXOpeningElement(path13) {
|
|
1418
|
+
const node = path13.node;
|
|
1419
|
+
if (attribute(node, "__appilotsControl")) return;
|
|
1420
|
+
const value = propValue(path13, "onPress");
|
|
1421
|
+
if (!value && !attribute(node, "onPress") || value?.isNullLiteral() || value?.isJSXEmptyExpression())
|
|
1422
|
+
return;
|
|
1423
|
+
const icons = collectIcons(path13);
|
|
1424
|
+
if (!icons.length || node.end == null) return;
|
|
1425
|
+
const evidence = {
|
|
1426
|
+
version: 1,
|
|
1427
|
+
siteId: (0, import_node_crypto.createHash)("sha256").update(`${file2}:${sourceHash}:${node.start}`).digest("hex").slice(0, 20),
|
|
1428
|
+
component: symbol(node.name) ?? "unknown",
|
|
1429
|
+
icons,
|
|
1430
|
+
...value && symbol(value.node) ? { handler: symbol(value.node) } : {},
|
|
1431
|
+
calls: [],
|
|
1432
|
+
argumentBindings: [],
|
|
1433
|
+
conditions: conditionsAt(path13)
|
|
1434
|
+
};
|
|
1435
|
+
if (value) collectHandlerEvidence(value, evidence);
|
|
1436
|
+
candidates.push({ evidence, sourceHash, offset: node.end - (node.selfClosing ? 2 : 1) });
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
return candidates;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// src/analyzers/ScreenAnalyzer.ts
|
|
994
1443
|
var import_path3 = __toESM(require("path"));
|
|
995
|
-
var
|
|
1444
|
+
var import_traverse5 = __toESM(require("@babel/traverse"));
|
|
996
1445
|
var BabelTypes = __toESM(require("@babel/types"));
|
|
997
|
-
var import_fast_glob = __toESM(require("fast-glob"));
|
|
998
1446
|
|
|
999
1447
|
// src/ast/parse.ts
|
|
1000
1448
|
var import_parser = require("@babel/parser");
|
|
@@ -1018,25 +1466,25 @@ function parseSource(source, parserPlugins = []) {
|
|
|
1018
1466
|
}
|
|
1019
1467
|
|
|
1020
1468
|
// src/ast/jsx/attributes.ts
|
|
1021
|
-
var
|
|
1469
|
+
var t3 = __toESM(require("@babel/types"));
|
|
1022
1470
|
function findJsxAttribute(element, name) {
|
|
1023
1471
|
return element.attributes.find(
|
|
1024
|
-
(attr) =>
|
|
1472
|
+
(attr) => t3.isJSXAttribute(attr) && t3.isJSXIdentifier(attr.name) && attr.name.name === name
|
|
1025
1473
|
);
|
|
1026
1474
|
}
|
|
1027
1475
|
function getStringAttr(element, name) {
|
|
1028
1476
|
const attr = findJsxAttribute(element, name);
|
|
1029
1477
|
if (!attr || !attr.value) return void 0;
|
|
1030
|
-
if (
|
|
1031
|
-
if (
|
|
1478
|
+
if (t3.isStringLiteral(attr.value)) return attr.value.value;
|
|
1479
|
+
if (t3.isJSXExpressionContainer(attr.value) && t3.isStringLiteral(attr.value.expression)) {
|
|
1032
1480
|
return attr.value.expression.value;
|
|
1033
1481
|
}
|
|
1034
1482
|
return void 0;
|
|
1035
1483
|
}
|
|
1036
1484
|
function getExpressionIdentifierAttr(element, name) {
|
|
1037
1485
|
const attr = findJsxAttribute(element, name);
|
|
1038
|
-
if (!attr?.value || !
|
|
1039
|
-
return
|
|
1486
|
+
if (!attr?.value || !t3.isJSXExpressionContainer(attr.value)) return void 0;
|
|
1487
|
+
return t3.isIdentifier(attr.value.expression) ? attr.value.expression.name : void 0;
|
|
1040
1488
|
}
|
|
1041
1489
|
function hasJsxAttribute(element, name) {
|
|
1042
1490
|
return Boolean(findJsxAttribute(element, name));
|
|
@@ -1115,18 +1563,18 @@ function classifyJsxComponent(name, element) {
|
|
|
1115
1563
|
}
|
|
1116
1564
|
|
|
1117
1565
|
// src/ast/functions/collect.ts
|
|
1118
|
-
var
|
|
1119
|
-
var
|
|
1566
|
+
var import_traverse2 = __toESM(require("@babel/traverse"));
|
|
1567
|
+
var t4 = __toESM(require("@babel/types"));
|
|
1120
1568
|
function collectFunctions(ast) {
|
|
1121
1569
|
const handlers = /* @__PURE__ */ new Map();
|
|
1122
|
-
(0,
|
|
1570
|
+
(0, import_traverse2.default)(ast, {
|
|
1123
1571
|
FunctionDeclaration: (nodePath) => {
|
|
1124
1572
|
if (nodePath.node.id?.name) handlers.set(nodePath.node.id.name, nodePath.node);
|
|
1125
1573
|
},
|
|
1126
1574
|
VariableDeclarator: (nodePath) => {
|
|
1127
|
-
if (!
|
|
1575
|
+
if (!t4.isIdentifier(nodePath.node.id)) return;
|
|
1128
1576
|
const init = nodePath.node.init;
|
|
1129
|
-
if (
|
|
1577
|
+
if (t4.isArrowFunctionExpression(init) || t4.isFunctionExpression(init)) {
|
|
1130
1578
|
handlers.set(nodePath.node.id.name, init);
|
|
1131
1579
|
}
|
|
1132
1580
|
}
|
|
@@ -1135,8 +1583,8 @@ function collectFunctions(ast) {
|
|
|
1135
1583
|
}
|
|
1136
1584
|
|
|
1137
1585
|
// src/ast/functions/analyze-async.ts
|
|
1138
|
-
var
|
|
1139
|
-
var
|
|
1586
|
+
var import_traverse3 = __toESM(require("@babel/traverse"));
|
|
1587
|
+
var t5 = __toESM(require("@babel/types"));
|
|
1140
1588
|
var DESTRUCTIVE_VERB = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
|
|
1141
1589
|
function analyzeFunctionBehavior(name, fn, handlers, seen = /* @__PURE__ */ new Set()) {
|
|
1142
1590
|
if (seen.has(name)) {
|
|
@@ -1158,41 +1606,41 @@ function analyzeFunctionBehavior(name, fn, handlers, seen = /* @__PURE__ */ new
|
|
|
1158
1606
|
let destructive = DESTRUCTIVE_VERB.test(name);
|
|
1159
1607
|
const loadingStateBindings = /* @__PURE__ */ new Set();
|
|
1160
1608
|
const inspectCall = (node) => {
|
|
1161
|
-
if (
|
|
1609
|
+
if (t5.isMemberExpression(node.callee) && t5.isIdentifier(node.callee.property)) {
|
|
1162
1610
|
const method = node.callee.property.name;
|
|
1163
1611
|
if (method === "then" || method === "catch" || method === "finally") hasThen = true;
|
|
1164
|
-
if (
|
|
1612
|
+
if (t5.isIdentifier(node.callee.object) && node.callee.object.name === "navigation" && ["navigate", "push", "replace"].includes(method)) {
|
|
1165
1613
|
const firstArg = node.arguments[0];
|
|
1166
|
-
if (
|
|
1614
|
+
if (t5.isStringLiteral(firstArg)) {
|
|
1167
1615
|
targetScreen = firstArg.value;
|
|
1168
1616
|
successSignal = { type: "navigation", target: firstArg.value };
|
|
1169
1617
|
}
|
|
1170
1618
|
}
|
|
1171
|
-
if (
|
|
1619
|
+
if (t5.isIdentifier(node.callee.object) && node.callee.object.name === "navigation" && method === "goBack") {
|
|
1172
1620
|
successSignal = { type: "goBack", description: "Action returns to the previous screen" };
|
|
1173
1621
|
}
|
|
1174
|
-
if (
|
|
1622
|
+
if (t5.isIdentifier(node.callee.object) && node.callee.object.name === "Alert" && method === "alert") {
|
|
1175
1623
|
nativeConfirmationExpected = true;
|
|
1176
1624
|
}
|
|
1177
|
-
if (
|
|
1625
|
+
if (t5.isIdentifier(node.callee.object) && /(toast|notification|snackbar)/i.test(node.callee.object.name)) {
|
|
1178
1626
|
const firstArg = node.arguments[0];
|
|
1179
1627
|
const secondArg = node.arguments[1];
|
|
1180
|
-
const text =
|
|
1628
|
+
const text = t5.isStringLiteral(secondArg) ? secondArg.value : t5.isStringLiteral(firstArg) ? firstArg.value : void 0;
|
|
1181
1629
|
const signal = { type: "toast", description: text };
|
|
1182
|
-
if (/(error|danger|fail)/i.test(method) ||
|
|
1630
|
+
if (/(error|danger|fail)/i.test(method) || t5.isStringLiteral(firstArg) && /error/i.test(firstArg.value)) {
|
|
1183
1631
|
failureSignal = signal;
|
|
1184
1632
|
} else {
|
|
1185
1633
|
successSignal = successSignal ?? signal;
|
|
1186
1634
|
}
|
|
1187
1635
|
}
|
|
1188
1636
|
}
|
|
1189
|
-
if (
|
|
1637
|
+
if (t5.isIdentifier(node.callee)) {
|
|
1190
1638
|
const calleeName = node.callee.name;
|
|
1191
1639
|
if (/^set[A-Z]/.test(calleeName)) {
|
|
1192
1640
|
hasStateSetter = true;
|
|
1193
1641
|
const stateName = setterToStateName(calleeName);
|
|
1194
1642
|
const firstArg = node.arguments[0];
|
|
1195
|
-
if (stateName &&
|
|
1643
|
+
if (stateName && t5.isBooleanLiteral(firstArg)) {
|
|
1196
1644
|
if (/loading|submitting|saving|fetching|refreshing/i.test(stateName)) {
|
|
1197
1645
|
loadingStateBindings.add(stateName);
|
|
1198
1646
|
}
|
|
@@ -1219,14 +1667,14 @@ function analyzeFunctionBehavior(name, fn, handlers, seen = /* @__PURE__ */ new
|
|
|
1219
1667
|
}
|
|
1220
1668
|
};
|
|
1221
1669
|
const inspectNode = (node) => {
|
|
1222
|
-
if (
|
|
1223
|
-
if (
|
|
1224
|
-
if (
|
|
1670
|
+
if (t5.isAwaitExpression(node)) hasAwait = true;
|
|
1671
|
+
if (t5.isCallExpression(node)) inspectCall(node);
|
|
1672
|
+
if (t5.isMemberExpression(node) && t5.isIdentifier(node.property) && DESTRUCTIVE_VERB.test(node.property.name)) {
|
|
1225
1673
|
destructive = true;
|
|
1226
1674
|
}
|
|
1227
1675
|
};
|
|
1228
1676
|
if (fn.body) {
|
|
1229
|
-
(0,
|
|
1677
|
+
(0, import_traverse3.default)(fn.body, {
|
|
1230
1678
|
noScope: true,
|
|
1231
1679
|
enter: (nodePath) => inspectNode(nodePath.node)
|
|
1232
1680
|
});
|
|
@@ -1254,21 +1702,21 @@ function setterToStateName(setterName) {
|
|
|
1254
1702
|
}
|
|
1255
1703
|
|
|
1256
1704
|
// src/ast/navigation/calls.ts
|
|
1257
|
-
var
|
|
1258
|
-
var
|
|
1705
|
+
var import_traverse4 = __toESM(require("@babel/traverse"));
|
|
1706
|
+
var t6 = __toESM(require("@babel/types"));
|
|
1259
1707
|
function extractNavigationCalls(ast) {
|
|
1260
1708
|
const calls = [];
|
|
1261
|
-
(0,
|
|
1262
|
-
noScope: !
|
|
1709
|
+
(0, import_traverse4.default)(ast, {
|
|
1710
|
+
noScope: !t6.isFile(ast),
|
|
1263
1711
|
CallExpression: (nodePath) => {
|
|
1264
1712
|
const node = nodePath.node;
|
|
1265
|
-
if (!
|
|
1713
|
+
if (!t6.isMemberExpression(node.callee) || !t6.isIdentifier(node.callee.object) || node.callee.object.name !== "navigation" || !t6.isIdentifier(node.callee.property)) {
|
|
1266
1714
|
return;
|
|
1267
1715
|
}
|
|
1268
1716
|
const method = node.callee.property.name;
|
|
1269
1717
|
if (!["navigate", "push", "replace", "goBack"].includes(method)) return;
|
|
1270
1718
|
const firstArg = node.arguments[0];
|
|
1271
|
-
const targetScreen =
|
|
1719
|
+
const targetScreen = t6.isStringLiteral(firstArg) ? firstArg.value : void 0;
|
|
1272
1720
|
calls.push({
|
|
1273
1721
|
method,
|
|
1274
1722
|
...targetScreen ? { targetScreen } : {},
|
|
@@ -1280,16 +1728,16 @@ function extractNavigationCalls(ast) {
|
|
|
1280
1728
|
}
|
|
1281
1729
|
function extractNavigationParams(arg) {
|
|
1282
1730
|
const params = {};
|
|
1283
|
-
if (!arg || !
|
|
1731
|
+
if (!arg || !t6.isObjectExpression(arg)) return params;
|
|
1284
1732
|
for (const prop of arg.properties) {
|
|
1285
|
-
if (!
|
|
1286
|
-
const key =
|
|
1733
|
+
if (!t6.isObjectProperty(prop)) continue;
|
|
1734
|
+
const key = t6.isIdentifier(prop.key) ? prop.key.name : t6.isStringLiteral(prop.key) ? prop.key.value : void 0;
|
|
1287
1735
|
if (!key) continue;
|
|
1288
|
-
if (
|
|
1736
|
+
if (t6.isMemberExpression(prop.value) && t6.isIdentifier(prop.value.object) && t6.isIdentifier(prop.value.property)) {
|
|
1289
1737
|
params[key] = `${prop.value.object.name}.${prop.value.property.name}`;
|
|
1290
|
-
} else if (
|
|
1738
|
+
} else if (t6.isIdentifier(prop.value)) {
|
|
1291
1739
|
params[key] = prop.value.name;
|
|
1292
|
-
} else if (
|
|
1740
|
+
} else if (t6.isStringLiteral(prop.value)) {
|
|
1293
1741
|
params[key] = prop.value.value;
|
|
1294
1742
|
}
|
|
1295
1743
|
}
|
|
@@ -1397,14 +1845,26 @@ var ScreenAnalyzer = class {
|
|
|
1397
1845
|
strictScreens;
|
|
1398
1846
|
/** Glob patterns that identify screen files in strict mode */
|
|
1399
1847
|
screenPatterns;
|
|
1848
|
+
/**
|
|
1849
|
+
* Arquivos que uma ROTA monta (`component={…}` resolvido pelo
|
|
1850
|
+
* `NavigationAnalyzer`). Passam pelo filtro estrito sem depender de
|
|
1851
|
+
* convenção de nome, porque um componente que uma rota monta É uma tela por
|
|
1852
|
+
* definição — não é heurística, é o que o app declarou.
|
|
1853
|
+
*
|
|
1854
|
+
* É isto que resolve o caso `rocketchat`, cujas telas se chamam `*View.tsx`
|
|
1855
|
+
* em `app/views/`, e o `coopcycle`, que não tem diretório `screens/` nenhum.
|
|
1856
|
+
*/
|
|
1857
|
+
routeTargetFiles;
|
|
1400
1858
|
/** §D: Count of screens filtered out in strict mode (available after analyze()) */
|
|
1401
1859
|
screensFilteredOut = 0;
|
|
1860
|
+
controlEvidenceFiles = {};
|
|
1402
1861
|
constructor(config, options) {
|
|
1403
1862
|
this.config = config;
|
|
1863
|
+
this.routeTargetFiles = options?.routeTargetFiles ?? /* @__PURE__ */ new Set();
|
|
1404
1864
|
this.strictScreens = options?.strictScreens ?? false;
|
|
1405
1865
|
this.screenPatterns = options?.screenPatterns ?? [
|
|
1406
|
-
"**/*Screen.{ts,tsx}",
|
|
1407
|
-
"**/screens/**/*.{ts,tsx}"
|
|
1866
|
+
"**/*Screen.{ts,tsx,js,jsx}",
|
|
1867
|
+
"**/screens/**/*.{ts,tsx,js,jsx}"
|
|
1408
1868
|
];
|
|
1409
1869
|
}
|
|
1410
1870
|
/** Analyze all screens in the project */
|
|
@@ -1416,7 +1876,7 @@ var ScreenAnalyzer = class {
|
|
|
1416
1876
|
console.log(`[ScreenAnalyzer] Strict mode ON \u2014 screen patterns:`, this.screenPatterns);
|
|
1417
1877
|
}
|
|
1418
1878
|
}
|
|
1419
|
-
const files = await (
|
|
1879
|
+
const files = await globSorted(include, {
|
|
1420
1880
|
cwd: this.config.rootDir,
|
|
1421
1881
|
ignore: exclude
|
|
1422
1882
|
});
|
|
@@ -1425,7 +1885,7 @@ var ScreenAnalyzer = class {
|
|
|
1425
1885
|
}
|
|
1426
1886
|
let screenPatternFiles = null;
|
|
1427
1887
|
if (this.strictScreens) {
|
|
1428
|
-
const matched = await (
|
|
1888
|
+
const matched = await globSorted(this.screenPatterns, {
|
|
1429
1889
|
cwd: this.config.rootDir,
|
|
1430
1890
|
ignore: exclude
|
|
1431
1891
|
});
|
|
@@ -1441,7 +1901,8 @@ var ScreenAnalyzer = class {
|
|
|
1441
1901
|
if (this.strictScreens) {
|
|
1442
1902
|
const hasRegisterScreen = descriptor.__hasRegisterScreen === true;
|
|
1443
1903
|
const matchesPattern = screenPatternFiles?.has(filePath) ?? false;
|
|
1444
|
-
|
|
1904
|
+
const isRouteTarget = this.routeTargetFiles.has(filePath);
|
|
1905
|
+
if (!hasRegisterScreen && !matchesPattern && !isRouteTarget) {
|
|
1445
1906
|
this.screensFilteredOut++;
|
|
1446
1907
|
if (this.verbose) {
|
|
1447
1908
|
console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file2}`);
|
|
@@ -1497,6 +1958,11 @@ var ScreenAnalyzer = class {
|
|
|
1497
1958
|
title: registerScreenMeta?.title,
|
|
1498
1959
|
description: registerScreenMeta?.description,
|
|
1499
1960
|
components,
|
|
1961
|
+
controlCandidates: extractControlEvidence(
|
|
1962
|
+
ast,
|
|
1963
|
+
source,
|
|
1964
|
+
import_path3.default.relative(this.config.rootDir, filePath)
|
|
1965
|
+
),
|
|
1500
1966
|
forms,
|
|
1501
1967
|
actions,
|
|
1502
1968
|
navigationTargets,
|
|
@@ -1507,6 +1973,8 @@ var ScreenAnalyzer = class {
|
|
|
1507
1973
|
...permissionsFromJsDoc.isPii ? { isPii: true } : {}
|
|
1508
1974
|
} : {}
|
|
1509
1975
|
};
|
|
1976
|
+
if (descriptor.controlCandidates?.length)
|
|
1977
|
+
this.controlEvidenceFiles[import_path3.default.relative(this.config.rootDir, filePath).split(import_path3.default.sep).join("/")] = descriptor.controlCandidates;
|
|
1510
1978
|
descriptor.__hasRegisterScreen = hasRegisterScreenCall;
|
|
1511
1979
|
return descriptor;
|
|
1512
1980
|
}
|
|
@@ -1516,7 +1984,7 @@ var ScreenAnalyzer = class {
|
|
|
1516
1984
|
*/
|
|
1517
1985
|
detectRegisterScreenCall(ast) {
|
|
1518
1986
|
let found = false;
|
|
1519
|
-
(0,
|
|
1987
|
+
(0, import_traverse5.default)(ast, {
|
|
1520
1988
|
CallExpression: (nodePath) => {
|
|
1521
1989
|
if (found) return;
|
|
1522
1990
|
const callee = nodePath.node.callee;
|
|
@@ -1533,7 +2001,7 @@ var ScreenAnalyzer = class {
|
|
|
1533
2001
|
*/
|
|
1534
2002
|
extractRegisterScreenMetadata(ast) {
|
|
1535
2003
|
let metadata = null;
|
|
1536
|
-
(0,
|
|
2004
|
+
(0, import_traverse5.default)(ast, {
|
|
1537
2005
|
CallExpression: (nodePath) => {
|
|
1538
2006
|
const callee = nodePath.node.callee;
|
|
1539
2007
|
if (BabelTypes.isIdentifier(callee) && callee.name === "registerScreen" || BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.property) && callee.property.name === "registerScreen") {
|
|
@@ -1649,6 +2117,8 @@ var ScreenAnalyzer = class {
|
|
|
1649
2117
|
action.riskLevel = value.value;
|
|
1650
2118
|
} else if (key === "nativeConfirmationExpected" && BabelTypes.isBooleanLiteral(value)) {
|
|
1651
2119
|
action.nativeConfirmationExpected = value.value;
|
|
2120
|
+
} else if (key === "asyncBudgetMs" && BabelTypes.isNumericLiteral(value) && Number.isInteger(value.value) && value.value > 0) {
|
|
2121
|
+
action.asyncBudgetMs = value.value;
|
|
1652
2122
|
} else if (key === "appilotsInferred" && BabelTypes.isObjectExpression(value)) {
|
|
1653
2123
|
action.appilotsInferred = this.parseAppilotsInferredObject(value);
|
|
1654
2124
|
}
|
|
@@ -1828,7 +2298,7 @@ var ScreenAnalyzer = class {
|
|
|
1828
2298
|
*/
|
|
1829
2299
|
extractDefaultComponentName(ast) {
|
|
1830
2300
|
let componentName = "";
|
|
1831
|
-
(0,
|
|
2301
|
+
(0, import_traverse5.default)(ast, {
|
|
1832
2302
|
ExportDefaultDeclaration: (nodePath) => {
|
|
1833
2303
|
const declaration = nodePath.node.declaration;
|
|
1834
2304
|
if (BabelTypes.isFunctionDeclaration(declaration) && declaration.id?.name) {
|
|
@@ -1853,7 +2323,7 @@ var ScreenAnalyzer = class {
|
|
|
1853
2323
|
*/
|
|
1854
2324
|
extractNavigationTargets(ast) {
|
|
1855
2325
|
const targets = /* @__PURE__ */ new Set();
|
|
1856
|
-
(0,
|
|
2326
|
+
(0, import_traverse5.default)(ast, {
|
|
1857
2327
|
CallExpression: (nodePath) => {
|
|
1858
2328
|
const callee = nodePath.node.callee;
|
|
1859
2329
|
if (BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.object) && callee.object.name === "navigation" && BabelTypes.isIdentifier(callee.property) && callee.property.name === "navigate") {
|
|
@@ -1872,7 +2342,7 @@ var ScreenAnalyzer = class {
|
|
|
1872
2342
|
extractForms(ast) {
|
|
1873
2343
|
const forms = [];
|
|
1874
2344
|
const fields = /* @__PURE__ */ new Map();
|
|
1875
|
-
(0,
|
|
2345
|
+
(0, import_traverse5.default)(ast, {
|
|
1876
2346
|
JSXOpeningElement: (nodePath) => {
|
|
1877
2347
|
const element = nodePath.node;
|
|
1878
2348
|
if (BabelTypes.isJSXIdentifier(element.name)) {
|
|
@@ -1993,7 +2463,7 @@ var ScreenAnalyzer = class {
|
|
|
1993
2463
|
extractComponents(ast) {
|
|
1994
2464
|
const components = [];
|
|
1995
2465
|
const seen = /* @__PURE__ */ new Set();
|
|
1996
|
-
(0,
|
|
2466
|
+
(0, import_traverse5.default)(ast, {
|
|
1997
2467
|
JSXOpeningElement: (nodePath) => {
|
|
1998
2468
|
const element = nodePath.node;
|
|
1999
2469
|
if (BabelTypes.isJSXIdentifier(element.name)) {
|
|
@@ -2057,7 +2527,7 @@ var ScreenAnalyzer = class {
|
|
|
2057
2527
|
const actionLabels = new Map(
|
|
2058
2528
|
actions.filter((a) => a.label).map((a) => [this.normalizeLabel(a.label), a])
|
|
2059
2529
|
);
|
|
2060
|
-
(0,
|
|
2530
|
+
(0, import_traverse5.default)(ast, {
|
|
2061
2531
|
JSXOpeningElement: (nodePath) => {
|
|
2062
2532
|
const element = nodePath.node;
|
|
2063
2533
|
if (BabelTypes.isJSXIdentifier(element.name)) {
|
|
@@ -2126,7 +2596,7 @@ var ScreenAnalyzer = class {
|
|
|
2126
2596
|
}
|
|
2127
2597
|
collectButtonHandlersByLabel(ast) {
|
|
2128
2598
|
const out = /* @__PURE__ */ new Map();
|
|
2129
|
-
(0,
|
|
2599
|
+
(0, import_traverse5.default)(ast, {
|
|
2130
2600
|
JSXOpeningElement: (nodePath) => {
|
|
2131
2601
|
const element = nodePath.node;
|
|
2132
2602
|
if (!BabelTypes.isJSXIdentifier(element.name)) return;
|
|
@@ -2208,7 +2678,7 @@ var ScreenAnalyzer = class {
|
|
|
2208
2678
|
}
|
|
2209
2679
|
};
|
|
2210
2680
|
if (fn.body) {
|
|
2211
|
-
(0,
|
|
2681
|
+
(0, import_traverse5.default)(
|
|
2212
2682
|
fn.body,
|
|
2213
2683
|
{
|
|
2214
2684
|
noScope: true,
|
|
@@ -2405,7 +2875,7 @@ var ScreenAnalyzer = class {
|
|
|
2405
2875
|
extractCollections(ast) {
|
|
2406
2876
|
const renderItemFns = this.collectRenderItemFunctions(ast);
|
|
2407
2877
|
const collections = [];
|
|
2408
|
-
(0,
|
|
2878
|
+
(0, import_traverse5.default)(ast, {
|
|
2409
2879
|
JSXOpeningElement: (nodePath) => {
|
|
2410
2880
|
const element = nodePath.node;
|
|
2411
2881
|
if (!BabelTypes.isJSXIdentifier(element.name)) return;
|
|
@@ -2442,7 +2912,7 @@ var ScreenAnalyzer = class {
|
|
|
2442
2912
|
}
|
|
2443
2913
|
collectRenderItemFunctions(ast) {
|
|
2444
2914
|
const out = /* @__PURE__ */ new Map();
|
|
2445
|
-
(0,
|
|
2915
|
+
(0, import_traverse5.default)(ast, {
|
|
2446
2916
|
VariableDeclarator: (nodePath) => {
|
|
2447
2917
|
if (!BabelTypes.isIdentifier(nodePath.node.id)) return;
|
|
2448
2918
|
const init = nodePath.node.init;
|
|
@@ -2485,7 +2955,7 @@ var ScreenAnalyzer = class {
|
|
|
2485
2955
|
}
|
|
2486
2956
|
extractRowAction(fn) {
|
|
2487
2957
|
let action;
|
|
2488
|
-
(0,
|
|
2958
|
+
(0, import_traverse5.default)(
|
|
2489
2959
|
fn.body,
|
|
2490
2960
|
{
|
|
2491
2961
|
noScope: true,
|
|
@@ -2538,7 +3008,7 @@ var ScreenAnalyzer = class {
|
|
|
2538
3008
|
} else if (BabelTypes.isIdentifier(firstParam)) {
|
|
2539
3009
|
itemNames.add(firstParam.name);
|
|
2540
3010
|
}
|
|
2541
|
-
(0,
|
|
3011
|
+
(0, import_traverse5.default)(
|
|
2542
3012
|
fn.body,
|
|
2543
3013
|
{
|
|
2544
3014
|
noScope: true,
|
|
@@ -2580,7 +3050,7 @@ var ScreenAnalyzer = class {
|
|
|
2580
3050
|
inferSearchField(ast, dataSource) {
|
|
2581
3051
|
if (!dataSource) return void 0;
|
|
2582
3052
|
let queryBinding;
|
|
2583
|
-
(0,
|
|
3053
|
+
(0, import_traverse5.default)(ast, {
|
|
2584
3054
|
CallExpression: (nodePath) => {
|
|
2585
3055
|
const node = nodePath.node;
|
|
2586
3056
|
if (!BabelTypes.isMemberExpression(node.callee)) return;
|
|
@@ -2591,7 +3061,7 @@ var ScreenAnalyzer = class {
|
|
|
2591
3061
|
const fn = node.arguments[0];
|
|
2592
3062
|
if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn))
|
|
2593
3063
|
return;
|
|
2594
|
-
(0,
|
|
3064
|
+
(0, import_traverse5.default)(
|
|
2595
3065
|
fn.body,
|
|
2596
3066
|
{
|
|
2597
3067
|
noScope: true,
|
|
@@ -2636,43 +3106,880 @@ var ScreenAnalyzer = class {
|
|
|
2636
3106
|
// src/analyzers/NavigationAnalyzer.ts
|
|
2637
3107
|
var import_fs4 = require("fs");
|
|
2638
3108
|
var import_path4 = __toESM(require("path"));
|
|
2639
|
-
var
|
|
3109
|
+
var parser2 = __toESM(require("@babel/parser"));
|
|
3110
|
+
var import_traverse6 = __toESM(require("@babel/traverse"));
|
|
3111
|
+
var t8 = __toESM(require("@babel/types"));
|
|
3112
|
+
|
|
3113
|
+
// src/ast/navigation/module-graph.ts
|
|
3114
|
+
var import_node_fs = require("fs");
|
|
3115
|
+
var import_node_path = __toESM(require("path"));
|
|
2640
3116
|
var parser = __toESM(require("@babel/parser"));
|
|
2641
|
-
var
|
|
2642
|
-
var
|
|
3117
|
+
var t7 = __toESM(require("@babel/types"));
|
|
3118
|
+
var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
|
|
3119
|
+
var MAX_HOPS2 = 8;
|
|
3120
|
+
var ModuleGraph = class {
|
|
3121
|
+
asts = /* @__PURE__ */ new Map();
|
|
3122
|
+
resolved = /* @__PURE__ */ new Map();
|
|
3123
|
+
/** `@src/*` → `<root>/src/*`, lido do tsconfig do app. */
|
|
3124
|
+
aliases;
|
|
3125
|
+
/** `uniswap` → `<repo>/packages/uniswap`, lido do workspace do monorepo. */
|
|
3126
|
+
workspacePackages;
|
|
3127
|
+
constructor(rootDir) {
|
|
3128
|
+
this.aliases = rootDir ? readTsconfigAliases(rootDir) : [];
|
|
3129
|
+
this.workspacePackages = rootDir ? readWorkspacePackages(rootDir) : [];
|
|
3130
|
+
}
|
|
3131
|
+
/** AST de um arquivo, memoizada. `null` quando não parseia. */
|
|
3132
|
+
parse(file2) {
|
|
3133
|
+
const cached = this.asts.get(file2);
|
|
3134
|
+
if (cached !== void 0) return cached;
|
|
3135
|
+
let ast = null;
|
|
3136
|
+
try {
|
|
3137
|
+
ast = parser.parse((0, import_node_fs.readFileSync)(file2, "utf-8"), {
|
|
3138
|
+
sourceType: "module",
|
|
3139
|
+
plugins: ["jsx", "typescript"]
|
|
3140
|
+
});
|
|
3141
|
+
} catch {
|
|
3142
|
+
ast = null;
|
|
3143
|
+
}
|
|
3144
|
+
this.asts.set(file2, ast);
|
|
3145
|
+
return ast;
|
|
3146
|
+
}
|
|
3147
|
+
/**
|
|
3148
|
+
* `./account/Home` a partir de `src/navigation/index.tsx` → caminho absoluto.
|
|
3149
|
+
* Só resolve caminho relativo: import de pacote (`@react-navigation/native`)
|
|
3150
|
+
* é de terceiro e não tem tela nossa dentro.
|
|
3151
|
+
*/
|
|
3152
|
+
resolve(fromFile, spec) {
|
|
3153
|
+
const key = `${fromFile} ${spec}`;
|
|
3154
|
+
const cached = this.resolved.get(key);
|
|
3155
|
+
if (cached !== void 0) return cached;
|
|
3156
|
+
let base = null;
|
|
3157
|
+
if (spec.startsWith(".")) {
|
|
3158
|
+
base = import_node_path.default.resolve(import_node_path.default.dirname(fromFile), spec);
|
|
3159
|
+
} else {
|
|
3160
|
+
for (const { prefix, target } of this.aliases) {
|
|
3161
|
+
if (spec === prefix || spec.startsWith(prefix + "/")) {
|
|
3162
|
+
base = import_node_path.default.join(target, spec.slice(prefix.length));
|
|
3163
|
+
break;
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
if (!base) {
|
|
3167
|
+
for (const pkg of this.workspacePackages) {
|
|
3168
|
+
if (spec === pkg.name || spec.startsWith(pkg.name + "/")) {
|
|
3169
|
+
base = import_node_path.default.join(pkg.dir, spec.slice(pkg.name.length));
|
|
3170
|
+
break;
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
if (!base) {
|
|
3176
|
+
this.resolved.set(key, null);
|
|
3177
|
+
return null;
|
|
3178
|
+
}
|
|
3179
|
+
const candidates = [
|
|
3180
|
+
base,
|
|
3181
|
+
...EXTENSIONS.map((e) => base + e),
|
|
3182
|
+
...EXTENSIONS.map((e) => import_node_path.default.join(base, "index" + e))
|
|
3183
|
+
];
|
|
3184
|
+
let found = null;
|
|
3185
|
+
for (const c of candidates) {
|
|
3186
|
+
try {
|
|
3187
|
+
if ((0, import_node_fs.existsSync)(c) && (0, import_node_fs.statSync)(c).isFile()) {
|
|
3188
|
+
found = c;
|
|
3189
|
+
break;
|
|
3190
|
+
}
|
|
3191
|
+
} catch {
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
this.resolved.set(key, found);
|
|
3195
|
+
return found;
|
|
3196
|
+
}
|
|
3197
|
+
/** `import`s do arquivo, por nome local. */
|
|
3198
|
+
imports(file2) {
|
|
3199
|
+
const out = /* @__PURE__ */ new Map();
|
|
3200
|
+
const ast = this.parse(file2);
|
|
3201
|
+
if (!ast) return out;
|
|
3202
|
+
for (const stmt of ast.program.body) {
|
|
3203
|
+
if (!t7.isImportDeclaration(stmt)) continue;
|
|
3204
|
+
const source = stmt.source.value;
|
|
3205
|
+
for (const spec of stmt.specifiers) {
|
|
3206
|
+
if (t7.isImportDefaultSpecifier(spec)) {
|
|
3207
|
+
out.set(spec.local.name, { source, imported: "default" });
|
|
3208
|
+
} else if (t7.isImportNamespaceSpecifier(spec)) {
|
|
3209
|
+
out.set(spec.local.name, { source, imported: "*" });
|
|
3210
|
+
} else if (t7.isImportSpecifier(spec)) {
|
|
3211
|
+
const imported = t7.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
|
|
3212
|
+
out.set(spec.local.name, { source, imported });
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
return out;
|
|
3217
|
+
}
|
|
3218
|
+
/** `const X = <init>` no topo do arquivo, incluindo `export const`. */
|
|
3219
|
+
topLevelInit(file2, name) {
|
|
3220
|
+
const ast = this.parse(file2);
|
|
3221
|
+
if (!ast) return null;
|
|
3222
|
+
for (const stmt of ast.program.body) {
|
|
3223
|
+
const decl = t7.isExportNamedDeclaration(stmt) ? stmt.declaration : stmt;
|
|
3224
|
+
if (!t7.isVariableDeclaration(decl)) continue;
|
|
3225
|
+
for (const d of decl.declarations) {
|
|
3226
|
+
if (t7.isIdentifier(d.id) && d.id.name === name && d.init) {
|
|
3227
|
+
return t7.isTSAsExpression(d.init) ? d.init.expression : d.init;
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
return null;
|
|
3232
|
+
}
|
|
3233
|
+
/**
|
|
3234
|
+
* Onde `name` é DEFINIDO — segue import e reexport de barrel.
|
|
3235
|
+
* Devolve o arquivo e o nome sob o qual ele é definido lá.
|
|
3236
|
+
*/
|
|
3237
|
+
resolveBinding(file2, name, hops = 0) {
|
|
3238
|
+
if (hops > MAX_HOPS2) return null;
|
|
3239
|
+
if (this.topLevelInit(file2, name) !== null) return { file: file2, name };
|
|
3240
|
+
const binding = this.imports(file2).get(name);
|
|
3241
|
+
if (binding) {
|
|
3242
|
+
const target = this.resolve(file2, binding.source);
|
|
3243
|
+
if (!target) return null;
|
|
3244
|
+
const next = binding.imported === "default" || binding.imported === "*" ? name : binding.imported;
|
|
3245
|
+
const deeper = this.resolveBinding(target, next, hops + 1);
|
|
3246
|
+
return deeper ?? { file: target, name: next };
|
|
3247
|
+
}
|
|
3248
|
+
const ast = this.parse(file2);
|
|
3249
|
+
if (ast) {
|
|
3250
|
+
for (const stmt of ast.program.body) {
|
|
3251
|
+
if (!t7.isExportNamedDeclaration(stmt) || !stmt.source) continue;
|
|
3252
|
+
for (const spec of stmt.specifiers) {
|
|
3253
|
+
if (!t7.isExportSpecifier(spec)) continue;
|
|
3254
|
+
const exported = t7.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value;
|
|
3255
|
+
if (exported !== name) continue;
|
|
3256
|
+
const target = this.resolve(file2, stmt.source.value);
|
|
3257
|
+
if (!target) return null;
|
|
3258
|
+
const local = spec.local.name;
|
|
3259
|
+
return this.resolveBinding(target, local, hops + 1) ?? { file: target, name: local };
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
if (ast) {
|
|
3264
|
+
for (const stmt of ast.program.body) {
|
|
3265
|
+
if (!t7.isExportAllDeclaration(stmt)) continue;
|
|
3266
|
+
const target = this.resolve(file2, stmt.source.value);
|
|
3267
|
+
if (!target || target === file2) continue;
|
|
3268
|
+
const deeper = this.resolveBinding(target, name, hops + 1);
|
|
3269
|
+
if (deeper) return deeper;
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
if (ast) {
|
|
3273
|
+
for (const stmt of ast.program.body) {
|
|
3274
|
+
if (!t7.isExportDefaultDeclaration(stmt)) continue;
|
|
3275
|
+
if (t7.isIdentifier(stmt.declaration)) {
|
|
3276
|
+
const local = stmt.declaration.name;
|
|
3277
|
+
if (local === name) return null;
|
|
3278
|
+
return this.resolveBinding(file2, local, hops + 1) ?? { file: file2, name: local };
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
return null;
|
|
3283
|
+
}
|
|
3284
|
+
/**
|
|
3285
|
+
* O valor string de uma expressão de nome de rota, ou `null`.
|
|
3286
|
+
*
|
|
3287
|
+
* Cobre `"Chat"`, `ROUTES.CHAT`, `ROUTES.ONBOARDING.SPLASH` e `SOME_CONST` —
|
|
3288
|
+
* seguindo import quando o objeto vem de outro arquivo. NÃO cobre template
|
|
3289
|
+
* com interpolação nem valor calculado, de propósito.
|
|
3290
|
+
*/
|
|
3291
|
+
stringConstant(file2, node, hops = 0) {
|
|
3292
|
+
if (!node || hops > MAX_HOPS2) return null;
|
|
3293
|
+
if (t7.isStringLiteral(node)) return node.value;
|
|
3294
|
+
if (t7.isTemplateLiteral(node)) {
|
|
3295
|
+
return node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? null : null;
|
|
3296
|
+
}
|
|
3297
|
+
if (t7.isTSAsExpression(node)) return this.stringConstant(file2, node.expression, hops + 1);
|
|
3298
|
+
const chain = memberChain(node);
|
|
3299
|
+
if (!chain) return null;
|
|
3300
|
+
const origin = this.resolveBinding(file2, chain.root);
|
|
3301
|
+
if (!origin) return null;
|
|
3302
|
+
let current = this.topLevelInit(origin.file, origin.name);
|
|
3303
|
+
if (!current) return null;
|
|
3304
|
+
for (const key of chain.path) {
|
|
3305
|
+
if (!t7.isObjectExpression(current)) return null;
|
|
3306
|
+
const prop = objectProperty(current, key);
|
|
3307
|
+
if (!prop) return null;
|
|
3308
|
+
current = t7.isTSAsExpression(prop) ? prop.expression : prop;
|
|
3309
|
+
}
|
|
3310
|
+
return t7.isStringLiteral(current) ? current.value : null;
|
|
3311
|
+
}
|
|
3312
|
+
/**
|
|
3313
|
+
* O ARQUIVO onde vive o componente de uma rota, ou `null`.
|
|
3314
|
+
*
|
|
3315
|
+
* Aceita as três formas que o corpus mostrou: identificador
|
|
3316
|
+
* (`component={Home}`), membro de barrel (`component={screens.AccountHome}`)
|
|
3317
|
+
* e componente embrulhado em HOC (`component={gestureHandlerRootHOC(Chat)}`,
|
|
3318
|
+
* que é como o `pocketpal` monta todas as telas do Drawer).
|
|
3319
|
+
*/
|
|
3320
|
+
componentFile(file2, node, hops = 0) {
|
|
3321
|
+
if (!node || hops > MAX_HOPS2) return null;
|
|
3322
|
+
if (t7.isCallExpression(node)) {
|
|
3323
|
+
for (const arg of node.arguments) {
|
|
3324
|
+
if (t7.isIdentifier(arg) || t7.isMemberExpression(arg)) {
|
|
3325
|
+
const inner = this.componentFile(file2, arg, hops + 1);
|
|
3326
|
+
if (inner) return inner;
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
return null;
|
|
3330
|
+
}
|
|
3331
|
+
if (t7.isTSAsExpression(node)) return this.componentFile(file2, node.expression, hops + 1);
|
|
3332
|
+
const chain = memberChain(node);
|
|
3333
|
+
if (!chain) return null;
|
|
3334
|
+
const origin = this.resolveBinding(file2, chain.root);
|
|
3335
|
+
if (!origin) return null;
|
|
3336
|
+
if (chain.path.length === 0) return origin.file;
|
|
3337
|
+
const init = this.topLevelInit(origin.file, origin.name);
|
|
3338
|
+
if (init && t7.isObjectExpression(init)) {
|
|
3339
|
+
const prop = objectProperty(init, chain.path[0]);
|
|
3340
|
+
if (prop) return this.componentFile(origin.file, prop, hops + 1);
|
|
3341
|
+
}
|
|
3342
|
+
const viaExport = this.resolveBinding(origin.file, chain.path[0], hops + 1);
|
|
3343
|
+
return viaExport?.file ?? null;
|
|
3344
|
+
}
|
|
3345
|
+
/**
|
|
3346
|
+
* Os arquivos DO PROJETO que este arquivo renderiza como JSX.
|
|
3347
|
+
*
|
|
3348
|
+
* `<ChatView …/>` em `ChatScreen.tsx` → `components/ChatView/ChatView.tsx`.
|
|
3349
|
+
* Import de pacote devolve `null` no `resolve` e fica de fora: componente de
|
|
3350
|
+
* terceiro não tem tela nossa dentro.
|
|
3351
|
+
*
|
|
3352
|
+
* Ignora `<X.Screen>` e `<X.Navigator>` de propósito — navegação é outro
|
|
3353
|
+
* extractor, e um navegador não é conteúdo de tela.
|
|
3354
|
+
*/
|
|
3355
|
+
renderedComponentFiles(file2) {
|
|
3356
|
+
const ast = this.parse(file2);
|
|
3357
|
+
if (!ast) return [];
|
|
3358
|
+
const names = /* @__PURE__ */ new Set();
|
|
3359
|
+
const visit = (node) => {
|
|
3360
|
+
if (!node || typeof node !== "object") return;
|
|
3361
|
+
if (t7.isJSXOpeningElement(node) && t7.isJSXIdentifier(node.name)) {
|
|
3362
|
+
const n = node.name.name;
|
|
3363
|
+
if (/^[A-Z]/.test(n)) names.add(n);
|
|
3364
|
+
}
|
|
3365
|
+
for (const key of Object.keys(node)) {
|
|
3366
|
+
const value = node[key];
|
|
3367
|
+
if (Array.isArray(value)) {
|
|
3368
|
+
for (const item of value) visit(item);
|
|
3369
|
+
} else if (value && typeof value === "object" && "type" in value) {
|
|
3370
|
+
visit(value);
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
};
|
|
3374
|
+
visit(ast.program);
|
|
3375
|
+
const out = /* @__PURE__ */ new Set();
|
|
3376
|
+
for (const name of names) {
|
|
3377
|
+
const origin = this.resolveBinding(file2, name);
|
|
3378
|
+
if (origin && origin.file !== file2) out.add(origin.file);
|
|
3379
|
+
}
|
|
3380
|
+
return [...out];
|
|
3381
|
+
}
|
|
3382
|
+
};
|
|
3383
|
+
function memberChain(node) {
|
|
3384
|
+
const chain = [];
|
|
3385
|
+
let current = node;
|
|
3386
|
+
while (t7.isMemberExpression(current)) {
|
|
3387
|
+
if (current.computed) {
|
|
3388
|
+
if (!t7.isStringLiteral(current.property)) return null;
|
|
3389
|
+
chain.unshift(current.property.value);
|
|
3390
|
+
} else if (t7.isIdentifier(current.property)) {
|
|
3391
|
+
chain.unshift(current.property.name);
|
|
3392
|
+
} else {
|
|
3393
|
+
return null;
|
|
3394
|
+
}
|
|
3395
|
+
current = current.object;
|
|
3396
|
+
}
|
|
3397
|
+
return t7.isIdentifier(current) ? { root: current.name, path: chain } : null;
|
|
3398
|
+
}
|
|
3399
|
+
function objectProperty(obj, key) {
|
|
3400
|
+
for (const prop of obj.properties) {
|
|
3401
|
+
if (!t7.isObjectProperty(prop)) continue;
|
|
3402
|
+
const name = t7.isIdentifier(prop.key) ? prop.key.name : t7.isStringLiteral(prop.key) ? prop.key.value : null;
|
|
3403
|
+
if (name === key && t7.isExpression(prop.value)) return prop.value;
|
|
3404
|
+
}
|
|
3405
|
+
return null;
|
|
3406
|
+
}
|
|
3407
|
+
function readTsconfigAliases(rootDir) {
|
|
3408
|
+
const file2 = import_node_path.default.join(rootDir, "tsconfig.json");
|
|
3409
|
+
if (!(0, import_node_fs.existsSync)(file2)) return [];
|
|
3410
|
+
let parsed;
|
|
3411
|
+
try {
|
|
3412
|
+
const raw = stripJsonComments((0, import_node_fs.readFileSync)(file2, "utf-8")).replace(/,(\s*[}\]])/g, "$1");
|
|
3413
|
+
parsed = JSON.parse(raw);
|
|
3414
|
+
} catch {
|
|
3415
|
+
return [];
|
|
3416
|
+
}
|
|
3417
|
+
const paths = parsed.compilerOptions?.paths;
|
|
3418
|
+
if (!paths) return [];
|
|
3419
|
+
const baseUrl = import_node_path.default.resolve(rootDir, parsed.compilerOptions?.baseUrl ?? ".");
|
|
3420
|
+
const out = [];
|
|
3421
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
3422
|
+
const first = targets[0];
|
|
3423
|
+
if (typeof first !== "string") continue;
|
|
3424
|
+
out.push({
|
|
3425
|
+
prefix: pattern.replace(/\/?\*$/, ""),
|
|
3426
|
+
target: import_node_path.default.resolve(baseUrl, first.replace(/\/?\*$/, ""))
|
|
3427
|
+
});
|
|
3428
|
+
}
|
|
3429
|
+
return out.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
3430
|
+
}
|
|
3431
|
+
function stripJsonComments(text) {
|
|
3432
|
+
let out = "";
|
|
3433
|
+
let inString = false;
|
|
3434
|
+
let escaped = false;
|
|
3435
|
+
for (let i = 0; i < text.length; i++) {
|
|
3436
|
+
const c = text[i];
|
|
3437
|
+
if (inString) {
|
|
3438
|
+
out += c;
|
|
3439
|
+
if (escaped) escaped = false;
|
|
3440
|
+
else if (c === "\\") escaped = true;
|
|
3441
|
+
else if (c === '"') inString = false;
|
|
3442
|
+
continue;
|
|
3443
|
+
}
|
|
3444
|
+
if (c === '"') {
|
|
3445
|
+
inString = true;
|
|
3446
|
+
out += c;
|
|
3447
|
+
continue;
|
|
3448
|
+
}
|
|
3449
|
+
if (c === "/" && text[i + 1] === "/") {
|
|
3450
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
3451
|
+
out += "\n";
|
|
3452
|
+
continue;
|
|
3453
|
+
}
|
|
3454
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
3455
|
+
i += 2;
|
|
3456
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
3457
|
+
i++;
|
|
3458
|
+
continue;
|
|
3459
|
+
}
|
|
3460
|
+
out += c;
|
|
3461
|
+
}
|
|
3462
|
+
return out;
|
|
3463
|
+
}
|
|
3464
|
+
function readWorkspacePackages(rootDir) {
|
|
3465
|
+
const start = import_node_path.default.resolve(rootDir);
|
|
3466
|
+
let dir = start;
|
|
3467
|
+
for (let i = 0; i < 6; i++) {
|
|
3468
|
+
const globs = workspaceGlobs(dir);
|
|
3469
|
+
if (globs.length > 0) {
|
|
3470
|
+
const packages = expandWorkspaceGlobs(dir, globs);
|
|
3471
|
+
const containsApp = packages.some(
|
|
3472
|
+
(p) => start === p.dir || start.startsWith(p.dir + import_node_path.default.sep)
|
|
3473
|
+
);
|
|
3474
|
+
return containsApp ? packages : [];
|
|
3475
|
+
}
|
|
3476
|
+
const parent = import_node_path.default.dirname(dir);
|
|
3477
|
+
if (parent === dir) break;
|
|
3478
|
+
dir = parent;
|
|
3479
|
+
}
|
|
3480
|
+
return [];
|
|
3481
|
+
}
|
|
3482
|
+
function workspaceGlobs(dir) {
|
|
3483
|
+
const pnpm = import_node_path.default.join(dir, "pnpm-workspace.yaml");
|
|
3484
|
+
if ((0, import_node_fs.existsSync)(pnpm)) {
|
|
3485
|
+
try {
|
|
3486
|
+
const lines = (0, import_node_fs.readFileSync)(pnpm, "utf-8").split(/\r?\n/);
|
|
3487
|
+
const out = [];
|
|
3488
|
+
let inPackages = false;
|
|
3489
|
+
for (const line of lines) {
|
|
3490
|
+
if (/^packages:/.test(line)) {
|
|
3491
|
+
inPackages = true;
|
|
3492
|
+
continue;
|
|
3493
|
+
}
|
|
3494
|
+
if (inPackages) {
|
|
3495
|
+
const m = /^\s*-\s*["']?([^"'#]+?)["']?\s*$/.exec(line);
|
|
3496
|
+
if (m) out.push(m[1].trim());
|
|
3497
|
+
else if (/^\S/.test(line)) break;
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
if (out.length) return out;
|
|
3501
|
+
} catch {
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
const pkgPath = import_node_path.default.join(dir, "package.json");
|
|
3505
|
+
if (!(0, import_node_fs.existsSync)(pkgPath)) return [];
|
|
3506
|
+
try {
|
|
3507
|
+
const pkg = JSON.parse((0, import_node_fs.readFileSync)(pkgPath, "utf-8"));
|
|
3508
|
+
const ws = pkg.workspaces;
|
|
3509
|
+
if (Array.isArray(ws)) return ws;
|
|
3510
|
+
if (ws && Array.isArray(ws.packages)) return ws.packages;
|
|
3511
|
+
} catch {
|
|
3512
|
+
}
|
|
3513
|
+
return [];
|
|
3514
|
+
}
|
|
3515
|
+
function expandWorkspaceGlobs(root, globs) {
|
|
3516
|
+
const out = [];
|
|
3517
|
+
const add2 = (dir) => {
|
|
3518
|
+
const pkgPath = import_node_path.default.join(dir, "package.json");
|
|
3519
|
+
if (!(0, import_node_fs.existsSync)(pkgPath)) return;
|
|
3520
|
+
try {
|
|
3521
|
+
const name = JSON.parse((0, import_node_fs.readFileSync)(pkgPath, "utf-8")).name;
|
|
3522
|
+
if (name) out.push({ name, dir });
|
|
3523
|
+
} catch {
|
|
3524
|
+
}
|
|
3525
|
+
};
|
|
3526
|
+
for (const glob2 of globs) {
|
|
3527
|
+
if (glob2.endsWith("/*")) {
|
|
3528
|
+
const parent = import_node_path.default.join(root, glob2.slice(0, -2));
|
|
3529
|
+
let entries = [];
|
|
3530
|
+
try {
|
|
3531
|
+
entries = (0, import_node_fs.readdirSync)(parent);
|
|
3532
|
+
} catch {
|
|
3533
|
+
continue;
|
|
3534
|
+
}
|
|
3535
|
+
for (const entry of entries) {
|
|
3536
|
+
const dir = import_node_path.default.join(parent, entry);
|
|
3537
|
+
try {
|
|
3538
|
+
if ((0, import_node_fs.statSync)(dir).isDirectory()) add2(dir);
|
|
3539
|
+
} catch {
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
} else if (!glob2.includes("*")) {
|
|
3543
|
+
add2(import_node_path.default.join(root, glob2));
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
return out.sort((a, b) => b.name.length - a.name.length);
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
// src/ast/navigation/expo-router.ts
|
|
3550
|
+
var import_node_fs2 = require("fs");
|
|
3551
|
+
var import_node_path2 = __toESM(require("path"));
|
|
3552
|
+
var ROUTE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
3553
|
+
var PLATFORM_SUFFIXES = [".ios", ".android", ".native", ".web"];
|
|
3554
|
+
var ROUTE_DIR_CANDIDATES = ["app", "src/app"];
|
|
3555
|
+
function baseName(file2) {
|
|
3556
|
+
let name = file2;
|
|
3557
|
+
for (const ext of ROUTE_EXTENSIONS) {
|
|
3558
|
+
if (name.endsWith(ext)) {
|
|
3559
|
+
name = name.slice(0, -ext.length);
|
|
3560
|
+
break;
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
for (const suffix of PLATFORM_SUFFIXES) {
|
|
3564
|
+
if (name.endsWith(suffix)) return name.slice(0, -suffix.length);
|
|
3565
|
+
}
|
|
3566
|
+
return name;
|
|
3567
|
+
}
|
|
3568
|
+
function isRouteFile(file2) {
|
|
3569
|
+
return ROUTE_EXTENSIONS.some((ext) => file2.endsWith(ext));
|
|
3570
|
+
}
|
|
3571
|
+
function isSpecial(base) {
|
|
3572
|
+
if (base === "_layout") return true;
|
|
3573
|
+
if (base.startsWith("+")) return base !== "+not-found";
|
|
3574
|
+
return base.startsWith("_");
|
|
3575
|
+
}
|
|
3576
|
+
function layoutType(file2) {
|
|
3577
|
+
try {
|
|
3578
|
+
const src = (0, import_node_fs2.readFileSync)(file2, "utf-8");
|
|
3579
|
+
if (/<(?:Native)?Tabs\b/.test(src)) return "tab";
|
|
3580
|
+
if (/<Drawer\b/.test(src)) return "drawer";
|
|
3581
|
+
} catch {
|
|
3582
|
+
}
|
|
3583
|
+
return "stack";
|
|
3584
|
+
}
|
|
3585
|
+
function findLayout(dir) {
|
|
3586
|
+
for (const ext of ROUTE_EXTENSIONS) {
|
|
3587
|
+
const candidate = import_node_path2.default.join(dir, "_layout" + ext);
|
|
3588
|
+
if ((0, import_node_fs2.existsSync)(candidate)) return candidate;
|
|
3589
|
+
}
|
|
3590
|
+
return null;
|
|
3591
|
+
}
|
|
3592
|
+
function analyzeExpoRouter(appRoot) {
|
|
3593
|
+
let routeDir = null;
|
|
3594
|
+
for (const candidate of ROUTE_DIR_CANDIDATES) {
|
|
3595
|
+
const full = import_node_path2.default.join(appRoot, candidate);
|
|
3596
|
+
if ((0, import_node_fs2.existsSync)(full) && findLayout(full)) {
|
|
3597
|
+
routeDir = full;
|
|
3598
|
+
break;
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
if (!routeDir) return null;
|
|
3602
|
+
const routes = [];
|
|
3603
|
+
const navigators = /* @__PURE__ */ new Map();
|
|
3604
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3605
|
+
const walk = (dir, segments, navigator) => {
|
|
3606
|
+
const layout = findLayout(dir);
|
|
3607
|
+
const current = layout ? segments.join("/") || "/" : navigator;
|
|
3608
|
+
if (layout && !navigators.has(current)) navigators.set(current, layoutType(layout));
|
|
3609
|
+
let entries;
|
|
3610
|
+
try {
|
|
3611
|
+
entries = (0, import_node_fs2.readdirSync)(dir);
|
|
3612
|
+
} catch {
|
|
3613
|
+
return;
|
|
3614
|
+
}
|
|
3615
|
+
for (const entry of entries.sort()) {
|
|
3616
|
+
const full = import_node_path2.default.join(dir, entry);
|
|
3617
|
+
let isDir = false;
|
|
3618
|
+
try {
|
|
3619
|
+
isDir = (0, import_node_fs2.statSync)(full).isDirectory();
|
|
3620
|
+
} catch {
|
|
3621
|
+
continue;
|
|
3622
|
+
}
|
|
3623
|
+
if (isDir) {
|
|
3624
|
+
if (entry === "node_modules" || entry.startsWith(".")) continue;
|
|
3625
|
+
walk(full, [...segments, entry], current);
|
|
3626
|
+
continue;
|
|
3627
|
+
}
|
|
3628
|
+
if (!isRouteFile(entry)) continue;
|
|
3629
|
+
const base = baseName(entry);
|
|
3630
|
+
if (isSpecial(base)) continue;
|
|
3631
|
+
const routeSegments = base === "index" ? segments : [...segments, base];
|
|
3632
|
+
const name = "/" + routeSegments.join("/");
|
|
3633
|
+
if (seen.has(name)) continue;
|
|
3634
|
+
seen.add(name);
|
|
3635
|
+
routes.push({
|
|
3636
|
+
name,
|
|
3637
|
+
componentFile: full,
|
|
3638
|
+
navigatorName: current,
|
|
3639
|
+
navigatorType: navigators.get(current) ?? "stack",
|
|
3640
|
+
params: routeSegments.filter((s) => s.startsWith("[") && s.endsWith("]")).map((s) => s.slice(1, -1).replace(/^\.\.\./, ""))
|
|
3641
|
+
});
|
|
3642
|
+
}
|
|
3643
|
+
};
|
|
3644
|
+
walk(routeDir, [], "/");
|
|
3645
|
+
return {
|
|
3646
|
+
routeDir: import_node_path2.default.relative(appRoot, routeDir),
|
|
3647
|
+
routes,
|
|
3648
|
+
navigators: [...navigators.entries()].map(([name, type]) => ({ name, type }))
|
|
3649
|
+
};
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
// src/analyzers/NavigationAnalyzer.ts
|
|
3653
|
+
var NAVIGATOR_FACTORIES = {
|
|
3654
|
+
createStackNavigator: "stack",
|
|
3655
|
+
createNativeStackNavigator: "stack",
|
|
3656
|
+
createBottomTabNavigator: "tab",
|
|
3657
|
+
createTabNavigator: "tab",
|
|
3658
|
+
createMaterialTopTabNavigator: "tab",
|
|
3659
|
+
createMaterialBottomTabNavigator: "tab",
|
|
3660
|
+
createDrawerNavigator: "drawer"
|
|
3661
|
+
};
|
|
3662
|
+
var CUSTOM_FACTORY = /^create[A-Za-z0-9]*(Navigator|Stack|Tabs?|Drawer)$/;
|
|
3663
|
+
function unwrapStaticScreen(value) {
|
|
3664
|
+
if (t8.isCallExpression(value)) {
|
|
3665
|
+
const arg = value.arguments[0];
|
|
3666
|
+
return arg ? unwrapStaticScreen(arg) : null;
|
|
3667
|
+
}
|
|
3668
|
+
if (t8.isObjectExpression(value)) {
|
|
3669
|
+
for (const prop of value.properties) {
|
|
3670
|
+
if (!t8.isObjectProperty(prop)) continue;
|
|
3671
|
+
const key = t8.isIdentifier(prop.key) ? prop.key.name : null;
|
|
3672
|
+
if (key === "screen" && t8.isExpression(prop.value)) return prop.value;
|
|
3673
|
+
}
|
|
3674
|
+
return null;
|
|
3675
|
+
}
|
|
3676
|
+
return t8.isExpression(value) ? value : null;
|
|
3677
|
+
}
|
|
3678
|
+
function inferNavigatorType(factoryName) {
|
|
3679
|
+
if (/drawer/i.test(factoryName)) return "drawer";
|
|
3680
|
+
if (/tab/i.test(factoryName)) return "tab";
|
|
3681
|
+
return "stack";
|
|
3682
|
+
}
|
|
3683
|
+
var NAVIGATION_EVIDENCE = new RegExp(
|
|
3684
|
+
// `\.Navigator`/`\.Screen` entram porque o JSX pode estar num arquivo que
|
|
3685
|
+
// não menciona fábrica nenhuma: `comapeo` declara `RootStack` em
|
|
3686
|
+
// `Stack/RootStack.ts` e escreve todas as 120 rotas em `Stack/index.tsx`,
|
|
3687
|
+
// que não cita `createNativeStackNavigator` uma vez sequer. Sem isto a
|
|
3688
|
+
// junção entre arquivos nunca chega a ser tentada.
|
|
3689
|
+
// Sem `\\b` depois da palavra — `createNativeStackNavigatorWithAuth` não tem
|
|
3690
|
+
// fronteira ali, e exigi-la fecharia o portão para o arquivo que declara os
|
|
3691
|
+
// seis navegadores do `bluesky`.
|
|
3692
|
+
`create[A-Za-z0-9]*(?:Navigator|Stack|Tabs?|Drawer)|ParamList|\\.Navigator\\b|\\.Screen\\b`
|
|
3693
|
+
);
|
|
2643
3694
|
var NavigationAnalyzer = class {
|
|
2644
3695
|
config;
|
|
2645
3696
|
/** Extra glob patterns for navigation file discovery (added to defaults) */
|
|
2646
3697
|
navigationInclude;
|
|
2647
3698
|
/** Extra glob patterns to exclude from navigation analysis */
|
|
2648
3699
|
navigationExclude;
|
|
3700
|
+
/** Exposto para a composição reusar o cache de AST em vez de reparsear a árvore. */
|
|
3701
|
+
graph;
|
|
3702
|
+
/**
|
|
3703
|
+
* Rota → arquivo do componente que ela monta. É a única ligação entre o grafo
|
|
3704
|
+
* de navegação e a árvore de telas; sem ela as duas metades do documento
|
|
3705
|
+
* falam de coisas diferentes. Populada por `analyze()`.
|
|
3706
|
+
*/
|
|
3707
|
+
routeTargets = /* @__PURE__ */ new Map();
|
|
3708
|
+
/**
|
|
3709
|
+
* Arquivos que DECLARAM uma fábrica de navegador.
|
|
3710
|
+
*
|
|
3711
|
+
* Um navegador aninhado é montado como `component=` de uma rota — no
|
|
3712
|
+
* `apps/example-app`, `<Stack.Screen name="Main" component={MainTabNavigator} />`.
|
|
3713
|
+
* Promover esse arquivo a tela criaria seis "telas" sem uma única ação, que é
|
|
3714
|
+
* exatamente o falso positivo que o corpus existe para não repetir. Um
|
|
3715
|
+
* navegador é um contêiner de rotas; a tela está um nível abaixo.
|
|
3716
|
+
*
|
|
3717
|
+
* Entram aqui os dois lados: o arquivo que CHAMA a fábrica e o arquivo que
|
|
3718
|
+
* RENDERIZA `<X.Navigator>`. Não são o mesmo — `bluewallet` declara
|
|
3719
|
+
* `DetailViewStack` num arquivo e escreve o JSX em
|
|
3720
|
+
* `navigation/DetailViewScreensStack.tsx`, e checar só o primeiro deixava o
|
|
3721
|
+
* segundo entrar como tela.
|
|
3722
|
+
*/
|
|
3723
|
+
navigatorFiles = /* @__PURE__ */ new Set();
|
|
3724
|
+
/** Preenchido por `analyze()`. Vazio antes disso. */
|
|
3725
|
+
diagnostics = {
|
|
3726
|
+
filesScanned: 0,
|
|
3727
|
+
filesWithEvidence: 0,
|
|
3728
|
+
navigatorsDeclared: 0,
|
|
3729
|
+
navigatorsWithJsx: 0,
|
|
3730
|
+
routes: 0,
|
|
3731
|
+
routesLinkedToFile: 0
|
|
3732
|
+
};
|
|
2649
3733
|
constructor(config, options) {
|
|
2650
3734
|
this.config = config;
|
|
3735
|
+
this.graph = new ModuleGraph(config.rootDir);
|
|
2651
3736
|
this.navigationInclude = options?.navigationInclude ?? [];
|
|
2652
3737
|
this.navigationExclude = options?.navigationExclude ?? [];
|
|
2653
3738
|
}
|
|
2654
3739
|
/** Build the full navigation graph */
|
|
2655
3740
|
async analyze() {
|
|
2656
3741
|
const navigationFiles = await this.findNavigationFiles();
|
|
2657
|
-
const parsedNavigators = [];
|
|
2658
3742
|
const typeExports = [];
|
|
3743
|
+
this.routeTargets.clear();
|
|
3744
|
+
const declarations = /* @__PURE__ */ new Map();
|
|
3745
|
+
const evidenceFiles = [];
|
|
3746
|
+
this.navigatorFiles.clear();
|
|
2659
3747
|
for (const filePath of navigationFiles) {
|
|
2660
3748
|
try {
|
|
2661
3749
|
const content = await import_fs4.promises.readFile(filePath, "utf-8");
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
3750
|
+
if (!NAVIGATION_EVIDENCE.test(content)) continue;
|
|
3751
|
+
evidenceFiles.push(filePath);
|
|
3752
|
+
for (const decl of this.collectDeclarations(filePath)) {
|
|
3753
|
+
declarations.set(`${decl.file}#${decl.name}`, decl);
|
|
3754
|
+
if (decl.confident) this.navigatorFiles.add(decl.file);
|
|
3755
|
+
}
|
|
3756
|
+
typeExports.push(...this.parseParamTypes(content, filePath));
|
|
2666
3757
|
} catch (error2) {
|
|
2667
3758
|
console.warn(`Failed to parse ${filePath}:`, error2);
|
|
2668
3759
|
}
|
|
2669
3760
|
}
|
|
3761
|
+
const parsedNavigators = [];
|
|
3762
|
+
for (const filePath of evidenceFiles) {
|
|
3763
|
+
try {
|
|
3764
|
+
parsedNavigators.push(...this.parseNavigatorUsages(filePath, declarations));
|
|
3765
|
+
} catch (error2) {
|
|
3766
|
+
console.warn(`Failed to parse navigators in ${filePath}:`, error2);
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
let detachedScreens = 0;
|
|
3770
|
+
for (const filePath of evidenceFiles) {
|
|
3771
|
+
try {
|
|
3772
|
+
for (const detached of this.parseDetachedScreens(filePath, declarations)) {
|
|
3773
|
+
const existing = parsedNavigators.find(
|
|
3774
|
+
(nav) => nav.name === detached.name && nav.type === detached.type
|
|
3775
|
+
);
|
|
3776
|
+
if (!existing) {
|
|
3777
|
+
parsedNavigators.push(detached);
|
|
3778
|
+
detachedScreens += detached.screens.length;
|
|
3779
|
+
continue;
|
|
3780
|
+
}
|
|
3781
|
+
const seen = new Set(existing.screens.map((screen) => screen.name));
|
|
3782
|
+
for (const screen of detached.screens) {
|
|
3783
|
+
if (seen.has(screen.name)) continue;
|
|
3784
|
+
seen.add(screen.name);
|
|
3785
|
+
existing.screens.push(screen);
|
|
3786
|
+
detachedScreens++;
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
} catch (error2) {
|
|
3790
|
+
console.warn(`Failed to parse detached screens in ${filePath}:`, error2);
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
const expo = analyzeExpoRouter(this.config.rootDir);
|
|
3794
|
+
if (expo) {
|
|
3795
|
+
console.log(
|
|
3796
|
+
`[NavigationAnalyzer] expo-router em "${expo.routeDir}": ${expo.routes.length} rotas, ${expo.navigators.length} layouts`
|
|
3797
|
+
);
|
|
3798
|
+
this.diagnostics.fileBasedRouter = {
|
|
3799
|
+
kind: "expo-router",
|
|
3800
|
+
routeDir: expo.routeDir,
|
|
3801
|
+
routes: expo.routes.length
|
|
3802
|
+
};
|
|
3803
|
+
const byNavigator = /* @__PURE__ */ new Map();
|
|
3804
|
+
for (const nav of expo.navigators) {
|
|
3805
|
+
byNavigator.set(nav.name, { name: nav.name, type: nav.type, screens: [] });
|
|
3806
|
+
}
|
|
3807
|
+
for (const route of expo.routes) {
|
|
3808
|
+
const navigator = byNavigator.get(route.navigatorName) ?? byNavigator.set(route.navigatorName, {
|
|
3809
|
+
name: route.navigatorName,
|
|
3810
|
+
type: route.navigatorType,
|
|
3811
|
+
screens: []
|
|
3812
|
+
}).get(route.navigatorName);
|
|
3813
|
+
navigator.screens.push({
|
|
3814
|
+
name: route.name,
|
|
3815
|
+
navigatorName: route.navigatorName,
|
|
3816
|
+
navigatorType: route.navigatorType,
|
|
3817
|
+
componentFile: route.componentFile,
|
|
3818
|
+
...route.params.length ? {
|
|
3819
|
+
params: route.params.map((name) => ({ name, type: "string", required: true }))
|
|
3820
|
+
} : {}
|
|
3821
|
+
});
|
|
3822
|
+
this.routeTargets.set(route.name, route.componentFile);
|
|
3823
|
+
}
|
|
3824
|
+
parsedNavigators.push(...byNavigator.values());
|
|
3825
|
+
}
|
|
3826
|
+
let staticOnly = 0;
|
|
3827
|
+
for (const decl of declarations.values()) {
|
|
3828
|
+
if (!decl.confident || !decl.staticScreens?.length) continue;
|
|
3829
|
+
const existing = parsedNavigators.find((n) => n.name === decl.name && n.type === decl.type);
|
|
3830
|
+
if (existing) {
|
|
3831
|
+
const seen = new Set(existing.screens.map((s) => s.name));
|
|
3832
|
+
for (const screen of decl.staticScreens) {
|
|
3833
|
+
if (!seen.has(screen.name)) existing.screens.push(screen);
|
|
3834
|
+
}
|
|
3835
|
+
continue;
|
|
3836
|
+
}
|
|
3837
|
+
parsedNavigators.push({ name: decl.name, type: decl.type, screens: decl.staticScreens });
|
|
3838
|
+
staticOnly++;
|
|
3839
|
+
}
|
|
3840
|
+
const confidentDeclarations = [...declarations.values()].filter((d) => d.confident).length;
|
|
3841
|
+
const uniqueRoutes = new Set(
|
|
3842
|
+
parsedNavigators.flatMap((nav) => nav.screens.map((screen) => screen.name))
|
|
3843
|
+
);
|
|
3844
|
+
this.diagnostics = {
|
|
3845
|
+
...this.diagnostics,
|
|
3846
|
+
filesScanned: navigationFiles.length,
|
|
3847
|
+
filesWithEvidence: evidenceFiles.length,
|
|
3848
|
+
navigatorsDeclared: confidentDeclarations,
|
|
3849
|
+
navigatorsWithJsx: parsedNavigators.length,
|
|
3850
|
+
routes: uniqueRoutes.size,
|
|
3851
|
+
// `routeTargets` é indexado por nome de rota, e uma rota pode ter sido
|
|
3852
|
+
// registrada por um navegador que não sobreviveu. O mínimo evita a
|
|
3853
|
+
// cobertura acima de 100% que a medição do corpus expôs no `coopcycle`.
|
|
3854
|
+
routesLinkedToFile: Math.min(this.routeTargets.size, uniqueRoutes.size)
|
|
3855
|
+
};
|
|
3856
|
+
console.log(
|
|
3857
|
+
`[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`
|
|
3858
|
+
);
|
|
2670
3859
|
this.attachParamsToNavigators(parsedNavigators, typeExports);
|
|
2671
3860
|
return this.buildNavigationGraph(parsedNavigators);
|
|
2672
3861
|
}
|
|
2673
|
-
/**
|
|
3862
|
+
/** Fase 1: as fábricas `create*Navigator()` atribuídas a um nome neste arquivo. */
|
|
3863
|
+
collectDeclarations(filePath) {
|
|
3864
|
+
const ast = this.graph.parse(filePath);
|
|
3865
|
+
if (!ast) return [];
|
|
3866
|
+
const out = /* @__PURE__ */ new Map();
|
|
3867
|
+
(0, import_traverse6.default)(ast, {
|
|
3868
|
+
CallExpression: (nodePath) => {
|
|
3869
|
+
const callee = nodePath.node.callee;
|
|
3870
|
+
if (!t8.isIdentifier(callee)) return;
|
|
3871
|
+
const known = NAVIGATOR_FACTORIES[callee.name] ?? (CUSTOM_FACTORY.test(callee.name) ? inferNavigatorType(callee.name) : null);
|
|
3872
|
+
const type = known ?? inferNavigatorType(callee.name);
|
|
3873
|
+
const confident = known !== null;
|
|
3874
|
+
let up = nodePath.parentPath;
|
|
3875
|
+
for (let i = 0; i < 4 && up; i++) {
|
|
3876
|
+
if (t8.isVariableDeclarator(up.node)) break;
|
|
3877
|
+
if (!t8.isMemberExpression(up.node) && !t8.isCallExpression(up.node)) {
|
|
3878
|
+
up = null;
|
|
3879
|
+
break;
|
|
3880
|
+
}
|
|
3881
|
+
up = up.parentPath;
|
|
3882
|
+
}
|
|
3883
|
+
const declarator = up?.node;
|
|
3884
|
+
if (!declarator || !t8.isVariableDeclarator(declarator) || !t8.isIdentifier(declarator.id)) {
|
|
3885
|
+
return;
|
|
3886
|
+
}
|
|
3887
|
+
const name = declarator.id.name;
|
|
3888
|
+
if (out.get(name)?.confident) return;
|
|
3889
|
+
const staticScreens = confident ? this.parseStaticScreens(filePath, nodePath.node.arguments[0], name, type) : [];
|
|
3890
|
+
out.set(name, {
|
|
3891
|
+
file: filePath,
|
|
3892
|
+
name,
|
|
3893
|
+
type,
|
|
3894
|
+
confident,
|
|
3895
|
+
...staticScreens.length ? { staticScreens } : {}
|
|
3896
|
+
});
|
|
3897
|
+
}
|
|
3898
|
+
});
|
|
3899
|
+
return [...out.values()];
|
|
3900
|
+
}
|
|
3901
|
+
/**
|
|
3902
|
+
* As rotas de `createXNavigator({ screens: { … } })`.
|
|
3903
|
+
*
|
|
3904
|
+
* Quatro formas no corpus, todas em `rocketchat`:
|
|
3905
|
+
* `NewServerView` — shorthand, nome = componente
|
|
3906
|
+
* `LoginView: createNativeStackScreen({ screen })` — embrulho da própria lib
|
|
3907
|
+
* `SelectListView: SelectListViewScreen` — identificador direto
|
|
3908
|
+
* `ChatsStackNavigator: ChatsStack` — navegador aninhado
|
|
3909
|
+
*
|
|
3910
|
+
* `groups: { G: { screens: { … } } }` também entra: faz parte da API estática
|
|
3911
|
+
* e agrupar rotas não muda o que elas são.
|
|
3912
|
+
*/
|
|
3913
|
+
parseStaticScreens(filePath, config, navigatorName, navigatorType, depth = 0) {
|
|
3914
|
+
if (!config || !t8.isObjectExpression(config) || depth > 4) return [];
|
|
3915
|
+
const out = [];
|
|
3916
|
+
for (const prop of config.properties) {
|
|
3917
|
+
if (!t8.isObjectProperty(prop)) continue;
|
|
3918
|
+
const key = t8.isIdentifier(prop.key) ? prop.key.name : t8.isStringLiteral(prop.key) ? prop.key.value : null;
|
|
3919
|
+
if (!key) continue;
|
|
3920
|
+
if (key === "groups" && t8.isObjectExpression(prop.value)) {
|
|
3921
|
+
for (const group of prop.value.properties) {
|
|
3922
|
+
if (!t8.isObjectProperty(group) || !t8.isExpression(group.value)) continue;
|
|
3923
|
+
out.push(
|
|
3924
|
+
...this.parseStaticScreens(
|
|
3925
|
+
filePath,
|
|
3926
|
+
group.value,
|
|
3927
|
+
navigatorName,
|
|
3928
|
+
navigatorType,
|
|
3929
|
+
depth + 1
|
|
3930
|
+
)
|
|
3931
|
+
);
|
|
3932
|
+
}
|
|
3933
|
+
continue;
|
|
3934
|
+
}
|
|
3935
|
+
if (key !== "screens" || !t8.isObjectExpression(prop.value)) continue;
|
|
3936
|
+
for (const entry of prop.value.properties) {
|
|
3937
|
+
if (!t8.isObjectProperty(entry)) continue;
|
|
3938
|
+
const routeName = t8.isIdentifier(entry.key) ? entry.key.name : t8.isStringLiteral(entry.key) ? entry.key.value : null;
|
|
3939
|
+
if (!routeName) continue;
|
|
3940
|
+
const componentExpr = unwrapStaticScreen(entry.value);
|
|
3941
|
+
const componentFile = componentExpr ? this.graph.componentFile(filePath, componentExpr) : null;
|
|
3942
|
+
if (componentFile) this.routeTargets.set(routeName, componentFile);
|
|
3943
|
+
out.push({
|
|
3944
|
+
name: routeName,
|
|
3945
|
+
navigatorName,
|
|
3946
|
+
navigatorType,
|
|
3947
|
+
...componentFile ? { componentFile } : {}
|
|
3948
|
+
});
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
return out;
|
|
3952
|
+
}
|
|
3953
|
+
/**
|
|
3954
|
+
* Todo arquivo-fonte do projeto — não os que ficam num diretório com o nome
|
|
3955
|
+
* certo.
|
|
3956
|
+
*
|
|
3957
|
+
* POR QUE ISTO MUDOU. A lista anterior era `**\/navigation/**`,
|
|
3958
|
+
* `**\/navigator*` e `**\/routes*`, o que fazia da descoberta de navegação uma
|
|
3959
|
+
* convenção de caminho. Medido contra 20 apps React Native de terceiros
|
|
3960
|
+
* (`qa/eval-corpus`), o filtro errava por motivos que nada têm a ver com o
|
|
3961
|
+
* app não ter navegação:
|
|
3962
|
+
*
|
|
3963
|
+
* - `pocketpal` declara 4 navegadores em `App.tsx` e dentro de `src/screens/`;
|
|
3964
|
+
* - `comapeo` usa `src/frontend/Navigation/` — `N` maiúsculo, e o glob é
|
|
3965
|
+
* sensível a caixa;
|
|
3966
|
+
* - `abacus` usa `src/routes/index.tsx` — `routes` é o DIRETÓRIO, e o glob
|
|
3967
|
+
* pedia um arquivo chamado `routes*`;
|
|
3968
|
+
* - `discourse` declara em `js/Discourse.js` — o glob só aceitava `.ts`/`.tsx`;
|
|
3969
|
+
* - `rainbow` tem 11 arquivos com navegador e o glob alcançava 1.
|
|
3970
|
+
*
|
|
3971
|
+
* Varrer tudo não custa uma varredura nova: `ReactNativePlatformAnalyzer` já
|
|
3972
|
+
* globa e parseia a árvore inteira para `ComponentAnalyzer` e `FormAnalyzer`.
|
|
3973
|
+
* O que segura o custo aqui é o portão por evidência em `analyze()`, que só
|
|
3974
|
+
* parseia arquivo cujo texto menciona uma fábrica de navegador.
|
|
3975
|
+
*
|
|
3976
|
+
* Os globs antigos continuam na lista: se um projeto restringir `include`, o
|
|
3977
|
+
* que era encontrado antes continua sendo.
|
|
3978
|
+
*/
|
|
2674
3979
|
async findNavigationFiles() {
|
|
2675
3980
|
const patterns = [
|
|
3981
|
+
...this.config.include ?? ["**/*.{ts,tsx,js,jsx}"],
|
|
3982
|
+
// Piso de compatibilidade — nunca encontrar MENOS que a versão anterior.
|
|
2676
3983
|
"**/navigation/**/*.{ts,tsx}",
|
|
2677
3984
|
"**/navigator*.{ts,tsx}",
|
|
2678
3985
|
"**/routes*.{ts,tsx}",
|
|
@@ -2683,91 +3990,188 @@ var NavigationAnalyzer = class {
|
|
|
2683
3990
|
"**/node_modules/**",
|
|
2684
3991
|
"**/dist/**",
|
|
2685
3992
|
"**/build/**",
|
|
3993
|
+
// Um navegador declarado dentro de um teste é fixture, não a navegação
|
|
3994
|
+
// do app. Isto passou a importar quando a varredura deixou de ser por
|
|
3995
|
+
// caminho: em `expensify`, os únicos arquivos que o glob antigo
|
|
3996
|
+
// alcançava eram três de `tests/`.
|
|
3997
|
+
"**/__tests__/**",
|
|
3998
|
+
"**/test/**",
|
|
3999
|
+
"**/tests/**",
|
|
4000
|
+
"**/*.test.{ts,tsx,js,jsx}",
|
|
4001
|
+
"**/*.tests.{ts,tsx,js,jsx}",
|
|
4002
|
+
"**/*.spec.{ts,tsx,js,jsx}",
|
|
4003
|
+
// Sufixo `Test` sem ponto: `expensify` chama os fixtures dele de
|
|
4004
|
+
// `LegalNameStepTest.tsx`, e cada um monta um `<Stack.Screen>` próprio.
|
|
4005
|
+
// Auditando as 144 rotas lidas dele contra `SCREENS.ts`, 2 vinham daqui —
|
|
4006
|
+
// destinos que existem no teste e não no app, para os quais o agente
|
|
4007
|
+
// tentaria navegar.
|
|
4008
|
+
"**/*Test.{ts,tsx,js,jsx}",
|
|
4009
|
+
"**/*Tests.{ts,tsx,js,jsx}",
|
|
2686
4010
|
...this.config.exclude || [],
|
|
2687
4011
|
// §5: Add user-configured navigation excludes
|
|
2688
4012
|
...this.navigationExclude
|
|
2689
4013
|
];
|
|
2690
|
-
const files = await (
|
|
4014
|
+
const files = await globSorted(patterns, {
|
|
2691
4015
|
cwd: this.config.rootDir,
|
|
2692
4016
|
ignore: excludePatterns
|
|
2693
4017
|
});
|
|
2694
4018
|
return files.map((file2) => import_path4.default.join(this.config.rootDir, file2));
|
|
2695
4019
|
}
|
|
2696
|
-
/**
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
(0, import_traverse5.default)(ast, {
|
|
2711
|
-
// Detect createStackNavigator / createTabNavigator / createDrawerNavigator calls
|
|
2712
|
-
CallExpression: (nodePath) => {
|
|
2713
|
-
const { node } = nodePath;
|
|
2714
|
-
const callee = node.callee;
|
|
2715
|
-
let navigatorType = null;
|
|
2716
|
-
if (t5.isIdentifier(callee) && callee.name === "createNativeStackNavigator") {
|
|
2717
|
-
navigatorType = "stack";
|
|
2718
|
-
} else if (t5.isIdentifier(callee) && callee.name === "createStackNavigator") {
|
|
2719
|
-
navigatorType = "stack";
|
|
2720
|
-
} else if (t5.isIdentifier(callee) && callee.name === "createBottomTabNavigator") {
|
|
2721
|
-
navigatorType = "tab";
|
|
2722
|
-
} else if (t5.isIdentifier(callee) && callee.name === "createTabNavigator") {
|
|
2723
|
-
navigatorType = "tab";
|
|
2724
|
-
} else if (t5.isIdentifier(callee) && callee.name === "createDrawerNavigator") {
|
|
2725
|
-
navigatorType = "drawer";
|
|
2726
|
-
}
|
|
2727
|
-
if (navigatorType) {
|
|
2728
|
-
const parent = nodePath.parent;
|
|
2729
|
-
if (t5.isVariableDeclarator(parent) && t5.isIdentifier(parent.id)) {
|
|
2730
|
-
const varName = parent.id.name;
|
|
2731
|
-
navigatorCalls.set(varName, {
|
|
2732
|
-
name: varName,
|
|
2733
|
-
type: navigatorType,
|
|
2734
|
-
screens: []
|
|
2735
|
-
});
|
|
2736
|
-
}
|
|
2737
|
-
}
|
|
2738
|
-
},
|
|
2739
|
-
// Detect Stack.Navigator / Tab.Navigator JSX elements
|
|
2740
|
-
JSXElement: (nodePath) => {
|
|
2741
|
-
const { node } = nodePath;
|
|
2742
|
-
const openingElement = node.openingElement;
|
|
2743
|
-
if (t5.isJSXMemberExpression(openingElement.name) && t5.isJSXIdentifier(openingElement.name.object)) {
|
|
2744
|
-
const objectName = openingElement.name.object.name;
|
|
2745
|
-
const propertyName = t5.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
|
|
2746
|
-
if (propertyName === "Navigator") {
|
|
2747
|
-
const navigator = navigatorCalls.get(objectName);
|
|
2748
|
-
if (navigator) {
|
|
2749
|
-
const initialRouteAttr = openingElement.attributes.find(
|
|
2750
|
-
(attr) => t5.isJSXAttribute(attr) && t5.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
|
|
2751
|
-
);
|
|
2752
|
-
if (t5.isJSXAttribute(initialRouteAttr) && t5.isStringLiteral(initialRouteAttr.value)) {
|
|
2753
|
-
navigator.initialRouteName = initialRouteAttr.value.value;
|
|
2754
|
-
}
|
|
2755
|
-
const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
|
|
2756
|
-
screensByNavigator.set(objectName, screens);
|
|
2757
|
-
}
|
|
2758
|
-
}
|
|
2759
|
-
}
|
|
4020
|
+
/**
|
|
4021
|
+
* Fase 2: o JSX `<X.Navigator>` deste arquivo, ligado à declaração de `X` —
|
|
4022
|
+
* que pode estar aqui ou em qualquer arquivo que este importe.
|
|
4023
|
+
*/
|
|
4024
|
+
parseNavigatorUsages(filePath, declarations) {
|
|
4025
|
+
const ast = this.graph.parse(filePath);
|
|
4026
|
+
if (!ast) return [];
|
|
4027
|
+
const found = /* @__PURE__ */ new Map();
|
|
4028
|
+
(0, import_traverse6.default)(ast, {
|
|
4029
|
+
JSXElement: (nodePath) => {
|
|
4030
|
+
const { node } = nodePath;
|
|
4031
|
+
const openingElement = node.openingElement;
|
|
4032
|
+
if (!t8.isJSXMemberExpression(openingElement.name) || !t8.isJSXIdentifier(openingElement.name.object)) {
|
|
4033
|
+
return;
|
|
2760
4034
|
}
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
4035
|
+
const objectName = openingElement.name.object.name;
|
|
4036
|
+
const propertyName = t8.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
|
|
4037
|
+
if (propertyName !== "Navigator") return;
|
|
4038
|
+
const decl = this.resolveNavigator(filePath, objectName, declarations);
|
|
4039
|
+
if (!decl) return;
|
|
4040
|
+
const navigator = found.get(objectName) ?? {
|
|
4041
|
+
name: objectName,
|
|
4042
|
+
type: decl.type,
|
|
4043
|
+
screens: []
|
|
4044
|
+
};
|
|
4045
|
+
const initialRouteAttr = openingElement.attributes.find(
|
|
4046
|
+
(attr) => t8.isJSXAttribute(attr) && t8.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
|
|
4047
|
+
);
|
|
4048
|
+
if (t8.isJSXAttribute(initialRouteAttr)) {
|
|
4049
|
+
const initial = this.attributeString(filePath, initialRouteAttr);
|
|
4050
|
+
if (initial) navigator.initialRouteName = initial;
|
|
4051
|
+
}
|
|
4052
|
+
this.navigatorFiles.add(filePath);
|
|
4053
|
+
this.navigatorFiles.add(decl.file);
|
|
4054
|
+
navigator.screens = this.extractScreensFromNavigator(filePath, node, objectName, decl.type);
|
|
4055
|
+
found.set(objectName, navigator);
|
|
4056
|
+
}
|
|
4057
|
+
});
|
|
4058
|
+
return [...found.values()];
|
|
4059
|
+
}
|
|
4060
|
+
/**
|
|
4061
|
+
* Rotas declaradas FORA de qualquer `<X.Navigator>`.
|
|
4062
|
+
*
|
|
4063
|
+
* O CASO. A fase 2 desce a partir do `<X.Navigator>` e lê as `<X.Screen>`
|
|
4064
|
+
* que estão DENTRO dele. Dois alvos do corpus não escrevem assim, e entre os
|
|
4065
|
+
* dois são 171 rotas invisíveis:
|
|
4066
|
+
*
|
|
4067
|
+
* `bluesky` — `function commonScreens(Stack: typeof Flat) { return (<>
|
|
4068
|
+
* <Stack.Screen name="NotFound" … /> … </>) }`, chamada de
|
|
4069
|
+
* dentro de seis navegadores diferentes. 70 rotas.
|
|
4070
|
+
* `comapeo` — `export const createAppScreens = ({intl}) => (<>
|
|
4071
|
+
* <RootStack.Group><RootStack.Screen … /></RootStack.Group></>)`,
|
|
4072
|
+
* num arquivo sem `<RootStack.Navigator>` nenhum. 101 rotas.
|
|
4073
|
+
*
|
|
4074
|
+
* A REGRA. Uma `<X.Screen name="…">` sem `<Y.Navigator>` ancestral é rota do
|
|
4075
|
+
* navegador ao qual `X` se resolve. Não há palpite em jogo: o nome da rota é
|
|
4076
|
+
* literal do fonte (ou constante que o resolvedor segue), e `X` precisa
|
|
4077
|
+
* chegar a uma declaração de navegador que já existe. O que não resolve não
|
|
4078
|
+
* vira nada.
|
|
4079
|
+
*
|
|
4080
|
+
* O ancestral é o que evita contar duas vezes — dentro do `<X.Navigator>` a
|
|
4081
|
+
* fase 2 já leu, e somar aqui duplicaria cada rota do corpus inteiro.
|
|
4082
|
+
*
|
|
4083
|
+
* DUAS FORMAS DE RESOLVER `X`, e a segunda é o que o `bluesky` exige. A
|
|
4084
|
+
* primeira é a de sempre (declaração local, ou `import` seguido até a
|
|
4085
|
+
* origem) e resolve o `comapeo`. A segunda lê a ANOTAÇÃO DE TIPO do
|
|
4086
|
+
* parâmetro: em `commonScreens(Stack: typeof Flat)`, quem diz que `Stack` é
|
|
4087
|
+
* o navegador `Flat` é o próprio app, no fonte.
|
|
4088
|
+
*
|
|
4089
|
+
* O QUE ISTO NÃO FAZ. As 70 do `bluesky` ficam atribuídas a `Flat` — que as
|
|
4090
|
+
* contém de fato (`{commonScreens(Flat, numUnread)}`) — e não aos outros
|
|
4091
|
+
* cinco stacks que também chamam a mesma função. Seguir os seis pontos de
|
|
4092
|
+
* chamada é análise interprocedural, e o ganho seria só de atribuição: o nome
|
|
4093
|
+
* da rota e o arquivo da tela, que é o que o agente usa, já saem certos.
|
|
4094
|
+
*/
|
|
4095
|
+
parseDetachedScreens(filePath, declarations) {
|
|
4096
|
+
const ast = this.graph.parse(filePath);
|
|
4097
|
+
if (!ast) return [];
|
|
4098
|
+
const byNavigator = /* @__PURE__ */ new Map();
|
|
4099
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
4100
|
+
(0, import_traverse6.default)(ast, {
|
|
4101
|
+
JSXElement: (nodePath) => {
|
|
4102
|
+
const name = nodePath.node.openingElement.name;
|
|
4103
|
+
if (!t8.isJSXMemberExpression(name) || !t8.isJSXIdentifier(name.object)) return;
|
|
4104
|
+
if (!t8.isJSXIdentifier(name.property) || name.property.name !== "Screen") return;
|
|
4105
|
+
const insideNavigator = nodePath.findParent((parent) => {
|
|
4106
|
+
if (!parent.isJSXElement()) return false;
|
|
4107
|
+
const parentName = parent.node.openingElement.name;
|
|
4108
|
+
return t8.isJSXMemberExpression(parentName) && t8.isJSXIdentifier(parentName.property) && parentName.property.name === "Navigator";
|
|
4109
|
+
});
|
|
4110
|
+
if (insideNavigator) return;
|
|
4111
|
+
const local = name.object.name;
|
|
4112
|
+
if (!resolved.has(local)) {
|
|
4113
|
+
resolved.set(
|
|
4114
|
+
local,
|
|
4115
|
+
this.resolveDetachedNavigator(filePath, local, nodePath, declarations)
|
|
4116
|
+
);
|
|
4117
|
+
}
|
|
4118
|
+
const decl = resolved.get(local);
|
|
4119
|
+
if (!decl) return;
|
|
4120
|
+
const screen = this.parseScreenElement(filePath, nodePath.node, decl.name, decl.type);
|
|
4121
|
+
if (!screen) return;
|
|
4122
|
+
const nav = byNavigator.get(decl.name) ?? {
|
|
4123
|
+
name: decl.name,
|
|
4124
|
+
type: decl.type,
|
|
4125
|
+
screens: []
|
|
4126
|
+
};
|
|
4127
|
+
if (!nav.screens.some((existing) => existing.name === screen.name))
|
|
4128
|
+
nav.screens.push(screen);
|
|
4129
|
+
byNavigator.set(decl.name, nav);
|
|
4130
|
+
}
|
|
4131
|
+
});
|
|
4132
|
+
return [...byNavigator.values()];
|
|
4133
|
+
}
|
|
4134
|
+
/**
|
|
4135
|
+
* De um `X` usado como `<X.Screen>` até a declaração do navegador.
|
|
4136
|
+
*
|
|
4137
|
+
* Além do caminho normal — declaração local ou `import` seguido até a origem
|
|
4138
|
+
* — aceita `X` como PARÂMETRO anotado com `typeof Y`. É o idioma do
|
|
4139
|
+
* `bluesky`, e a anotação é declaração do app: nada aqui é inferido do nome.
|
|
4140
|
+
*/
|
|
4141
|
+
resolveDetachedNavigator(filePath, localName, nodePath, declarations) {
|
|
4142
|
+
const direct = this.resolveNavigator(filePath, localName, declarations);
|
|
4143
|
+
if (direct) return direct;
|
|
4144
|
+
const binding = nodePath.scope.getBinding(localName);
|
|
4145
|
+
if (!binding || binding.kind !== "param") return null;
|
|
4146
|
+
const param = binding.path.node;
|
|
4147
|
+
if (!t8.isIdentifier(param) || !t8.isTSTypeAnnotation(param.typeAnnotation)) return null;
|
|
4148
|
+
const annotation = param.typeAnnotation.typeAnnotation;
|
|
4149
|
+
if (!t8.isTSTypeQuery(annotation) || !t8.isIdentifier(annotation.exprName)) return null;
|
|
4150
|
+
return this.resolveNavigator(filePath, annotation.exprName.name, declarations);
|
|
4151
|
+
}
|
|
4152
|
+
/**
|
|
4153
|
+
* De `<X.Navigator>` até a declaração de `X`.
|
|
4154
|
+
*
|
|
4155
|
+
* Primeiro no próprio arquivo; se não estiver, segue o `import` até onde `X`
|
|
4156
|
+
* é definido. Resolver pelo import é o que torna a junção entre arquivos
|
|
4157
|
+
* segura: dois `Stack` de arquivos diferentes nunca colidem, porque a chave
|
|
4158
|
+
* é o arquivo de DEFINIÇÃO.
|
|
4159
|
+
*/
|
|
4160
|
+
resolveNavigator(filePath, localName, declarations) {
|
|
4161
|
+
const local = declarations.get(`${filePath}#${localName}`);
|
|
4162
|
+
if (local) return local;
|
|
4163
|
+
const origin = this.graph.resolveBinding(filePath, localName);
|
|
4164
|
+
if (!origin) return null;
|
|
4165
|
+
return declarations.get(`${origin.file}#${origin.name}`) ?? null;
|
|
4166
|
+
}
|
|
4167
|
+
/** O valor string de um atributo JSX, resolvendo constante importada. */
|
|
4168
|
+
attributeString(filePath, attr) {
|
|
4169
|
+
const value = attr.value;
|
|
4170
|
+
if (t8.isStringLiteral(value)) return value.value;
|
|
4171
|
+
if (t8.isJSXExpressionContainer(value) && t8.isExpression(value.expression)) {
|
|
4172
|
+
return this.graph.stringConstant(filePath, value.expression);
|
|
2769
4173
|
}
|
|
2770
|
-
return
|
|
4174
|
+
return null;
|
|
2771
4175
|
}
|
|
2772
4176
|
/**
|
|
2773
4177
|
* Is this JSX element `<navigatorVarName.MEMBER …>`?
|
|
@@ -2778,9 +4182,9 @@ var NavigationAnalyzer = class {
|
|
|
2778
4182
|
// `never`. It is a question about the member name, not about the node
|
|
2779
4183
|
// kind.
|
|
2780
4184
|
isNavigatorMember(node, navigatorVarName, member) {
|
|
2781
|
-
if (!
|
|
4185
|
+
if (!t8.isJSXElement(node)) return false;
|
|
2782
4186
|
const name = node.openingElement.name;
|
|
2783
|
-
return
|
|
4187
|
+
return t8.isJSXMemberExpression(name) && t8.isJSXIdentifier(name.object) && name.object.name === navigatorVarName && t8.isJSXIdentifier(name.property) && name.property.name === member;
|
|
2784
4188
|
}
|
|
2785
4189
|
/**
|
|
2786
4190
|
* Flatten a navigator's JSX children into the `<X.Screen>` elements they
|
|
@@ -2811,37 +4215,37 @@ var NavigationAnalyzer = class {
|
|
|
2811
4215
|
found.push(...this.collectScreenElements(group.children, navigatorVarName, depth + 1));
|
|
2812
4216
|
return;
|
|
2813
4217
|
}
|
|
2814
|
-
if (
|
|
4218
|
+
if (t8.isJSXElement(node)) {
|
|
2815
4219
|
const name = node.openingElement.name;
|
|
2816
|
-
const isForeignNavigator =
|
|
4220
|
+
const isForeignNavigator = t8.isJSXMemberExpression(name) && t8.isJSXIdentifier(name.property) && name.property.name === "Navigator";
|
|
2817
4221
|
if (isForeignNavigator) return;
|
|
2818
4222
|
found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
|
|
2819
4223
|
return;
|
|
2820
4224
|
}
|
|
2821
|
-
if (
|
|
4225
|
+
if (t8.isJSXFragment(node)) {
|
|
2822
4226
|
found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
|
|
2823
4227
|
return;
|
|
2824
4228
|
}
|
|
2825
|
-
if (
|
|
4229
|
+
if (t8.isJSXExpressionContainer(node)) {
|
|
2826
4230
|
visit(node.expression);
|
|
2827
4231
|
return;
|
|
2828
4232
|
}
|
|
2829
|
-
if (
|
|
4233
|
+
if (t8.isConditionalExpression(node)) {
|
|
2830
4234
|
visit(node.consequent);
|
|
2831
4235
|
visit(node.alternate);
|
|
2832
4236
|
return;
|
|
2833
4237
|
}
|
|
2834
|
-
if (
|
|
4238
|
+
if (t8.isLogicalExpression(node)) {
|
|
2835
4239
|
visit(node.left);
|
|
2836
4240
|
visit(node.right);
|
|
2837
4241
|
return;
|
|
2838
4242
|
}
|
|
2839
|
-
if (
|
|
4243
|
+
if (t8.isCallExpression(node)) {
|
|
2840
4244
|
for (const arg of node.arguments) {
|
|
2841
|
-
if (
|
|
2842
|
-
if (
|
|
4245
|
+
if (t8.isArrowFunctionExpression(arg) || t8.isFunctionExpression(arg)) {
|
|
4246
|
+
if (t8.isBlockStatement(arg.body)) {
|
|
2843
4247
|
for (const stmt of arg.body.body) {
|
|
2844
|
-
if (
|
|
4248
|
+
if (t8.isReturnStatement(stmt)) visit(stmt.argument);
|
|
2845
4249
|
}
|
|
2846
4250
|
} else {
|
|
2847
4251
|
visit(arg.body);
|
|
@@ -2850,11 +4254,11 @@ var NavigationAnalyzer = class {
|
|
|
2850
4254
|
}
|
|
2851
4255
|
return;
|
|
2852
4256
|
}
|
|
2853
|
-
if (
|
|
4257
|
+
if (t8.isArrayExpression(node)) {
|
|
2854
4258
|
for (const el of node.elements) visit(el);
|
|
2855
4259
|
return;
|
|
2856
4260
|
}
|
|
2857
|
-
if (
|
|
4261
|
+
if (t8.isTSAsExpression(node) || t8.isTSNonNullExpression(node)) {
|
|
2858
4262
|
visit(node.expression);
|
|
2859
4263
|
}
|
|
2860
4264
|
};
|
|
@@ -2862,28 +4266,54 @@ var NavigationAnalyzer = class {
|
|
|
2862
4266
|
return found;
|
|
2863
4267
|
}
|
|
2864
4268
|
/** Extract screens from a navigator JSX element */
|
|
2865
|
-
extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
|
|
4269
|
+
extractScreensFromNavigator(filePath, navigatorElement, navigatorVarName, navigatorType) {
|
|
2866
4270
|
const screens = [];
|
|
2867
4271
|
if (!navigatorElement.children) return screens;
|
|
2868
4272
|
const seen = /* @__PURE__ */ new Set();
|
|
2869
4273
|
for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
|
|
2870
|
-
const
|
|
2871
|
-
if (!
|
|
2872
|
-
seen.add(
|
|
2873
|
-
screens.push(
|
|
2874
|
-
name: screenName,
|
|
2875
|
-
navigatorName: navigatorVarName,
|
|
2876
|
-
navigatorType
|
|
2877
|
-
});
|
|
4274
|
+
const screen = this.parseScreenElement(filePath, element, navigatorVarName, navigatorType);
|
|
4275
|
+
if (!screen || seen.has(screen.name)) continue;
|
|
4276
|
+
seen.add(screen.name);
|
|
4277
|
+
screens.push(screen);
|
|
2878
4278
|
}
|
|
2879
4279
|
return screens;
|
|
2880
4280
|
}
|
|
4281
|
+
/**
|
|
4282
|
+
* Uma `<X.Screen>` isolada até a rota que ela declara.
|
|
4283
|
+
*
|
|
4284
|
+
* Separado de `extractScreensFromNavigator` porque a MESMA leitura serve para
|
|
4285
|
+
* a `<X.Screen>` que mora dentro do `<X.Navigator>` e para a que mora fora
|
|
4286
|
+
* dele — só a forma de chegar até o elemento muda.
|
|
4287
|
+
*/
|
|
4288
|
+
parseScreenElement(filePath, element, navigatorName, navigatorType) {
|
|
4289
|
+
const attrs = element.openingElement.attributes;
|
|
4290
|
+
const nameAttr = attrs.find(
|
|
4291
|
+
(a) => t8.isJSXAttribute(a) && t8.isJSXIdentifier(a.name) && a.name.name === "name"
|
|
4292
|
+
);
|
|
4293
|
+
const screenName = t8.isJSXAttribute(nameAttr) ? this.attributeString(filePath, nameAttr) : null;
|
|
4294
|
+
if (!screenName) return null;
|
|
4295
|
+
const componentAttr = attrs.find(
|
|
4296
|
+
(a) => t8.isJSXAttribute(a) && t8.isJSXIdentifier(a.name) && a.name.name === "component"
|
|
4297
|
+
);
|
|
4298
|
+
let componentFile = null;
|
|
4299
|
+
if (t8.isJSXAttribute(componentAttr) && t8.isJSXExpressionContainer(componentAttr.value)) {
|
|
4300
|
+
const expr = componentAttr.value.expression;
|
|
4301
|
+
if (t8.isExpression(expr)) componentFile = this.graph.componentFile(filePath, expr);
|
|
4302
|
+
}
|
|
4303
|
+
if (componentFile) this.routeTargets.set(screenName, componentFile);
|
|
4304
|
+
return {
|
|
4305
|
+
name: screenName,
|
|
4306
|
+
navigatorName,
|
|
4307
|
+
navigatorType,
|
|
4308
|
+
...componentFile ? { componentFile } : {}
|
|
4309
|
+
};
|
|
4310
|
+
}
|
|
2881
4311
|
/** Extract string attribute value from JSX attributes */
|
|
2882
4312
|
extractAttributeValue(attributes, attrName) {
|
|
2883
4313
|
const attr = attributes.find(
|
|
2884
|
-
(a) =>
|
|
4314
|
+
(a) => t8.isJSXAttribute(a) && t8.isJSXIdentifier(a.name) && a.name.name === attrName
|
|
2885
4315
|
);
|
|
2886
|
-
if (
|
|
4316
|
+
if (t8.isJSXAttribute(attr) && t8.isStringLiteral(attr.value)) {
|
|
2887
4317
|
return attr.value.value;
|
|
2888
4318
|
}
|
|
2889
4319
|
return null;
|
|
@@ -2892,7 +4322,7 @@ var NavigationAnalyzer = class {
|
|
|
2892
4322
|
parseParamTypes(content, filePath) {
|
|
2893
4323
|
const types = [];
|
|
2894
4324
|
try {
|
|
2895
|
-
const ast =
|
|
4325
|
+
const ast = parser2.parse(content, {
|
|
2896
4326
|
sourceType: "module",
|
|
2897
4327
|
plugins: [
|
|
2898
4328
|
"jsx",
|
|
@@ -2900,7 +4330,7 @@ var NavigationAnalyzer = class {
|
|
|
2900
4330
|
...this.config.parserPlugins || []
|
|
2901
4331
|
]
|
|
2902
4332
|
});
|
|
2903
|
-
(0,
|
|
4333
|
+
(0, import_traverse6.default)(ast, {
|
|
2904
4334
|
TSTypeAliasDeclaration: (nodePath) => {
|
|
2905
4335
|
const { node } = nodePath;
|
|
2906
4336
|
const typeName = node.id.name;
|
|
@@ -2973,7 +4403,7 @@ var NavigationAnalyzer = class {
|
|
|
2973
4403
|
if (type.type === "TSUndefinedKeyword") return "undefined";
|
|
2974
4404
|
if (type.type === "TSNullKeyword") return "null";
|
|
2975
4405
|
if (type.type === "TSUnionType") {
|
|
2976
|
-
return type.types.map((
|
|
4406
|
+
return type.types.map((t18) => this.typeToString(t18)).join(" | ");
|
|
2977
4407
|
}
|
|
2978
4408
|
if (type.type === "TSTypeLiteral") {
|
|
2979
4409
|
return "object";
|
|
@@ -2994,7 +4424,7 @@ var NavigationAnalyzer = class {
|
|
|
2994
4424
|
/** Attach parsed type params to navigator screens */
|
|
2995
4425
|
attachParamsToNavigators(navigators, types) {
|
|
2996
4426
|
for (const navigator of navigators) {
|
|
2997
|
-
const matchingType = types.find((
|
|
4427
|
+
const matchingType = types.find((t18) => t18.type === navigator.type);
|
|
2998
4428
|
if (matchingType) {
|
|
2999
4429
|
for (const screen of navigator.screens) {
|
|
3000
4430
|
const screenParams = matchingType.paramEntries.get(screen.name);
|
|
@@ -3072,9 +4502,9 @@ var NavigationAnalyzer = class {
|
|
|
3072
4502
|
|
|
3073
4503
|
// src/analyzers/ComponentAnalyzer.ts
|
|
3074
4504
|
var import_fs5 = require("fs");
|
|
3075
|
-
var
|
|
3076
|
-
var
|
|
3077
|
-
var
|
|
4505
|
+
var parser3 = __toESM(require("@babel/parser"));
|
|
4506
|
+
var import_traverse7 = __toESM(require("@babel/traverse"));
|
|
4507
|
+
var t9 = __toESM(require("@babel/types"));
|
|
3078
4508
|
var ComponentAnalyzer = class {
|
|
3079
4509
|
config;
|
|
3080
4510
|
constructor(config) {
|
|
@@ -3084,14 +4514,14 @@ var ComponentAnalyzer = class {
|
|
|
3084
4514
|
async analyzeFile(filePath) {
|
|
3085
4515
|
try {
|
|
3086
4516
|
const code = (0, import_fs5.readFileSync)(filePath, "utf-8");
|
|
3087
|
-
const ast =
|
|
4517
|
+
const ast = parser3.parse(code, {
|
|
3088
4518
|
sourceType: "module",
|
|
3089
4519
|
plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
|
|
3090
4520
|
});
|
|
3091
4521
|
const components = [];
|
|
3092
|
-
(0,
|
|
3093
|
-
JSXElement: (
|
|
3094
|
-
const component = this.extractComponentFromJSXElement(
|
|
4522
|
+
(0, import_traverse7.default)(ast, {
|
|
4523
|
+
JSXElement: (path13) => {
|
|
4524
|
+
const component = this.extractComponentFromJSXElement(path13.node);
|
|
3095
4525
|
if (component) {
|
|
3096
4526
|
components.push(component);
|
|
3097
4527
|
}
|
|
@@ -3120,19 +4550,19 @@ var ComponentAnalyzer = class {
|
|
|
3120
4550
|
};
|
|
3121
4551
|
}
|
|
3122
4552
|
getElementName(openingElement) {
|
|
3123
|
-
if (
|
|
4553
|
+
if (t9.isJSXIdentifier(openingElement.name)) {
|
|
3124
4554
|
return openingElement.name.name;
|
|
3125
4555
|
}
|
|
3126
|
-
if (
|
|
4556
|
+
if (t9.isJSXMemberExpression(openingElement.name)) {
|
|
3127
4557
|
const parts = [];
|
|
3128
4558
|
let current = openingElement.name;
|
|
3129
|
-
while (
|
|
3130
|
-
if (
|
|
4559
|
+
while (t9.isJSXMemberExpression(current)) {
|
|
4560
|
+
if (t9.isJSXIdentifier(current.property)) {
|
|
3131
4561
|
parts.unshift(current.property.name);
|
|
3132
4562
|
}
|
|
3133
4563
|
current = current.object;
|
|
3134
4564
|
}
|
|
3135
|
-
if (
|
|
4565
|
+
if (t9.isJSXIdentifier(current)) {
|
|
3136
4566
|
parts.unshift(current.name);
|
|
3137
4567
|
}
|
|
3138
4568
|
return parts.join(".");
|
|
@@ -3155,13 +4585,13 @@ var ComponentAnalyzer = class {
|
|
|
3155
4585
|
extractProps(openingElement) {
|
|
3156
4586
|
const props = {};
|
|
3157
4587
|
for (const attr of openingElement.attributes) {
|
|
3158
|
-
if (
|
|
4588
|
+
if (t9.isJSXAttribute(attr) && t9.isJSXIdentifier(attr.name)) {
|
|
3159
4589
|
const propName = attr.name.name;
|
|
3160
4590
|
if (attr.value === null) {
|
|
3161
4591
|
props[propName] = "true";
|
|
3162
|
-
} else if (
|
|
4592
|
+
} else if (t9.isStringLiteral(attr.value)) {
|
|
3163
4593
|
props[propName] = attr.value.value;
|
|
3164
|
-
} else if (
|
|
4594
|
+
} else if (t9.isJSXExpressionContainer(attr.value) && t9.isStringLiteral(attr.value.expression)) {
|
|
3165
4595
|
props[propName] = attr.value.expression.value;
|
|
3166
4596
|
}
|
|
3167
4597
|
}
|
|
@@ -3171,12 +4601,12 @@ var ComponentAnalyzer = class {
|
|
|
3171
4601
|
extractAccessibilityProps(openingElement) {
|
|
3172
4602
|
const result = {};
|
|
3173
4603
|
for (const attr of openingElement.attributes) {
|
|
3174
|
-
if (
|
|
4604
|
+
if (t9.isJSXAttribute(attr) && t9.isJSXIdentifier(attr.name)) {
|
|
3175
4605
|
const propName = attr.name.name;
|
|
3176
4606
|
if ((propName === "testID" || propName === "accessibilityLabel") && attr.value) {
|
|
3177
|
-
if (
|
|
4607
|
+
if (t9.isStringLiteral(attr.value)) {
|
|
3178
4608
|
result[propName] = attr.value.value;
|
|
3179
|
-
} else if (
|
|
4609
|
+
} else if (t9.isJSXExpressionContainer(attr.value) && t9.isStringLiteral(attr.value.expression)) {
|
|
3180
4610
|
result[propName] = attr.value.expression.value;
|
|
3181
4611
|
}
|
|
3182
4612
|
}
|
|
@@ -3188,7 +4618,7 @@ var ComponentAnalyzer = class {
|
|
|
3188
4618
|
if (depth >= maxDepth) return [];
|
|
3189
4619
|
const extracted = [];
|
|
3190
4620
|
for (const child of children) {
|
|
3191
|
-
if (
|
|
4621
|
+
if (t9.isJSXElement(child)) {
|
|
3192
4622
|
const component = this.extractComponentFromJSXElement(child);
|
|
3193
4623
|
if (component) {
|
|
3194
4624
|
extracted.push(component);
|
|
@@ -3201,10 +4631,10 @@ var ComponentAnalyzer = class {
|
|
|
3201
4631
|
|
|
3202
4632
|
// src/analyzers/FormAnalyzer.ts
|
|
3203
4633
|
var import_fs6 = require("fs");
|
|
3204
|
-
var
|
|
3205
|
-
var
|
|
3206
|
-
var
|
|
3207
|
-
var
|
|
4634
|
+
var parser4 = __toESM(require("@babel/parser"));
|
|
4635
|
+
var import_traverse8 = __toESM(require("@babel/traverse"));
|
|
4636
|
+
var t10 = __toESM(require("@babel/types"));
|
|
4637
|
+
var path5 = __toESM(require("path"));
|
|
3208
4638
|
var FormAnalyzer = class {
|
|
3209
4639
|
config;
|
|
3210
4640
|
stateVariables = /* @__PURE__ */ new Map();
|
|
@@ -3217,21 +4647,21 @@ var FormAnalyzer = class {
|
|
|
3217
4647
|
async analyzeFile(filePath) {
|
|
3218
4648
|
try {
|
|
3219
4649
|
const code = (0, import_fs6.readFileSync)(filePath, "utf-8");
|
|
3220
|
-
const ast =
|
|
4650
|
+
const ast = parser4.parse(code, {
|
|
3221
4651
|
sourceType: "module",
|
|
3222
4652
|
plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
|
|
3223
4653
|
});
|
|
3224
4654
|
this.stateVariables.clear();
|
|
3225
4655
|
this.inputElements = [];
|
|
3226
4656
|
this.submitButtons = [];
|
|
3227
|
-
(0,
|
|
3228
|
-
CallExpression: (
|
|
3229
|
-
this.extractStateVariables(
|
|
4657
|
+
(0, import_traverse8.default)(ast, {
|
|
4658
|
+
CallExpression: (path13) => {
|
|
4659
|
+
this.extractStateVariables(path13.node);
|
|
3230
4660
|
}
|
|
3231
4661
|
});
|
|
3232
|
-
(0,
|
|
3233
|
-
JSXElement: (
|
|
3234
|
-
this.extractFormElements(
|
|
4662
|
+
(0, import_traverse8.default)(ast, {
|
|
4663
|
+
JSXElement: (path13) => {
|
|
4664
|
+
this.extractFormElements(path13.node);
|
|
3235
4665
|
}
|
|
3236
4666
|
});
|
|
3237
4667
|
const validationRules = this.extractValidationRules(ast);
|
|
@@ -3242,7 +4672,7 @@ var FormAnalyzer = class {
|
|
|
3242
4672
|
}
|
|
3243
4673
|
}
|
|
3244
4674
|
extractStateVariables(node) {
|
|
3245
|
-
if (
|
|
4675
|
+
if (t10.isIdentifier(node.callee) && node.callee.name === "useState" && node.arguments.length > 0) {
|
|
3246
4676
|
return;
|
|
3247
4677
|
}
|
|
3248
4678
|
}
|
|
@@ -3263,28 +4693,28 @@ var FormAnalyzer = class {
|
|
|
3263
4693
|
extractInputInfo(openingElement) {
|
|
3264
4694
|
const info2 = { varName: "" };
|
|
3265
4695
|
for (const attr of openingElement.attributes) {
|
|
3266
|
-
if (
|
|
4696
|
+
if (t10.isJSXAttribute(attr) && t10.isJSXIdentifier(attr.name)) {
|
|
3267
4697
|
const propName = attr.name.name;
|
|
3268
|
-
const
|
|
4698
|
+
const propValue2 = this.extractAttributeValue(attr.value);
|
|
3269
4699
|
switch (propName) {
|
|
3270
4700
|
case "label":
|
|
3271
|
-
info2.label =
|
|
4701
|
+
info2.label = propValue2;
|
|
3272
4702
|
break;
|
|
3273
4703
|
case "placeholder":
|
|
3274
|
-
info2.placeholder =
|
|
4704
|
+
info2.placeholder = propValue2;
|
|
3275
4705
|
break;
|
|
3276
4706
|
case "keyboardType":
|
|
3277
|
-
info2.keyboardType =
|
|
4707
|
+
info2.keyboardType = propValue2;
|
|
3278
4708
|
break;
|
|
3279
4709
|
case "testID":
|
|
3280
|
-
info2.testID =
|
|
4710
|
+
info2.testID = propValue2;
|
|
3281
4711
|
break;
|
|
3282
4712
|
case "appilotsId":
|
|
3283
|
-
info2.appilotsId =
|
|
3284
|
-
if (
|
|
4713
|
+
info2.appilotsId = propValue2;
|
|
4714
|
+
if (propValue2) info2.varName = propValue2;
|
|
3285
4715
|
break;
|
|
3286
4716
|
case "value":
|
|
3287
|
-
if (attr.value &&
|
|
4717
|
+
if (attr.value && t10.isJSXExpressionContainer(attr.value) && t10.isIdentifier(attr.value.expression)) {
|
|
3288
4718
|
info2.varName = attr.value.expression.name;
|
|
3289
4719
|
}
|
|
3290
4720
|
break;
|
|
@@ -3296,16 +4726,16 @@ var FormAnalyzer = class {
|
|
|
3296
4726
|
extractButtonInfo(openingElement) {
|
|
3297
4727
|
const info2 = {};
|
|
3298
4728
|
for (const attr of openingElement.attributes) {
|
|
3299
|
-
if (
|
|
4729
|
+
if (t10.isJSXAttribute(attr) && t10.isJSXIdentifier(attr.name)) {
|
|
3300
4730
|
const propName = attr.name.name;
|
|
3301
4731
|
if (propName === "title" || propName === "label") {
|
|
3302
4732
|
info2.label = this.extractAttributeValue(attr.value);
|
|
3303
4733
|
}
|
|
3304
4734
|
if (propName === "onPress") {
|
|
3305
|
-
if (attr.value &&
|
|
3306
|
-
if (
|
|
4735
|
+
if (attr.value && t10.isJSXExpressionContainer(attr.value)) {
|
|
4736
|
+
if (t10.isIdentifier(attr.value.expression)) {
|
|
3307
4737
|
info2.handler = attr.value.expression.name;
|
|
3308
|
-
} else if (
|
|
4738
|
+
} else if (t10.isArrowFunctionExpression(attr.value.expression) || t10.isFunctionExpression(attr.value.expression)) {
|
|
3309
4739
|
info2.handler = "anonymous";
|
|
3310
4740
|
}
|
|
3311
4741
|
}
|
|
@@ -3316,23 +4746,23 @@ var FormAnalyzer = class {
|
|
|
3316
4746
|
}
|
|
3317
4747
|
extractAttributeValue(value) {
|
|
3318
4748
|
if (value === null) return void 0;
|
|
3319
|
-
if (
|
|
3320
|
-
if (
|
|
4749
|
+
if (t10.isStringLiteral(value)) return value.value;
|
|
4750
|
+
if (t10.isJSXExpressionContainer(value) && t10.isStringLiteral(value.expression)) {
|
|
3321
4751
|
return value.expression.value;
|
|
3322
4752
|
}
|
|
3323
4753
|
return void 0;
|
|
3324
4754
|
}
|
|
3325
4755
|
getElementName(openingElement) {
|
|
3326
|
-
if (
|
|
4756
|
+
if (t10.isJSXIdentifier(openingElement.name)) {
|
|
3327
4757
|
return openingElement.name.name;
|
|
3328
4758
|
}
|
|
3329
4759
|
return null;
|
|
3330
4760
|
}
|
|
3331
4761
|
extractValidationRules(ast) {
|
|
3332
4762
|
const rules = {};
|
|
3333
|
-
(0,
|
|
3334
|
-
IfStatement: (
|
|
3335
|
-
const test =
|
|
4763
|
+
(0, import_traverse8.default)(ast, {
|
|
4764
|
+
IfStatement: (path13) => {
|
|
4765
|
+
const test = path13.node.test;
|
|
3336
4766
|
const rule = this.extractRuleFromCondition(test);
|
|
3337
4767
|
if (rule) {
|
|
3338
4768
|
const { field, description } = rule;
|
|
@@ -3347,36 +4777,36 @@ var FormAnalyzer = class {
|
|
|
3347
4777
|
extractRuleFromCondition(test) {
|
|
3348
4778
|
let fieldName = "";
|
|
3349
4779
|
let description = "";
|
|
3350
|
-
if (
|
|
3351
|
-
if (
|
|
4780
|
+
if (t10.isUnaryExpression(test) && test.operator === "!") {
|
|
4781
|
+
if (t10.isCallExpression(test.argument)) {
|
|
3352
4782
|
const callExpr = test.argument;
|
|
3353
|
-
if (
|
|
4783
|
+
if (t10.isMemberExpression(callExpr.callee)) {
|
|
3354
4784
|
const memberExpr = callExpr.callee;
|
|
3355
|
-
if (
|
|
4785
|
+
if (t10.isIdentifier(memberExpr.object)) {
|
|
3356
4786
|
fieldName = memberExpr.object.name;
|
|
3357
4787
|
description = `${fieldName} is required`;
|
|
3358
4788
|
}
|
|
3359
4789
|
}
|
|
3360
|
-
} else if (
|
|
4790
|
+
} else if (t10.isIdentifier(test.argument)) {
|
|
3361
4791
|
fieldName = test.argument.name;
|
|
3362
4792
|
description = `${fieldName} is required`;
|
|
3363
4793
|
}
|
|
3364
4794
|
}
|
|
3365
|
-
if (
|
|
3366
|
-
if (
|
|
4795
|
+
if (t10.isBinaryExpression(test) && (test.operator === "<" || test.operator === "<=")) {
|
|
4796
|
+
if (t10.isMemberExpression(test.left)) {
|
|
3367
4797
|
const memberExpr = test.left;
|
|
3368
|
-
if (
|
|
4798
|
+
if (t10.isIdentifier(memberExpr.object)) {
|
|
3369
4799
|
fieldName = memberExpr.object.name;
|
|
3370
|
-
if (
|
|
4800
|
+
if (t10.isNumericLiteral(test.right)) {
|
|
3371
4801
|
description = `${fieldName} must be at least ${test.right.value} characters`;
|
|
3372
4802
|
}
|
|
3373
4803
|
}
|
|
3374
4804
|
}
|
|
3375
4805
|
}
|
|
3376
|
-
if (
|
|
3377
|
-
if (
|
|
3378
|
-
const methodName =
|
|
3379
|
-
if (methodName === "includes" &&
|
|
4806
|
+
if (t10.isUnaryExpression(test) && test.operator === "!") {
|
|
4807
|
+
if (t10.isCallExpression(test.argument) && t10.isMemberExpression(test.argument.callee)) {
|
|
4808
|
+
const methodName = t10.isIdentifier(test.argument.callee.property) ? test.argument.callee.property.name : null;
|
|
4809
|
+
if (methodName === "includes" && t10.isIdentifier(test.argument.callee.object)) {
|
|
3380
4810
|
fieldName = test.argument.callee.object.name;
|
|
3381
4811
|
description = `${fieldName} format is invalid`;
|
|
3382
4812
|
}
|
|
@@ -3386,7 +4816,7 @@ var FormAnalyzer = class {
|
|
|
3386
4816
|
}
|
|
3387
4817
|
buildForms(filePath, validationRules) {
|
|
3388
4818
|
if (this.inputElements.length === 0) return [];
|
|
3389
|
-
const fileName =
|
|
4819
|
+
const fileName = path5.basename(filePath, path5.extname(filePath));
|
|
3390
4820
|
const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
|
|
3391
4821
|
const fields = this.inputElements.map((input) => {
|
|
3392
4822
|
const fieldType = this.inferFieldType(input);
|
|
@@ -3436,14 +4866,104 @@ var FormAnalyzer = class {
|
|
|
3436
4866
|
}
|
|
3437
4867
|
};
|
|
3438
4868
|
|
|
4869
|
+
// src/pipeline/composition.ts
|
|
4870
|
+
var MAX_DEPTH = 3;
|
|
4871
|
+
var MAX_VISITED_PER_SCREEN = 60;
|
|
4872
|
+
var MAX_FAN_IN = 3;
|
|
4873
|
+
var ADDRESSABLE = /* @__PURE__ */ new Set(["testID", "appilotsId", "accessibilityLabel", "label"]);
|
|
4874
|
+
function descendants(graph, from, screenFiles) {
|
|
4875
|
+
const seen = /* @__PURE__ */ new Set([from]);
|
|
4876
|
+
const out = [];
|
|
4877
|
+
let frontier = [from];
|
|
4878
|
+
for (let depth = 0; depth < MAX_DEPTH && frontier.length > 0; depth++) {
|
|
4879
|
+
const next = [];
|
|
4880
|
+
for (const file2 of frontier) {
|
|
4881
|
+
for (const child of graph.renderedComponentFiles(file2)) {
|
|
4882
|
+
if (seen.has(child)) continue;
|
|
4883
|
+
seen.add(child);
|
|
4884
|
+
if (screenFiles.has(child)) continue;
|
|
4885
|
+
out.push(child);
|
|
4886
|
+
next.push(child);
|
|
4887
|
+
if (out.length >= MAX_VISITED_PER_SCREEN) return out;
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
frontier = next;
|
|
4891
|
+
}
|
|
4892
|
+
return out;
|
|
4893
|
+
}
|
|
4894
|
+
function addressable(locator) {
|
|
4895
|
+
if (!locator || typeof locator !== "object") return false;
|
|
4896
|
+
const l = locator;
|
|
4897
|
+
if (typeof l["source"] === "string" && ADDRESSABLE.has(l["source"])) return true;
|
|
4898
|
+
return typeof l["testID"] === "string" && l["testID"].length > 0;
|
|
4899
|
+
}
|
|
4900
|
+
async function composeAffordances(options) {
|
|
4901
|
+
const { screens, graph, analyzeFile, screenFiles } = options;
|
|
4902
|
+
const byScreen = /* @__PURE__ */ new Map();
|
|
4903
|
+
const fanIn = /* @__PURE__ */ new Map();
|
|
4904
|
+
for (const screen of screens) {
|
|
4905
|
+
if (!screen.filePath) continue;
|
|
4906
|
+
const children = descendants(graph, screen.filePath, screenFiles);
|
|
4907
|
+
byScreen.set(screen, children);
|
|
4908
|
+
for (const child of children) fanIn.set(child, (fanIn.get(child) ?? 0) + 1);
|
|
4909
|
+
}
|
|
4910
|
+
const analyzed = /* @__PURE__ */ new Map();
|
|
4911
|
+
let screensEnriched = 0;
|
|
4912
|
+
let filesMerged = 0;
|
|
4913
|
+
for (const [screen, children] of byScreen) {
|
|
4914
|
+
const exclusive = children.filter((c) => (fanIn.get(c) ?? 0) <= MAX_FAN_IN);
|
|
4915
|
+
if (exclusive.length === 0) continue;
|
|
4916
|
+
const actionIds = new Set(screen.actions.map((a) => a.id));
|
|
4917
|
+
const targetIds = new Set((screen.targets ?? []).map((t18) => t18.id));
|
|
4918
|
+
const formIds = new Set(screen.forms.map((f) => f.id));
|
|
4919
|
+
let gained = false;
|
|
4920
|
+
for (const file2 of exclusive) {
|
|
4921
|
+
if (!analyzed.has(file2)) {
|
|
4922
|
+
try {
|
|
4923
|
+
analyzed.set(file2, await analyzeFile(file2));
|
|
4924
|
+
} catch {
|
|
4925
|
+
analyzed.set(file2, null);
|
|
4926
|
+
}
|
|
4927
|
+
}
|
|
4928
|
+
const child = analyzed.get(file2);
|
|
4929
|
+
if (!child) continue;
|
|
4930
|
+
let merged = false;
|
|
4931
|
+
for (const action of child.actions) {
|
|
4932
|
+
if (!addressable(action.locator)) continue;
|
|
4933
|
+
if (actionIds.has(action.id)) continue;
|
|
4934
|
+
actionIds.add(action.id);
|
|
4935
|
+
screen.actions.push(action);
|
|
4936
|
+
merged = true;
|
|
4937
|
+
}
|
|
4938
|
+
for (const target of child.targets ?? []) {
|
|
4939
|
+
if (!addressable(target.locator)) continue;
|
|
4940
|
+
if (targetIds.has(target.id)) continue;
|
|
4941
|
+
targetIds.add(target.id);
|
|
4942
|
+
(screen.targets ??= []).push(target);
|
|
4943
|
+
merged = true;
|
|
4944
|
+
}
|
|
4945
|
+
for (const form of child.forms) {
|
|
4946
|
+
if (form.fields.length === 0) continue;
|
|
4947
|
+
const id = formIds.has(form.id) ? `${form.id}:${child.name}` : form.id;
|
|
4948
|
+
if (formIds.has(id)) continue;
|
|
4949
|
+
formIds.add(id);
|
|
4950
|
+
screen.forms.push({ ...form, id });
|
|
4951
|
+
merged = true;
|
|
4952
|
+
}
|
|
4953
|
+
if (merged) {
|
|
4954
|
+
filesMerged++;
|
|
4955
|
+
gained = true;
|
|
4956
|
+
}
|
|
4957
|
+
}
|
|
4958
|
+
if (gained) screensEnriched++;
|
|
4959
|
+
}
|
|
4960
|
+
return { screens, screensEnriched, filesMerged };
|
|
4961
|
+
}
|
|
4962
|
+
|
|
3439
4963
|
// src/analyzers/ReactNativePlatformAnalyzer.ts
|
|
3440
4964
|
var ReactNativePlatformAnalyzer = class {
|
|
3441
4965
|
platform = "react-native";
|
|
3442
4966
|
async analyze(config, options) {
|
|
3443
|
-
const screenAnalyzer = new ScreenAnalyzer(config, {
|
|
3444
|
-
strictScreens: options.strictScreens ?? true,
|
|
3445
|
-
screenPatterns: options.screenPatterns
|
|
3446
|
-
});
|
|
3447
4967
|
const navigationAnalyzer = new NavigationAnalyzer(config, {
|
|
3448
4968
|
navigationInclude: options.navigationInclude,
|
|
3449
4969
|
navigationExclude: options.navigationExclude
|
|
@@ -3451,14 +4971,31 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3451
4971
|
const componentAnalyzer = new ComponentAnalyzer(config);
|
|
3452
4972
|
const formAnalyzer = new FormAnalyzer(config);
|
|
3453
4973
|
console.log("[ReactNativePlatformAnalyzer] Running analyzers...");
|
|
3454
|
-
const
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
4974
|
+
const navigation = await navigationAnalyzer.analyze();
|
|
4975
|
+
const screenAnalyzer = new ScreenAnalyzer(config, {
|
|
4976
|
+
strictScreens: options.strictScreens ?? true,
|
|
4977
|
+
screenPatterns: options.screenPatterns,
|
|
4978
|
+
// Menos os arquivos que declaram navegador: ver `navigatorFiles`.
|
|
4979
|
+
routeTargetFiles: new Set(
|
|
4980
|
+
[...navigationAnalyzer.routeTargets.values()].filter(
|
|
4981
|
+
(file2) => !navigationAnalyzer.navigatorFiles.has(file2)
|
|
4982
|
+
)
|
|
4983
|
+
)
|
|
4984
|
+
});
|
|
4985
|
+
const screens = await screenAnalyzer.analyze();
|
|
4986
|
+
const composed = await composeAffordances({
|
|
4987
|
+
screens,
|
|
4988
|
+
graph: navigationAnalyzer.graph,
|
|
4989
|
+
analyzeFile: (file2) => screenAnalyzer.analyzeFile(file2),
|
|
4990
|
+
screenFiles: new Set(screens.map((s) => s.filePath).filter(Boolean))
|
|
4991
|
+
});
|
|
4992
|
+
console.log(
|
|
4993
|
+
`[ReactNativePlatformAnalyzer] Composi\xE7\xE3o: ${composed.filesMerged} componente(s) exclusivo(s) fundido(s) em ${composed.screensEnriched} tela(s)`
|
|
4994
|
+
);
|
|
3458
4995
|
console.log(
|
|
3459
4996
|
`[ReactNativePlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens`
|
|
3460
4997
|
);
|
|
3461
|
-
const screenFiles = await (
|
|
4998
|
+
const screenFiles = await globSorted(config.include || ["**/*.tsx", "**/*.ts"], {
|
|
3462
4999
|
cwd: config.rootDir,
|
|
3463
5000
|
ignore: config.exclude || ["**/node_modules/**"]
|
|
3464
5001
|
});
|
|
@@ -3466,7 +5003,7 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3466
5003
|
`[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
|
|
3467
5004
|
);
|
|
3468
5005
|
const enrichmentPromises = screenFiles.map(async (file2) => {
|
|
3469
|
-
const filePath =
|
|
5006
|
+
const filePath = import_node_path3.default.resolve(config.rootDir, file2);
|
|
3470
5007
|
try {
|
|
3471
5008
|
const [components, forms] = await Promise.all([
|
|
3472
5009
|
componentAnalyzer.analyzeFile(filePath),
|
|
@@ -3519,9 +5056,11 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3519
5056
|
});
|
|
3520
5057
|
return {
|
|
3521
5058
|
screens: enrichedScreens,
|
|
5059
|
+
controlEvidenceFiles: screenAnalyzer.controlEvidenceFiles,
|
|
3522
5060
|
navigation,
|
|
3523
5061
|
analyzedFiles: screenFiles.length,
|
|
3524
|
-
...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
|
|
5062
|
+
...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {},
|
|
5063
|
+
diagnostics: { navigation: navigationAnalyzer.diagnostics }
|
|
3525
5064
|
};
|
|
3526
5065
|
}
|
|
3527
5066
|
mergeForm(target, source) {
|
|
@@ -3539,9 +5078,12 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3539
5078
|
findEquivalentField(fields, incoming) {
|
|
3540
5079
|
return fields.find((field) => {
|
|
3541
5080
|
if (field.name && incoming.name && field.name === incoming.name) return true;
|
|
3542
|
-
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
3543
|
-
|
|
3544
|
-
if (field.
|
|
5081
|
+
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
5082
|
+
return true;
|
|
5083
|
+
if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
|
|
5084
|
+
return true;
|
|
5085
|
+
if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
|
|
5086
|
+
return true;
|
|
3545
5087
|
if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
|
|
3546
5088
|
return true;
|
|
3547
5089
|
}
|
|
@@ -3574,7 +5116,9 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3574
5116
|
if (incoming.fields.length === 0) return void 0;
|
|
3575
5117
|
let best;
|
|
3576
5118
|
for (const form of forms) {
|
|
3577
|
-
const overlap = incoming.fields.filter(
|
|
5119
|
+
const overlap = incoming.fields.filter(
|
|
5120
|
+
(field) => this.findEquivalentField(form.fields, field)
|
|
5121
|
+
).length;
|
|
3578
5122
|
if (overlap > 0 && (!best || overlap > best.overlap)) {
|
|
3579
5123
|
best = { form, overlap };
|
|
3580
5124
|
}
|
|
@@ -3601,9 +5145,8 @@ var GenericPlatformAnalyzer = class {
|
|
|
3601
5145
|
// src/analyzers/web/WebScreenAnalyzer.ts
|
|
3602
5146
|
var import_promises2 = __toESM(require("fs/promises"));
|
|
3603
5147
|
var import_path5 = __toESM(require("path"));
|
|
3604
|
-
var
|
|
3605
|
-
var
|
|
3606
|
-
var t10 = __toESM(require("@babel/types"));
|
|
5148
|
+
var import_traverse10 = __toESM(require("@babel/traverse"));
|
|
5149
|
+
var t13 = __toESM(require("@babel/types"));
|
|
3607
5150
|
|
|
3608
5151
|
// src/ast/jsx/web/classify.ts
|
|
3609
5152
|
var VIEW_TAGS = /* @__PURE__ */ new Set([
|
|
@@ -3719,49 +5262,49 @@ function classifyWebJsxComponent(name, element) {
|
|
|
3719
5262
|
}
|
|
3720
5263
|
|
|
3721
5264
|
// src/ast/jsx/names.ts
|
|
3722
|
-
var
|
|
5265
|
+
var t11 = __toESM(require("@babel/types"));
|
|
3723
5266
|
function getJsxElementName(openingElement) {
|
|
3724
5267
|
return getJsxName(openingElement.name);
|
|
3725
5268
|
}
|
|
3726
5269
|
function getJsxName(name) {
|
|
3727
|
-
if (
|
|
3728
|
-
if (
|
|
3729
|
-
if (
|
|
5270
|
+
if (t11.isJSXIdentifier(name)) return name.name;
|
|
5271
|
+
if (t11.isJSXNamespacedName(name)) return `${name.namespace.name}:${name.name.name}`;
|
|
5272
|
+
if (t11.isJSXMemberExpression(name)) {
|
|
3730
5273
|
const objectName = getJsxName(name.object);
|
|
3731
|
-
const propertyName =
|
|
5274
|
+
const propertyName = t11.isJSXIdentifier(name.property) ? name.property.name : null;
|
|
3732
5275
|
return objectName && propertyName ? `${objectName}.${propertyName}` : null;
|
|
3733
5276
|
}
|
|
3734
5277
|
return null;
|
|
3735
5278
|
}
|
|
3736
5279
|
|
|
3737
5280
|
// src/ast/navigation/web-calls.ts
|
|
3738
|
-
var
|
|
3739
|
-
var
|
|
5281
|
+
var import_traverse9 = __toESM(require("@babel/traverse"));
|
|
5282
|
+
var t12 = __toESM(require("@babel/types"));
|
|
3740
5283
|
var ROUTERISH_OBJECTS = /^(router|history|navigation)$/;
|
|
3741
5284
|
function staticRoutePath(node) {
|
|
3742
5285
|
if (!node) return void 0;
|
|
3743
|
-
if (
|
|
3744
|
-
if (
|
|
3745
|
-
let
|
|
5286
|
+
if (t12.isStringLiteral(node)) return node.value;
|
|
5287
|
+
if (t12.isTemplateLiteral(node)) {
|
|
5288
|
+
let path13 = "";
|
|
3746
5289
|
node.quasis.forEach((quasi, index) => {
|
|
3747
|
-
|
|
5290
|
+
path13 += quasi.value.cooked ?? quasi.value.raw;
|
|
3748
5291
|
const expr = node.expressions[index];
|
|
3749
|
-
if (expr)
|
|
5292
|
+
if (expr) path13 += `:${paramNameOf(expr)}`;
|
|
3750
5293
|
});
|
|
3751
|
-
return
|
|
5294
|
+
return path13;
|
|
3752
5295
|
}
|
|
3753
5296
|
return void 0;
|
|
3754
5297
|
}
|
|
3755
5298
|
function paramNameOf(expr) {
|
|
3756
|
-
if (
|
|
3757
|
-
if (
|
|
5299
|
+
if (t12.isIdentifier(expr)) return expr.name;
|
|
5300
|
+
if (t12.isMemberExpression(expr) && t12.isIdentifier(expr.property)) return expr.property.name;
|
|
3758
5301
|
return "param";
|
|
3759
5302
|
}
|
|
3760
5303
|
function extractWebNavigationCalls(ast) {
|
|
3761
5304
|
const calls = [];
|
|
3762
5305
|
const inspect = (node) => {
|
|
3763
|
-
if (
|
|
3764
|
-
if (
|
|
5306
|
+
if (t12.isCallExpression(node)) {
|
|
5307
|
+
if (t12.isIdentifier(node.callee) && node.callee.name === "navigate") {
|
|
3765
5308
|
const first = node.arguments[0];
|
|
3766
5309
|
const targetPath = staticRoutePath(first);
|
|
3767
5310
|
if (targetPath !== void 0) {
|
|
@@ -3769,12 +5312,12 @@ function extractWebNavigationCalls(ast) {
|
|
|
3769
5312
|
method: hasReplaceOption(node.arguments[1]) ? "replace" : "navigate",
|
|
3770
5313
|
targetPath
|
|
3771
5314
|
});
|
|
3772
|
-
} else if (
|
|
5315
|
+
} else if (t12.isNumericLiteral(first) || t12.isUnaryExpression(first) && first.operator === "-") {
|
|
3773
5316
|
calls.push({ method: "goBack" });
|
|
3774
5317
|
}
|
|
3775
5318
|
return;
|
|
3776
5319
|
}
|
|
3777
|
-
if (
|
|
5320
|
+
if (t12.isMemberExpression(node.callee) && t12.isIdentifier(node.callee.object) && ROUTERISH_OBJECTS.test(node.callee.object.name) && t12.isIdentifier(node.callee.property)) {
|
|
3778
5321
|
const method = node.callee.property.name;
|
|
3779
5322
|
const targetPath = staticRoutePath(node.arguments[0]);
|
|
3780
5323
|
if ((method === "push" || method === "navigate") && targetPath !== void 0) {
|
|
@@ -3787,25 +5330,25 @@ function extractWebNavigationCalls(ast) {
|
|
|
3787
5330
|
}
|
|
3788
5331
|
return;
|
|
3789
5332
|
}
|
|
3790
|
-
if (
|
|
5333
|
+
if (t12.isAssignmentExpression(node) && t12.isMemberExpression(node.left) && t12.isIdentifier(node.left.property) && node.left.property.name === "href" && isLocationExpression(node.left.object) && t12.isStringLiteral(node.right) && node.right.value.startsWith("/")) {
|
|
3791
5334
|
calls.push({ method: "navigate", targetPath: node.right.value });
|
|
3792
5335
|
}
|
|
3793
5336
|
};
|
|
3794
|
-
(0,
|
|
3795
|
-
noScope: !
|
|
5337
|
+
(0, import_traverse9.default)(ast, {
|
|
5338
|
+
noScope: !t12.isFile(ast),
|
|
3796
5339
|
enter: (nodePath) => inspect(nodePath.node)
|
|
3797
5340
|
});
|
|
3798
5341
|
return calls;
|
|
3799
5342
|
}
|
|
3800
5343
|
function hasReplaceOption(arg) {
|
|
3801
|
-
if (!arg || !
|
|
5344
|
+
if (!arg || !t12.isObjectExpression(arg)) return false;
|
|
3802
5345
|
return arg.properties.some(
|
|
3803
|
-
(prop) =>
|
|
5346
|
+
(prop) => t12.isObjectProperty(prop) && t12.isIdentifier(prop.key) && prop.key.name === "replace" && t12.isBooleanLiteral(prop.value) && prop.value.value === true
|
|
3804
5347
|
);
|
|
3805
5348
|
}
|
|
3806
5349
|
function isLocationExpression(node) {
|
|
3807
|
-
if (
|
|
3808
|
-
return
|
|
5350
|
+
if (t12.isIdentifier(node)) return node.name === "location";
|
|
5351
|
+
return t12.isMemberExpression(node) && t12.isIdentifier(node.object) && node.object.name === "window" && t12.isIdentifier(node.property) && node.property.name === "location";
|
|
3809
5352
|
}
|
|
3810
5353
|
|
|
3811
5354
|
// src/analyzers/web/WebScreenAnalyzer.ts
|
|
@@ -3833,10 +5376,10 @@ var WebScreenAnalyzer = class {
|
|
|
3833
5376
|
include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
|
|
3834
5377
|
exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
3835
5378
|
} = this.config;
|
|
3836
|
-
const files = (await (
|
|
5379
|
+
const files = (await globSorted(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
|
|
3837
5380
|
(file2) => !file2.endsWith(".d.ts")
|
|
3838
5381
|
);
|
|
3839
|
-
const patternMatches = await (
|
|
5382
|
+
const patternMatches = await globSorted(this.screenPatterns, {
|
|
3840
5383
|
cwd: this.config.rootDir,
|
|
3841
5384
|
ignore: exclude
|
|
3842
5385
|
});
|
|
@@ -3913,7 +5456,7 @@ var WebScreenAnalyzer = class {
|
|
|
3913
5456
|
// ── registerScreen ────────────────────────────────────────────────
|
|
3914
5457
|
detectRegisterScreenCall(ast) {
|
|
3915
5458
|
let found = false;
|
|
3916
|
-
(0,
|
|
5459
|
+
(0, import_traverse10.default)(ast, {
|
|
3917
5460
|
CallExpression: (nodePath) => {
|
|
3918
5461
|
if (found) return;
|
|
3919
5462
|
if (isRegisterScreenCallee(nodePath.node.callee)) {
|
|
@@ -3926,11 +5469,11 @@ var WebScreenAnalyzer = class {
|
|
|
3926
5469
|
}
|
|
3927
5470
|
extractRegisterScreenMetadata(ast) {
|
|
3928
5471
|
let plain = null;
|
|
3929
|
-
(0,
|
|
5472
|
+
(0, import_traverse10.default)(ast, {
|
|
3930
5473
|
CallExpression: (nodePath) => {
|
|
3931
5474
|
if (!isRegisterScreenCallee(nodePath.node.callee)) return;
|
|
3932
5475
|
const arg = nodePath.node.arguments[0];
|
|
3933
|
-
if (
|
|
5476
|
+
if (t13.isObjectExpression(arg)) {
|
|
3934
5477
|
plain = literalToPlain(arg);
|
|
3935
5478
|
}
|
|
3936
5479
|
}
|
|
@@ -3974,23 +5517,23 @@ var WebScreenAnalyzer = class {
|
|
|
3974
5517
|
extractComponentName(ast) {
|
|
3975
5518
|
let defaultName = "";
|
|
3976
5519
|
let firstExported = "";
|
|
3977
|
-
(0,
|
|
5520
|
+
(0, import_traverse10.default)(ast, {
|
|
3978
5521
|
ExportDefaultDeclaration: (nodePath) => {
|
|
3979
5522
|
const declaration = nodePath.node.declaration;
|
|
3980
|
-
if (
|
|
5523
|
+
if (t13.isFunctionDeclaration(declaration) && declaration.id?.name) {
|
|
3981
5524
|
defaultName = declaration.id.name;
|
|
3982
|
-
} else if (
|
|
5525
|
+
} else if (t13.isIdentifier(declaration)) {
|
|
3983
5526
|
defaultName = declaration.name;
|
|
3984
5527
|
}
|
|
3985
5528
|
},
|
|
3986
5529
|
ExportNamedDeclaration: (nodePath) => {
|
|
3987
5530
|
if (firstExported) return;
|
|
3988
5531
|
const declaration = nodePath.node.declaration;
|
|
3989
|
-
if (
|
|
5532
|
+
if (t13.isFunctionDeclaration(declaration) && declaration.id && /^[A-Z]/.test(declaration.id.name)) {
|
|
3990
5533
|
firstExported = declaration.id.name;
|
|
3991
|
-
} else if (
|
|
5534
|
+
} else if (t13.isVariableDeclaration(declaration)) {
|
|
3992
5535
|
for (const declarator of declaration.declarations) {
|
|
3993
|
-
if (
|
|
5536
|
+
if (t13.isIdentifier(declarator.id) && /^[A-Z]/.test(declarator.id.name) && (t13.isArrowFunctionExpression(declarator.init) || t13.isFunctionExpression(declarator.init))) {
|
|
3994
5537
|
firstExported = declarator.id.name;
|
|
3995
5538
|
break;
|
|
3996
5539
|
}
|
|
@@ -4003,7 +5546,7 @@ var WebScreenAnalyzer = class {
|
|
|
4003
5546
|
extractComponents(ast) {
|
|
4004
5547
|
const components = [];
|
|
4005
5548
|
const seen = /* @__PURE__ */ new Set();
|
|
4006
|
-
(0,
|
|
5549
|
+
(0, import_traverse10.default)(ast, {
|
|
4007
5550
|
JSXOpeningElement: (nodePath) => {
|
|
4008
5551
|
const element = nodePath.node;
|
|
4009
5552
|
const name = getJsxElementName(element);
|
|
@@ -4024,7 +5567,7 @@ var WebScreenAnalyzer = class {
|
|
|
4024
5567
|
// ── <label htmlFor> association ───────────────────────────────────
|
|
4025
5568
|
collectHtmlForLabels(ast) {
|
|
4026
5569
|
const labels = /* @__PURE__ */ new Map();
|
|
4027
|
-
(0,
|
|
5570
|
+
(0, import_traverse10.default)(ast, {
|
|
4028
5571
|
JSXElement: (nodePath) => {
|
|
4029
5572
|
const element = nodePath.node;
|
|
4030
5573
|
if (getJsxElementName(element.openingElement) !== "label") return;
|
|
@@ -4067,7 +5610,7 @@ var WebScreenAnalyzer = class {
|
|
|
4067
5610
|
formBuckets.set(formElement, bucket);
|
|
4068
5611
|
return bucket;
|
|
4069
5612
|
};
|
|
4070
|
-
(0,
|
|
5613
|
+
(0, import_traverse10.default)(ast, {
|
|
4071
5614
|
JSXElement: (nodePath) => {
|
|
4072
5615
|
const element = nodePath.node;
|
|
4073
5616
|
const name = getJsxElementName(element.openingElement);
|
|
@@ -4084,7 +5627,7 @@ var WebScreenAnalyzer = class {
|
|
|
4084
5627
|
if (!bucket.fields.has(field.name)) bucket.fields.set(field.name, field);
|
|
4085
5628
|
}
|
|
4086
5629
|
});
|
|
4087
|
-
(0,
|
|
5630
|
+
(0, import_traverse10.default)(ast, {
|
|
4088
5631
|
JSXElement: (nodePath) => {
|
|
4089
5632
|
const element = nodePath.node;
|
|
4090
5633
|
const name = getJsxElementName(element.openingElement);
|
|
@@ -4162,15 +5705,15 @@ var WebScreenAnalyzer = class {
|
|
|
4162
5705
|
});
|
|
4163
5706
|
}
|
|
4164
5707
|
for (const attr of opening.attributes) {
|
|
4165
|
-
if (!
|
|
5708
|
+
if (!t13.isJSXAttribute(attr) || !t13.isJSXIdentifier(attr.name)) continue;
|
|
4166
5709
|
const attrName = attr.name.name;
|
|
4167
5710
|
if (attrName === "required") {
|
|
4168
5711
|
if (attr.value === null) field.required = true;
|
|
4169
|
-
else if (
|
|
5712
|
+
else if (t13.isJSXExpressionContainer(attr.value) && t13.isBooleanLiteral(attr.value.expression)) {
|
|
4170
5713
|
field.required = attr.value.expression.value;
|
|
4171
5714
|
}
|
|
4172
5715
|
}
|
|
4173
|
-
if ((attrName === "value" || attrName === "checked") && attr.value &&
|
|
5716
|
+
if ((attrName === "value" || attrName === "checked") && attr.value && t13.isJSXExpressionContainer(attr.value) && t13.isIdentifier(attr.value.expression)) {
|
|
4174
5717
|
field.valueBinding = attr.value.expression.name;
|
|
4175
5718
|
if (!field.name) field.name = attr.value.expression.name;
|
|
4176
5719
|
}
|
|
@@ -4220,7 +5763,7 @@ var WebScreenAnalyzer = class {
|
|
|
4220
5763
|
extractSelectOptions(selectElement) {
|
|
4221
5764
|
const options = [];
|
|
4222
5765
|
for (const child of selectElement.children) {
|
|
4223
|
-
if (!
|
|
5766
|
+
if (!t13.isJSXElement(child)) continue;
|
|
4224
5767
|
if (getJsxElementName(child.openingElement) !== "option") continue;
|
|
4225
5768
|
const value = getStringAttr(child.openingElement, "value");
|
|
4226
5769
|
const label = jsxTextContent(child) || value || "";
|
|
@@ -4262,7 +5805,7 @@ var WebScreenAnalyzer = class {
|
|
|
4262
5805
|
const actionLabels = new Map(
|
|
4263
5806
|
actions.filter((a) => a.label).map((a) => [normalizeLabel(a.label), a])
|
|
4264
5807
|
);
|
|
4265
|
-
(0,
|
|
5808
|
+
(0, import_traverse10.default)(ast, {
|
|
4266
5809
|
JSXElement: (nodePath) => {
|
|
4267
5810
|
const element = nodePath.node;
|
|
4268
5811
|
const name = getJsxElementName(element.openingElement);
|
|
@@ -4331,14 +5874,14 @@ var WebScreenAnalyzer = class {
|
|
|
4331
5874
|
}
|
|
4332
5875
|
let handlerName;
|
|
4333
5876
|
const onClickAttr = opening.attributes.find(
|
|
4334
|
-
(attr) =>
|
|
5877
|
+
(attr) => t13.isJSXAttribute(attr) && t13.isJSXIdentifier(attr.name) && attr.name.name === "onClick"
|
|
4335
5878
|
);
|
|
4336
|
-
if (onClickAttr?.value &&
|
|
5879
|
+
if (onClickAttr?.value && t13.isJSXExpressionContainer(onClickAttr.value)) {
|
|
4337
5880
|
const expr = onClickAttr.value.expression;
|
|
4338
|
-
if (
|
|
5881
|
+
if (t13.isIdentifier(expr)) {
|
|
4339
5882
|
handlerName = expr.name;
|
|
4340
5883
|
action.handler = expr.name;
|
|
4341
|
-
} else if (!
|
|
5884
|
+
} else if (!t13.isJSXEmptyExpression(expr)) {
|
|
4342
5885
|
const inlineNavCalls = extractWebNavigationCalls(expr);
|
|
4343
5886
|
const inlineNav = inlineNavCalls.find((call) => call.targetPath);
|
|
4344
5887
|
if (inlineNav?.targetPath) {
|
|
@@ -4477,12 +6020,12 @@ var WebScreenAnalyzer = class {
|
|
|
4477
6020
|
}
|
|
4478
6021
|
handlerNameFromAttr(opening, attrName) {
|
|
4479
6022
|
const attr = opening.attributes.find(
|
|
4480
|
-
(candidate) =>
|
|
6023
|
+
(candidate) => t13.isJSXAttribute(candidate) && t13.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
|
|
4481
6024
|
);
|
|
4482
|
-
if (!attr?.value || !
|
|
6025
|
+
if (!attr?.value || !t13.isJSXExpressionContainer(attr.value)) return void 0;
|
|
4483
6026
|
const expr = attr.value.expression;
|
|
4484
|
-
if (
|
|
4485
|
-
if (
|
|
6027
|
+
if (t13.isIdentifier(expr)) return expr.name;
|
|
6028
|
+
if (t13.isArrowFunctionExpression(expr) || t13.isFunctionExpression(expr)) {
|
|
4486
6029
|
return firstCalledFunctionName(expr) ?? "anonymous";
|
|
4487
6030
|
}
|
|
4488
6031
|
return void 0;
|
|
@@ -4493,7 +6036,7 @@ var WebScreenAnalyzer = class {
|
|
|
4493
6036
|
for (const call of extractWebNavigationCalls(ast)) {
|
|
4494
6037
|
if (call.targetPath && call.targetPath.startsWith("/")) targets.add(call.targetPath);
|
|
4495
6038
|
}
|
|
4496
|
-
(0,
|
|
6039
|
+
(0, import_traverse10.default)(ast, {
|
|
4497
6040
|
JSXOpeningElement: (nodePath) => {
|
|
4498
6041
|
const element = nodePath.node;
|
|
4499
6042
|
const name = getJsxElementName(element);
|
|
@@ -4513,14 +6056,14 @@ var WebScreenAnalyzer = class {
|
|
|
4513
6056
|
extractCollections(ast) {
|
|
4514
6057
|
const collections = [];
|
|
4515
6058
|
const seen = /* @__PURE__ */ new Set();
|
|
4516
|
-
(0,
|
|
6059
|
+
(0, import_traverse10.default)(ast, {
|
|
4517
6060
|
JSXExpressionContainer: (nodePath) => {
|
|
4518
6061
|
const expr = nodePath.node.expression;
|
|
4519
|
-
if (!
|
|
6062
|
+
if (!t13.isCallExpression(expr) || !t13.isMemberExpression(expr.callee) || !t13.isIdentifier(expr.callee.object) || !t13.isIdentifier(expr.callee.property) || expr.callee.property.name !== "map") {
|
|
4520
6063
|
return;
|
|
4521
6064
|
}
|
|
4522
6065
|
const callback = expr.arguments[0];
|
|
4523
|
-
if (!
|
|
6066
|
+
if (!t13.isArrowFunctionExpression(callback) && !t13.isFunctionExpression(callback)) return;
|
|
4524
6067
|
const enclosing = nodePath.findParent(
|
|
4525
6068
|
(p) => p.isJSXElement()
|
|
4526
6069
|
);
|
|
@@ -4555,22 +6098,22 @@ var WebScreenAnalyzer = class {
|
|
|
4555
6098
|
}
|
|
4556
6099
|
};
|
|
4557
6100
|
function isRegisterScreenCallee(callee) {
|
|
4558
|
-
return
|
|
6101
|
+
return t13.isIdentifier(callee) && callee.name === "registerScreen" || t13.isMemberExpression(callee) && t13.isIdentifier(callee.property) && callee.property.name === "registerScreen";
|
|
4559
6102
|
}
|
|
4560
6103
|
function literalToPlain(node) {
|
|
4561
|
-
if (
|
|
6104
|
+
if (t13.isStringLiteral(node) || t13.isNumericLiteral(node) || t13.isBooleanLiteral(node)) {
|
|
4562
6105
|
return node.value;
|
|
4563
6106
|
}
|
|
4564
|
-
if (
|
|
4565
|
-
if (
|
|
4566
|
-
return node.elements.filter((el) => el !== null &&
|
|
6107
|
+
if (t13.isNullLiteral(node)) return null;
|
|
6108
|
+
if (t13.isArrayExpression(node)) {
|
|
6109
|
+
return node.elements.filter((el) => el !== null && t13.isExpression(el)).map((el) => literalToPlain(el)).filter((value) => value !== void 0);
|
|
4567
6110
|
}
|
|
4568
|
-
if (
|
|
6111
|
+
if (t13.isObjectExpression(node)) {
|
|
4569
6112
|
const out = {};
|
|
4570
6113
|
for (const prop of node.properties) {
|
|
4571
|
-
if (!
|
|
4572
|
-
const key =
|
|
4573
|
-
if (!key || !
|
|
6114
|
+
if (!t13.isObjectProperty(prop)) continue;
|
|
6115
|
+
const key = t13.isIdentifier(prop.key) ? prop.key.name : t13.isStringLiteral(prop.key) ? prop.key.value : void 0;
|
|
6116
|
+
if (!key || !t13.isExpression(prop.value)) continue;
|
|
4574
6117
|
const value = literalToPlain(prop.value);
|
|
4575
6118
|
if (value !== void 0) out[key] = value;
|
|
4576
6119
|
}
|
|
@@ -4582,12 +6125,12 @@ function jsxTextContent(element) {
|
|
|
4582
6125
|
const parts = [];
|
|
4583
6126
|
const walk = (children) => {
|
|
4584
6127
|
for (const child of children) {
|
|
4585
|
-
if (
|
|
6128
|
+
if (t13.isJSXText(child)) {
|
|
4586
6129
|
const trimmed = child.value.replace(/\s+/g, " ").trim();
|
|
4587
6130
|
if (trimmed) parts.push(trimmed);
|
|
4588
|
-
} else if (
|
|
6131
|
+
} else if (t13.isJSXExpressionContainer(child) && t13.isStringLiteral(child.expression)) {
|
|
4589
6132
|
parts.push(child.expression.value);
|
|
4590
|
-
} else if (
|
|
6133
|
+
} else if (t13.isJSXElement(child)) {
|
|
4591
6134
|
walk(child.children);
|
|
4592
6135
|
}
|
|
4593
6136
|
}
|
|
@@ -4597,26 +6140,26 @@ function jsxTextContent(element) {
|
|
|
4597
6140
|
return text.length > 0 ? text : void 0;
|
|
4598
6141
|
}
|
|
4599
6142
|
function firstCalledFunctionName(node) {
|
|
4600
|
-
if (
|
|
4601
|
-
if (
|
|
6143
|
+
if (t13.isIdentifier(node)) return node.name;
|
|
6144
|
+
if (t13.isArrowFunctionExpression(node) || t13.isFunctionExpression(node)) {
|
|
4602
6145
|
return firstCalledFunctionName(node.body);
|
|
4603
6146
|
}
|
|
4604
|
-
if (
|
|
6147
|
+
if (t13.isBlockStatement(node)) {
|
|
4605
6148
|
for (const statement of node.body) {
|
|
4606
6149
|
const handler = firstCalledFunctionName(statement);
|
|
4607
6150
|
if (handler) return handler;
|
|
4608
6151
|
}
|
|
4609
6152
|
return void 0;
|
|
4610
6153
|
}
|
|
4611
|
-
if (
|
|
4612
|
-
if (
|
|
6154
|
+
if (t13.isExpressionStatement(node)) return firstCalledFunctionName(node.expression);
|
|
6155
|
+
if (t13.isReturnStatement(node)) {
|
|
4613
6156
|
return node.argument ? firstCalledFunctionName(node.argument) : void 0;
|
|
4614
6157
|
}
|
|
4615
|
-
if (
|
|
6158
|
+
if (t13.isAwaitExpression(node) || t13.isUnaryExpression(node)) {
|
|
4616
6159
|
return firstCalledFunctionName(node.argument);
|
|
4617
6160
|
}
|
|
4618
|
-
if (
|
|
4619
|
-
if (
|
|
6161
|
+
if (t13.isCallExpression(node)) {
|
|
6162
|
+
if (t13.isIdentifier(node.callee) && !/^(navigate|confirm|alert)$/.test(node.callee.name)) {
|
|
4620
6163
|
return node.callee.name;
|
|
4621
6164
|
}
|
|
4622
6165
|
return void 0;
|
|
@@ -4625,14 +6168,14 @@ function firstCalledFunctionName(node) {
|
|
|
4625
6168
|
}
|
|
4626
6169
|
function containsWindowConfirm(body) {
|
|
4627
6170
|
let found = false;
|
|
4628
|
-
(0,
|
|
6171
|
+
(0, import_traverse10.default)(
|
|
4629
6172
|
body,
|
|
4630
6173
|
{
|
|
4631
6174
|
noScope: true,
|
|
4632
6175
|
CallExpression: (nodePath) => {
|
|
4633
6176
|
const callee = nodePath.node.callee;
|
|
4634
|
-
if (
|
|
4635
|
-
if (
|
|
6177
|
+
if (t13.isIdentifier(callee) && callee.name === "confirm") found = true;
|
|
6178
|
+
if (t13.isMemberExpression(callee) && t13.isIdentifier(callee.object) && callee.object.name === "window" && t13.isIdentifier(callee.property) && callee.property.name === "confirm") {
|
|
4636
6179
|
found = true;
|
|
4637
6180
|
}
|
|
4638
6181
|
}
|
|
@@ -4647,9 +6190,9 @@ function routePathAttr(opening, attrName) {
|
|
|
4647
6190
|
const literal = getStringAttr(opening, attrName);
|
|
4648
6191
|
if (literal) return literal;
|
|
4649
6192
|
const attr = opening.attributes.find(
|
|
4650
|
-
(candidate) =>
|
|
6193
|
+
(candidate) => t13.isJSXAttribute(candidate) && t13.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
|
|
4651
6194
|
);
|
|
4652
|
-
if (!attr?.value || !
|
|
6195
|
+
if (!attr?.value || !t13.isJSXExpressionContainer(attr.value)) return void 0;
|
|
4653
6196
|
return staticRoutePath(attr.value.expression);
|
|
4654
6197
|
}
|
|
4655
6198
|
function slugify(label) {
|
|
@@ -4664,10 +6207,10 @@ function isWeakInferredFieldName(name) {
|
|
|
4664
6207
|
function collectionItemNames(callback) {
|
|
4665
6208
|
const names = /* @__PURE__ */ new Set(["item"]);
|
|
4666
6209
|
const firstParam = callback.params[0];
|
|
4667
|
-
if (
|
|
4668
|
-
if (
|
|
6210
|
+
if (t13.isIdentifier(firstParam)) names.add(firstParam.name);
|
|
6211
|
+
if (t13.isObjectPattern(firstParam)) {
|
|
4669
6212
|
for (const prop of firstParam.properties) {
|
|
4670
|
-
if (
|
|
6213
|
+
if (t13.isObjectProperty(prop) && t13.isIdentifier(prop.key) && t13.isIdentifier(prop.value)) {
|
|
4671
6214
|
names.add(prop.value.name);
|
|
4672
6215
|
}
|
|
4673
6216
|
}
|
|
@@ -4677,13 +6220,13 @@ function collectionItemNames(callback) {
|
|
|
4677
6220
|
function collectionDisplayFields(callback, itemNames) {
|
|
4678
6221
|
const fields = /* @__PURE__ */ new Set();
|
|
4679
6222
|
if (!callback.body) return [];
|
|
4680
|
-
(0,
|
|
6223
|
+
(0, import_traverse10.default)(
|
|
4681
6224
|
callback.body,
|
|
4682
6225
|
{
|
|
4683
6226
|
noScope: true,
|
|
4684
6227
|
MemberExpression: (nodePath) => {
|
|
4685
6228
|
const node = nodePath.node;
|
|
4686
|
-
if (
|
|
6229
|
+
if (t13.isIdentifier(node.object) && itemNames.has(node.object.name) && t13.isIdentifier(node.property)) {
|
|
4687
6230
|
fields.add(node.property.name);
|
|
4688
6231
|
}
|
|
4689
6232
|
}
|
|
@@ -4694,16 +6237,16 @@ function collectionDisplayFields(callback, itemNames) {
|
|
|
4694
6237
|
function collectionKeyField(callback, itemNames) {
|
|
4695
6238
|
let keyField;
|
|
4696
6239
|
if (!callback.body) return void 0;
|
|
4697
|
-
(0,
|
|
6240
|
+
(0, import_traverse10.default)(
|
|
4698
6241
|
callback.body,
|
|
4699
6242
|
{
|
|
4700
6243
|
noScope: true,
|
|
4701
6244
|
JSXAttribute: (nodePath) => {
|
|
4702
6245
|
const attr = nodePath.node;
|
|
4703
|
-
if (!
|
|
4704
|
-
if (!attr.value || !
|
|
6246
|
+
if (!t13.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
|
|
6247
|
+
if (!attr.value || !t13.isJSXExpressionContainer(attr.value)) return;
|
|
4705
6248
|
const expr = attr.value.expression;
|
|
4706
|
-
if (
|
|
6249
|
+
if (t13.isMemberExpression(expr) && t13.isIdentifier(expr.object) && itemNames.has(expr.object.name) && t13.isIdentifier(expr.property)) {
|
|
4707
6250
|
keyField = keyField ?? expr.property.name;
|
|
4708
6251
|
}
|
|
4709
6252
|
}
|
|
@@ -4726,18 +6269,18 @@ function callbackReturnsTag(callback, tags) {
|
|
|
4726
6269
|
let found = false;
|
|
4727
6270
|
const inspect = (node) => {
|
|
4728
6271
|
if (!node || found) return;
|
|
4729
|
-
if (
|
|
6272
|
+
if (t13.isJSXElement(node)) {
|
|
4730
6273
|
const name = getJsxElementName(node.openingElement);
|
|
4731
6274
|
if (name && tags.has(name)) found = true;
|
|
4732
6275
|
return;
|
|
4733
6276
|
}
|
|
4734
|
-
if (
|
|
6277
|
+
if (t13.isBlockStatement(node)) {
|
|
4735
6278
|
for (const statement of node.body) {
|
|
4736
|
-
if (
|
|
6279
|
+
if (t13.isReturnStatement(statement)) inspect(statement.argument);
|
|
4737
6280
|
}
|
|
4738
6281
|
}
|
|
4739
|
-
if (
|
|
4740
|
-
if (
|
|
6282
|
+
if (t13.isParenthesizedExpression(node)) inspect(node.expression);
|
|
6283
|
+
if (t13.isConditionalExpression(node)) {
|
|
4741
6284
|
inspect(node.consequent);
|
|
4742
6285
|
inspect(node.alternate);
|
|
4743
6286
|
}
|
|
@@ -4764,9 +6307,8 @@ function inferIdentityFields(keyField, displayFields) {
|
|
|
4764
6307
|
// src/analyzers/web/WebNavigationAnalyzer.ts
|
|
4765
6308
|
var import_fs7 = require("fs");
|
|
4766
6309
|
var import_path6 = __toESM(require("path"));
|
|
4767
|
-
var
|
|
4768
|
-
var
|
|
4769
|
-
var t11 = __toESM(require("@babel/types"));
|
|
6310
|
+
var import_traverse11 = __toESM(require("@babel/traverse"));
|
|
6311
|
+
var t14 = __toESM(require("@babel/types"));
|
|
4770
6312
|
var WEB_NAVIGATOR_TYPE = "route";
|
|
4771
6313
|
var WebNavigationAnalyzer = class {
|
|
4772
6314
|
config;
|
|
@@ -4783,7 +6325,9 @@ var WebNavigationAnalyzer = class {
|
|
|
4783
6325
|
for (const filePath of files) {
|
|
4784
6326
|
try {
|
|
4785
6327
|
const content = await import_fs7.promises.readFile(filePath, "utf-8");
|
|
4786
|
-
if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
|
|
6328
|
+
if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
|
|
6329
|
+
content
|
|
6330
|
+
)) {
|
|
4787
6331
|
continue;
|
|
4788
6332
|
}
|
|
4789
6333
|
const ast = parseSource(content, this.config.parserPlugins);
|
|
@@ -4813,7 +6357,7 @@ var WebNavigationAnalyzer = class {
|
|
|
4813
6357
|
...this.config.exclude || [],
|
|
4814
6358
|
...this.navigationExclude
|
|
4815
6359
|
];
|
|
4816
|
-
const files = await (
|
|
6360
|
+
const files = await globSorted(patterns, { cwd: this.config.rootDir, ignore });
|
|
4817
6361
|
return files.map((file2) => import_path6.default.join(this.config.rootDir, file2));
|
|
4818
6362
|
}
|
|
4819
6363
|
// ── JSX <Route> style ────────────────────────────────────────────
|
|
@@ -4824,7 +6368,7 @@ var WebNavigationAnalyzer = class {
|
|
|
4824
6368
|
const name = getJsxElementName(opening);
|
|
4825
6369
|
if (name !== "Route") {
|
|
4826
6370
|
for (const child of element.children) {
|
|
4827
|
-
if (
|
|
6371
|
+
if (t14.isJSXElement(child)) visitRoute(child, parentPath);
|
|
4828
6372
|
}
|
|
4829
6373
|
return;
|
|
4830
6374
|
}
|
|
@@ -4833,16 +6377,16 @@ var WebNavigationAnalyzer = class {
|
|
|
4833
6377
|
const fullPath = this.joinPaths(parentPath, segment, isIndex);
|
|
4834
6378
|
const componentName = this.componentNameFromElementAttr(opening) ?? void 0;
|
|
4835
6379
|
const isLeaf = !element.children.some(
|
|
4836
|
-
(child) =>
|
|
6380
|
+
(child) => t14.isJSXElement(child) && getJsxElementName(child.openingElement) === "Route"
|
|
4837
6381
|
);
|
|
4838
6382
|
if ((segment || isIndex) && (componentName || isLeaf)) {
|
|
4839
6383
|
routes.push(this.buildRoute(fullPath, componentName, isIndex, !isLeaf));
|
|
4840
6384
|
}
|
|
4841
6385
|
for (const child of element.children) {
|
|
4842
|
-
if (
|
|
6386
|
+
if (t14.isJSXElement(child)) visitRoute(child, fullPath);
|
|
4843
6387
|
}
|
|
4844
6388
|
};
|
|
4845
|
-
(0,
|
|
6389
|
+
(0, import_traverse11.default)(ast, {
|
|
4846
6390
|
JSXElement: (nodePath) => {
|
|
4847
6391
|
const name = getJsxElementName(nodePath.node.openingElement);
|
|
4848
6392
|
if (name !== "Routes" && name !== "Route") return;
|
|
@@ -4861,13 +6405,13 @@ var WebNavigationAnalyzer = class {
|
|
|
4861
6405
|
/** `element={<VehicleList/>}` or `Component={VehicleList}`. */
|
|
4862
6406
|
componentNameFromElementAttr(opening) {
|
|
4863
6407
|
for (const attr of opening.attributes) {
|
|
4864
|
-
if (!
|
|
4865
|
-
if (attr.name.name === "element" &&
|
|
6408
|
+
if (!t14.isJSXAttribute(attr) || !t14.isJSXIdentifier(attr.name)) continue;
|
|
6409
|
+
if (attr.name.name === "element" && t14.isJSXExpressionContainer(attr.value)) {
|
|
4866
6410
|
const expr = attr.value.expression;
|
|
4867
|
-
if (
|
|
6411
|
+
if (t14.isJSXElement(expr)) return getJsxElementName(expr.openingElement);
|
|
4868
6412
|
}
|
|
4869
|
-
if (attr.name.name === "Component" &&
|
|
4870
|
-
if (
|
|
6413
|
+
if (attr.name.name === "Component" && t14.isJSXExpressionContainer(attr.value)) {
|
|
6414
|
+
if (t14.isIdentifier(attr.value.expression)) return attr.value.expression.name;
|
|
4871
6415
|
}
|
|
4872
6416
|
}
|
|
4873
6417
|
return null;
|
|
@@ -4881,12 +6425,12 @@ var WebNavigationAnalyzer = class {
|
|
|
4881
6425
|
"createMemoryRouter",
|
|
4882
6426
|
"useRoutes"
|
|
4883
6427
|
]);
|
|
4884
|
-
(0,
|
|
6428
|
+
(0, import_traverse11.default)(ast, {
|
|
4885
6429
|
CallExpression: (nodePath) => {
|
|
4886
6430
|
const callee = nodePath.node.callee;
|
|
4887
|
-
if (!
|
|
6431
|
+
if (!t14.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
|
|
4888
6432
|
const first = nodePath.node.arguments[0];
|
|
4889
|
-
if (!
|
|
6433
|
+
if (!t14.isArrayExpression(first)) return;
|
|
4890
6434
|
this.visitRouteObjects(first, "", routes);
|
|
4891
6435
|
}
|
|
4892
6436
|
});
|
|
@@ -4894,21 +6438,21 @@ var WebNavigationAnalyzer = class {
|
|
|
4894
6438
|
}
|
|
4895
6439
|
visitRouteObjects(arr, parentPath, out) {
|
|
4896
6440
|
for (const element of arr.elements) {
|
|
4897
|
-
if (!
|
|
6441
|
+
if (!t14.isObjectExpression(element)) continue;
|
|
4898
6442
|
let segment;
|
|
4899
6443
|
let isIndex = false;
|
|
4900
6444
|
let componentName;
|
|
4901
6445
|
let children;
|
|
4902
6446
|
for (const prop of element.properties) {
|
|
4903
|
-
if (!
|
|
6447
|
+
if (!t14.isObjectProperty(prop) || !t14.isIdentifier(prop.key)) continue;
|
|
4904
6448
|
const key = prop.key.name;
|
|
4905
|
-
if (key === "path" &&
|
|
4906
|
-
if (key === "index" &&
|
|
4907
|
-
if (key === "element" &&
|
|
6449
|
+
if (key === "path" && t14.isStringLiteral(prop.value)) segment = prop.value.value;
|
|
6450
|
+
if (key === "index" && t14.isBooleanLiteral(prop.value)) isIndex = prop.value.value;
|
|
6451
|
+
if (key === "element" && t14.isJSXElement(prop.value)) {
|
|
4908
6452
|
componentName = getJsxElementName(prop.value.openingElement) ?? void 0;
|
|
4909
6453
|
}
|
|
4910
|
-
if (key === "Component" &&
|
|
4911
|
-
if (key === "children" &&
|
|
6454
|
+
if (key === "Component" && t14.isIdentifier(prop.value)) componentName = prop.value.name;
|
|
6455
|
+
if (key === "children" && t14.isArrayExpression(prop.value)) children = prop.value;
|
|
4912
6456
|
}
|
|
4913
6457
|
const fullPath = this.joinPaths(parentPath, segment, isIndex);
|
|
4914
6458
|
if ((segment !== void 0 || isIndex) && (componentName || !children)) {
|
|
@@ -4986,11 +6530,13 @@ var WebNavigationAnalyzer = class {
|
|
|
4986
6530
|
return {
|
|
4987
6531
|
screens,
|
|
4988
6532
|
initialScreen: initialRoute?.screenName ?? "",
|
|
4989
|
-
navigators: routes.length > 0 ? [
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
6533
|
+
navigators: routes.length > 0 ? [
|
|
6534
|
+
{
|
|
6535
|
+
name: navigatorName,
|
|
6536
|
+
type: WEB_NAVIGATOR_TYPE,
|
|
6537
|
+
screens: screenNames
|
|
6538
|
+
}
|
|
6539
|
+
] : []
|
|
4994
6540
|
};
|
|
4995
6541
|
}
|
|
4996
6542
|
};
|
|
@@ -5067,23 +6613,23 @@ var ReactWebPlatformAnalyzer = class {
|
|
|
5067
6613
|
}
|
|
5068
6614
|
/** Replace route-path references with screen names where the route table resolves them. */
|
|
5069
6615
|
resolveRoutePaths(screen, routes) {
|
|
5070
|
-
const
|
|
6616
|
+
const resolve3 = (target) => {
|
|
5071
6617
|
if (!target || !target.startsWith("/")) return target;
|
|
5072
6618
|
return resolvePathToScreen(routes, target) ?? target;
|
|
5073
6619
|
};
|
|
5074
6620
|
const navigationTargets = Array.from(
|
|
5075
6621
|
new Set(
|
|
5076
|
-
screen.navigationTargets.map((target) =>
|
|
6622
|
+
screen.navigationTargets.map((target) => resolve3(target)).filter((target) => target !== screen.name)
|
|
5077
6623
|
)
|
|
5078
6624
|
).sort();
|
|
5079
6625
|
const actions = screen.actions.map((action) => {
|
|
5080
|
-
const resolved =
|
|
6626
|
+
const resolved = resolve3(action.targetScreen);
|
|
5081
6627
|
return resolved === action.targetScreen ? action : { ...action, targetScreen: resolved };
|
|
5082
6628
|
});
|
|
5083
6629
|
const collections = screen.collections?.map((collection) => {
|
|
5084
6630
|
const resolveRow = (row) => {
|
|
5085
6631
|
if (!row?.targetScreen) return row;
|
|
5086
|
-
const resolved =
|
|
6632
|
+
const resolved = resolve3(row.targetScreen);
|
|
5087
6633
|
if (resolved === row.targetScreen) return row;
|
|
5088
6634
|
return {
|
|
5089
6635
|
...row,
|
|
@@ -5109,7 +6655,7 @@ var ReactWebPlatformAnalyzer = class {
|
|
|
5109
6655
|
|
|
5110
6656
|
// src/manifest/loadManifest.ts
|
|
5111
6657
|
var import_promises3 = require("fs/promises");
|
|
5112
|
-
var
|
|
6658
|
+
var import_node_path4 = __toESM(require("path"));
|
|
5113
6659
|
|
|
5114
6660
|
// ../../node_modules/zod/v3/external.js
|
|
5115
6661
|
var external_exports = {};
|
|
@@ -5316,8 +6862,8 @@ var ZodParsedType = util.arrayToEnum([
|
|
|
5316
6862
|
"set"
|
|
5317
6863
|
]);
|
|
5318
6864
|
var getParsedType = (data) => {
|
|
5319
|
-
const
|
|
5320
|
-
switch (
|
|
6865
|
+
const t18 = typeof data;
|
|
6866
|
+
switch (t18) {
|
|
5321
6867
|
case "undefined":
|
|
5322
6868
|
return ZodParsedType.undefined;
|
|
5323
6869
|
case "string":
|
|
@@ -5589,8 +7135,8 @@ function getErrorMap() {
|
|
|
5589
7135
|
|
|
5590
7136
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
5591
7137
|
var makeIssue = (params) => {
|
|
5592
|
-
const { data, path:
|
|
5593
|
-
const fullPath = [...
|
|
7138
|
+
const { data, path: path13, errorMaps, issueData } = params;
|
|
7139
|
+
const fullPath = [...path13, ...issueData.path || []];
|
|
5594
7140
|
const fullIssue = {
|
|
5595
7141
|
...issueData,
|
|
5596
7142
|
path: fullPath
|
|
@@ -5706,11 +7252,11 @@ var errorUtil;
|
|
|
5706
7252
|
|
|
5707
7253
|
// ../../node_modules/zod/v3/types.js
|
|
5708
7254
|
var ParseInputLazyPath = class {
|
|
5709
|
-
constructor(parent, value,
|
|
7255
|
+
constructor(parent, value, path13, key) {
|
|
5710
7256
|
this._cachedPath = [];
|
|
5711
7257
|
this.parent = parent;
|
|
5712
7258
|
this.data = value;
|
|
5713
|
-
this._path =
|
|
7259
|
+
this._path = path13;
|
|
5714
7260
|
this._key = key;
|
|
5715
7261
|
}
|
|
5716
7262
|
get path() {
|
|
@@ -9152,7 +10698,36 @@ var coerce = {
|
|
|
9152
10698
|
};
|
|
9153
10699
|
var NEVER = INVALID;
|
|
9154
10700
|
|
|
9155
|
-
// ../shared/dist/chunk-
|
|
10701
|
+
// ../shared/dist/chunk-SWQOZWHP.mjs
|
|
10702
|
+
var SAFE_IMAGE_URL_RE = /^(?:https?:\/\/|data:image\/(?:png|jpe?g|gif|webp);base64,)/i;
|
|
10703
|
+
var HAS_SCHEME_RE = /^[\s\u0000-\u001f]*[a-z][a-z0-9+.-]*:/i;
|
|
10704
|
+
function isSafeImageUrl(value) {
|
|
10705
|
+
if (value === null || value === void 0) return true;
|
|
10706
|
+
const trimmed = value.trim();
|
|
10707
|
+
if (trimmed === "") return true;
|
|
10708
|
+
if (!HAS_SCHEME_RE.test(trimmed)) return true;
|
|
10709
|
+
return SAFE_IMAGE_URL_RE.test(trimmed);
|
|
10710
|
+
}
|
|
10711
|
+
|
|
10712
|
+
// ../shared/dist/chunk-KUMPS6RH.mjs
|
|
10713
|
+
var SUPPORTED_APPILOTS_LOCALES = ["pt-BR", "en", "es", "fr"];
|
|
10714
|
+
|
|
10715
|
+
// ../shared/dist/chunk-2GTRKNPJ.mjs
|
|
10716
|
+
var symbols = external_exports.array(external_exports.string().max(240)).max(12);
|
|
10717
|
+
var controlEvidenceSchema = external_exports.object({
|
|
10718
|
+
version: external_exports.literal(1),
|
|
10719
|
+
siteId: external_exports.string().regex(/^[a-f0-9]{20}$/),
|
|
10720
|
+
component: external_exports.string().max(240),
|
|
10721
|
+
icons: symbols,
|
|
10722
|
+
handler: external_exports.string().max(240).optional(),
|
|
10723
|
+
calls: symbols,
|
|
10724
|
+
argumentBindings: symbols,
|
|
10725
|
+
conditions: symbols,
|
|
10726
|
+
nativeConfirmation: external_exports.object({
|
|
10727
|
+
title: external_exports.string().max(240).optional(),
|
|
10728
|
+
destructiveOption: external_exports.boolean()
|
|
10729
|
+
}).optional()
|
|
10730
|
+
});
|
|
9156
10731
|
var locatorSourceSchema = external_exports.enum([
|
|
9157
10732
|
"appilotsId",
|
|
9158
10733
|
"testID",
|
|
@@ -9331,6 +10906,23 @@ var actionDescriptorSchema = external_exports.object({
|
|
|
9331
10906
|
effect: external_exports.enum(["read", "write", "destructive"]).or(external_exports.string()).optional(),
|
|
9332
10907
|
riskLevel: external_exports.enum(["low", "medium", "high"]).or(external_exports.string()).optional(),
|
|
9333
10908
|
requiresConfirmation: external_exports.boolean().optional(),
|
|
10909
|
+
/**
|
|
10910
|
+
* How long this action's work is expected to take, in milliseconds,
|
|
10911
|
+
* DECLARED by the app — not inferred (that is `appilotsInferred`).
|
|
10912
|
+
*
|
|
10913
|
+
* The SDK's post-action wait was a constant: 6s, or 10s when the
|
|
10914
|
+
* generator could prove the handler awaits something. No constant
|
|
10915
|
+
* fits, because app operations run from ~100ms to minutes, and the
|
|
10916
|
+
* failure is silent in both directions — too short and the agent
|
|
10917
|
+
* photographs a loading screen with no controls on it, too long and
|
|
10918
|
+
* every fast press pays for the slowest one. Only the app knows.
|
|
10919
|
+
*
|
|
10920
|
+
* Absent means absent: the SDK keeps its current defaults, so a
|
|
10921
|
+
* document generated before this field existed behaves exactly as it
|
|
10922
|
+
* did. The SDK also clamps the value to its own safety ceiling — a
|
|
10923
|
+
* declared budget is a request, not a licence to hang the session.
|
|
10924
|
+
*/
|
|
10925
|
+
asyncBudgetMs: external_exports.number().int().positive().optional(),
|
|
9334
10926
|
appilotsInferred: appilotsInferredActionSchema.optional()
|
|
9335
10927
|
}).passthrough();
|
|
9336
10928
|
var screenPermissionDescriptorSchema = external_exports.object({
|
|
@@ -9513,7 +11105,7 @@ var uploadMCPDocumentSchema = external_exports.object({
|
|
|
9513
11105
|
version: external_exports.string().default("1.0"),
|
|
9514
11106
|
content: external_exports.record(external_exports.unknown())
|
|
9515
11107
|
});
|
|
9516
|
-
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
|
|
11108
|
+
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator", "publish"]);
|
|
9517
11109
|
var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
|
|
9518
11110
|
var createApiKeySchema = external_exports.object({
|
|
9519
11111
|
name: external_exports.string().min(1).max(100),
|
|
@@ -9524,8 +11116,8 @@ var createApiKeySchema = external_exports.object({
|
|
|
9524
11116
|
scope: apiKeyScopeSchema.default("sdk"),
|
|
9525
11117
|
environment: apiKeyEnvironmentSchema.optional(),
|
|
9526
11118
|
expiresAt: external_exports.string().datetime().optional()
|
|
9527
|
-
}).refine((v) => v.scope
|
|
9528
|
-
message:
|
|
11119
|
+
}).refine((v) => v.scope === "operator" || !!v.projectId, {
|
|
11120
|
+
message: "projectId is required for SDK and publishing keys",
|
|
9529
11121
|
path: ["projectId"]
|
|
9530
11122
|
});
|
|
9531
11123
|
var boundedString = (max) => external_exports.string().max(max);
|
|
@@ -9550,10 +11142,21 @@ var snapshotInputSchema = external_exports.object({
|
|
|
9550
11142
|
type: boundedString(40).optional(),
|
|
9551
11143
|
required: external_exports.boolean().optional(),
|
|
9552
11144
|
invalid: external_exports.boolean().optional(),
|
|
11145
|
+
/**
|
|
11146
|
+
* Whether the field holds anything, as its own fact rather than an
|
|
11147
|
+
* inference over `value`. The value is the user's and is the first
|
|
11148
|
+
* thing a privacy policy withholds; the existence of a value is the
|
|
11149
|
+
* agent's and is what stops it filling the same field twice.
|
|
11150
|
+
*/
|
|
11151
|
+
filled: external_exports.boolean().optional(),
|
|
11152
|
+
/** This input has keyboard focus right now. */
|
|
11153
|
+
focused: external_exports.boolean().optional(),
|
|
9553
11154
|
inModal: external_exports.boolean().optional()
|
|
9554
11155
|
}).passthrough();
|
|
9555
11156
|
var snapshotButtonSchema = external_exports.object({
|
|
11157
|
+
controlEvidence: controlEvidenceSchema.optional(),
|
|
9556
11158
|
id: boundedString(160).optional(),
|
|
11159
|
+
dispatchable: external_exports.boolean().optional(),
|
|
9557
11160
|
provenance: identityProvenanceSchema.optional(),
|
|
9558
11161
|
/**
|
|
9559
11162
|
* False when the control is mounted but currently OUTSIDE the window —
|
|
@@ -9614,7 +11217,36 @@ var snapshotListSchema = external_exports.object({
|
|
|
9614
11217
|
*/
|
|
9615
11218
|
source: boundedString(40).optional(),
|
|
9616
11219
|
itemCount: external_exports.number().int().optional(),
|
|
11220
|
+
/**
|
|
11221
|
+
* Size of the whole collection when the app declared it, for a list
|
|
11222
|
+
* that is a WINDOW onto more data than it holds.
|
|
11223
|
+
*
|
|
11224
|
+
* `itemCount` is how many rows the list is rendering from and
|
|
11225
|
+
* `visibleItemCount` how many of those are mounted; neither can
|
|
11226
|
+
* express "there are 36 and you are looking at the first 20",
|
|
11227
|
+
* because a paginated list's `data` is the page. Absent means
|
|
11228
|
+
* unknown — never "same as itemCount".
|
|
11229
|
+
*/
|
|
11230
|
+
totalItemCount: external_exports.number().int().optional(),
|
|
9617
11231
|
visibleItemCount: external_exports.number().int().optional(),
|
|
11232
|
+
viewportItemCount: external_exports.number().int().nonnegative().optional(),
|
|
11233
|
+
exploration: external_exports.object({
|
|
11234
|
+
revision: external_exports.number().int().nonnegative(),
|
|
11235
|
+
observedItemCount: external_exports.number().int().min(0).max(5e3),
|
|
11236
|
+
observedRanges: external_exports.array(
|
|
11237
|
+
external_exports.object({
|
|
11238
|
+
start: external_exports.number().int().nonnegative(),
|
|
11239
|
+
end: external_exports.number().int().nonnegative()
|
|
11240
|
+
})
|
|
11241
|
+
).max(32),
|
|
11242
|
+
rangesTruncated: external_exports.boolean(),
|
|
11243
|
+
coverage: external_exports.enum(["partial", "all-loaded"]),
|
|
11244
|
+
pagination: external_exports.enum(["possible", "not-declared"]),
|
|
11245
|
+
scrollSteps: external_exports.number().int().nonnegative(),
|
|
11246
|
+
remainingScrollSteps: external_exports.number().int().nonnegative(),
|
|
11247
|
+
consecutiveNoProgress: external_exports.number().int().nonnegative(),
|
|
11248
|
+
lastScroll: external_exports.enum(["moved", "no-progress", "boundary", "unverified"]).optional()
|
|
11249
|
+
}).optional(),
|
|
9618
11250
|
refreshing: external_exports.boolean().optional(),
|
|
9619
11251
|
empty: external_exports.boolean().optional(),
|
|
9620
11252
|
label: boundedString(300).optional(),
|
|
@@ -9662,6 +11294,7 @@ var snapshotChoiceGroupSchema = external_exports.object({
|
|
|
9662
11294
|
}).passthrough();
|
|
9663
11295
|
var snapshotElementSchema = external_exports.object({
|
|
9664
11296
|
id: boundedString(160).optional(),
|
|
11297
|
+
dispatchable: external_exports.boolean().optional(),
|
|
9665
11298
|
role: boundedString(40).optional(),
|
|
9666
11299
|
label: boundedString(300).optional(),
|
|
9667
11300
|
texts: external_exports.array(boundedString(500)).max(50).optional(),
|
|
@@ -9705,6 +11338,34 @@ var agentSnapshotSchema = external_exports.object({
|
|
|
9705
11338
|
* it. Optional: older SDKs never clamp and never send it.
|
|
9706
11339
|
*/
|
|
9707
11340
|
truncated: external_exports.boolean().optional(),
|
|
11341
|
+
/**
|
|
11342
|
+
* What moved since the previous observation, computed on the device
|
|
11343
|
+
* because that is the only side holding both snapshots.
|
|
11344
|
+
*
|
|
11345
|
+
* Shape only — counts, booleans, and ids the app declared itself — so
|
|
11346
|
+
* it survives an observation whose content was withheld. ABSENT means
|
|
11347
|
+
* there was no previous observation to compare against; `unchanged:
|
|
11348
|
+
* true` means we compared and nothing moved, which is the strongest
|
|
11349
|
+
* evidence available that an action did nothing. The two must not be
|
|
11350
|
+
* collapsed, for the same reason `truncated` exists.
|
|
11351
|
+
*/
|
|
11352
|
+
delta: external_exports.object({
|
|
11353
|
+
routeChanged: external_exports.boolean().optional(),
|
|
11354
|
+
modalOpened: external_exports.boolean().optional(),
|
|
11355
|
+
modalClosed: external_exports.boolean().optional(),
|
|
11356
|
+
loadingStarted: external_exports.boolean().optional(),
|
|
11357
|
+
loadingFinished: external_exports.boolean().optional(),
|
|
11358
|
+
textsAdded: external_exports.number().int().nonnegative().optional(),
|
|
11359
|
+
textsRemoved: external_exports.number().int().nonnegative().optional(),
|
|
11360
|
+
buttonsAddedIndices: external_exports.array(external_exports.number().int().nonnegative()).max(12).optional(),
|
|
11361
|
+
buttonsRemoved: external_exports.number().int().nonnegative().optional(),
|
|
11362
|
+
visibleRowsDelta: external_exports.number().int().optional(),
|
|
11363
|
+
totalRowsDelta: external_exports.number().int().optional(),
|
|
11364
|
+
fieldsNewlyFilled: external_exports.array(boundedString(160)).max(12).optional(),
|
|
11365
|
+
fieldsCleared: external_exports.array(boundedString(160)).max(12).optional(),
|
|
11366
|
+
invalidAppeared: external_exports.boolean().optional(),
|
|
11367
|
+
unchanged: external_exports.boolean().optional()
|
|
11368
|
+
}).passthrough().optional(),
|
|
9708
11369
|
/**
|
|
9709
11370
|
* How many of this screen's controls the client could name, split by
|
|
9710
11371
|
* `identityProvenance`. Diagnostic only — the relay never grounds an
|
|
@@ -9723,6 +11384,10 @@ var agentSnapshotSchema = external_exports.object({
|
|
|
9723
11384
|
}).passthrough().optional()
|
|
9724
11385
|
}).passthrough();
|
|
9725
11386
|
var agentContextSchema = external_exports.object({
|
|
11387
|
+
missionProtocol: external_exports.literal(1).optional(),
|
|
11388
|
+
missionId: boundedString(160).optional(),
|
|
11389
|
+
/** Preferred supported device language, reported by the SDK independently of map/UI labels. */
|
|
11390
|
+
deviceLocale: external_exports.enum(SUPPORTED_APPILOTS_LOCALES).optional(),
|
|
9726
11391
|
/**
|
|
9727
11392
|
* Client platform this observation was captured on. Optional and
|
|
9728
11393
|
* additive (see `clientPlatformSchema`) — absent means
|
|
@@ -9738,7 +11403,19 @@ var agentContextSchema = external_exports.object({
|
|
|
9738
11403
|
rootRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
|
|
9739
11404
|
currentRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
|
|
9740
11405
|
routeNames: external_exports.array(boundedString(200)).max(500).optional(),
|
|
9741
|
-
canGoBack: external_exports.boolean().optional()
|
|
11406
|
+
canGoBack: external_exports.boolean().optional(),
|
|
11407
|
+
/**
|
|
11408
|
+
* The stack a back press pops through, oldest first, ending on
|
|
11409
|
+
* the current screen. `canGoBack` says a back exists; this says
|
|
11410
|
+
* where it goes.
|
|
11411
|
+
*/
|
|
11412
|
+
backStack: external_exports.array(boundedString(200)).max(50).optional(),
|
|
11413
|
+
/**
|
|
11414
|
+
* Screens the session has been on, oldest first. Names only —
|
|
11415
|
+
* params carry record ids and often personal data, and a route
|
|
11416
|
+
* name is structure.
|
|
11417
|
+
*/
|
|
11418
|
+
visited: external_exports.array(boundedString(200)).max(8).optional()
|
|
9742
11419
|
}).passthrough().optional(),
|
|
9743
11420
|
screenMetadata: external_exports.object({
|
|
9744
11421
|
name: boundedString(200).optional(),
|
|
@@ -9844,7 +11521,7 @@ var formFillPayloadSchema = external_exports.object({
|
|
|
9844
11521
|
submitAfterFill: external_exports.boolean().optional()
|
|
9845
11522
|
}).passthrough();
|
|
9846
11523
|
var uiInteractionPayloadSchema = external_exports.object({
|
|
9847
|
-
action: external_exports.enum(["press", "longPress", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
|
|
11524
|
+
action: external_exports.enum(["press", "longPress", "toggle", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
|
|
9848
11525
|
// `targetId` is the canonical server/LLM field. SDK runtimes still
|
|
9849
11526
|
// accept `componentId` as a compatibility alias.
|
|
9850
11527
|
targetId: external_exports.string().min(1),
|
|
@@ -9961,6 +11638,12 @@ var actionDiagnoseSchema = external_exports.object({
|
|
|
9961
11638
|
requiresUserInput: external_exports.boolean().optional()
|
|
9962
11639
|
});
|
|
9963
11640
|
var actionResultSchema = external_exports.object({
|
|
11641
|
+
nativeConfirmation: external_exports.object({
|
|
11642
|
+
title: external_exports.string().max(300),
|
|
11643
|
+
message: external_exports.string().max(1e3).optional(),
|
|
11644
|
+
buttonLabel: external_exports.string().max(160).optional(),
|
|
11645
|
+
handlerCompleted: external_exports.boolean()
|
|
11646
|
+
}).optional(),
|
|
9964
11647
|
actionId: external_exports.string(),
|
|
9965
11648
|
type: external_exports.string(),
|
|
9966
11649
|
success: external_exports.boolean(),
|
|
@@ -10012,8 +11695,15 @@ var continueAgentSchema = external_exports.object({
|
|
|
10012
11695
|
// issue #169 — the observation the server grounds targets against, now
|
|
10013
11696
|
// validated + bounded at the border instead of z.record(z.unknown()).
|
|
10014
11697
|
context: agentContextSchema.optional(),
|
|
10015
|
-
/**
|
|
10016
|
-
|
|
11698
|
+
/**
|
|
11699
|
+
* Hop counter — server enforces a cap to prevent runaway loops.
|
|
11700
|
+
* The max here must stay ABOVE the server's `MAX_AGENT_HOPS` (10 since
|
|
11701
|
+
* #503, apps/api/src/modules/agents/routes.ts): the first over-budget
|
|
11702
|
+
* hop has to get through validation so the route can answer it with
|
|
11703
|
+
* the friendly automation-limit message instead of a 422. The +3
|
|
11704
|
+
* headroom mirrors what 7/10 was before the recalibration.
|
|
11705
|
+
*/
|
|
11706
|
+
hop: external_exports.number().int().min(1).max(13).optional()
|
|
10017
11707
|
});
|
|
10018
11708
|
var agentAccessLevelSchema = external_exports.enum(["read", "write", "none"]);
|
|
10019
11709
|
var screenPermissionSchema = external_exports.object({
|
|
@@ -10081,7 +11771,7 @@ var updateProjectBudgetSchema = external_exports.object({
|
|
|
10081
11771
|
/** null = remove webhook, undefined = leave unchanged. */
|
|
10082
11772
|
budgetWebhookUrl: external_exports.string().url().nullable().optional()
|
|
10083
11773
|
}).strict();
|
|
10084
|
-
var localeSchema = external_exports.enum(
|
|
11774
|
+
var localeSchema = external_exports.enum(SUPPORTED_APPILOTS_LOCALES);
|
|
10085
11775
|
var hexColorSchema = external_exports.string().regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
|
|
10086
11776
|
message: "Must be a hex color like #6366f1"
|
|
10087
11777
|
});
|
|
@@ -10119,15 +11809,18 @@ var themeTokensSchema = external_exports.object({
|
|
|
10119
11809
|
*/
|
|
10120
11810
|
mode: external_exports.enum(["auto", "light", "dark"]).optional()
|
|
10121
11811
|
}).strict();
|
|
11812
|
+
var imageSourceField = external_exports.string().max(500).refine(isSafeImageUrl, {
|
|
11813
|
+
message: "Only http(s) URLs, base64 image data URIs, emoji or asset ids are allowed here"
|
|
11814
|
+
});
|
|
10122
11815
|
var projectPersonalizationSchema = external_exports.object({
|
|
10123
11816
|
/** Tone / persona instructions appended to the system prompt. */
|
|
10124
11817
|
personaPrompt: external_exports.string().max(2048).nullable(),
|
|
10125
11818
|
/** Display name in the chat header (e.g. "Aria"). */
|
|
10126
11819
|
assistantName: external_exports.string().max(60).nullable(),
|
|
10127
11820
|
/** URL or remote asset id for the avatar shown next to assistant turns. */
|
|
10128
|
-
assistantAvatar:
|
|
11821
|
+
assistantAvatar: imageSourceField.nullable(),
|
|
10129
11822
|
/** Emoji or image URL for the empty-state icon. Auto-detected by prefix. */
|
|
10130
|
-
emptyStateIcon:
|
|
11823
|
+
emptyStateIcon: imageSourceField.nullable(),
|
|
10131
11824
|
/** First-message text shown in the empty state. */
|
|
10132
11825
|
welcomeMessage: external_exports.string().max(500).nullable(),
|
|
10133
11826
|
/** Title rendered at the top of the chat. */
|
|
@@ -10141,20 +11834,20 @@ var projectPersonalizationSchema = external_exports.object({
|
|
|
10141
11834
|
/** Chat-open FAB background color. Null falls back to theme.colors.primary. */
|
|
10142
11835
|
triggerButtonColor: hexColorSchema.nullable(),
|
|
10143
11836
|
/** Image URL rendered inside the FAB instead of the default chat-bubble icon. */
|
|
10144
|
-
triggerButtonImageUrl:
|
|
11837
|
+
triggerButtonImageUrl: imageSourceField.nullable()
|
|
10145
11838
|
});
|
|
10146
11839
|
var updateProjectPersonalizationSchema = external_exports.object({
|
|
10147
11840
|
personaPrompt: external_exports.string().max(2048).nullable().optional(),
|
|
10148
11841
|
assistantName: external_exports.string().max(60).nullable().optional(),
|
|
10149
|
-
assistantAvatar:
|
|
10150
|
-
emptyStateIcon:
|
|
11842
|
+
assistantAvatar: imageSourceField.nullable().optional(),
|
|
11843
|
+
emptyStateIcon: imageSourceField.nullable().optional(),
|
|
10151
11844
|
welcomeMessage: external_exports.string().max(500).nullable().optional(),
|
|
10152
11845
|
chatTitle: external_exports.string().max(60).nullable().optional(),
|
|
10153
11846
|
poweredByVisible: external_exports.boolean().optional(),
|
|
10154
11847
|
theme: themeTokensSchema.nullable().optional(),
|
|
10155
11848
|
defaultLocale: localeSchema.nullable().optional(),
|
|
10156
11849
|
triggerButtonColor: hexColorSchema.nullable().optional(),
|
|
10157
|
-
triggerButtonImageUrl:
|
|
11850
|
+
triggerButtonImageUrl: imageSourceField.nullable().optional()
|
|
10158
11851
|
}).strict();
|
|
10159
11852
|
var sandboxObservationSchema = external_exports.object({
|
|
10160
11853
|
route: external_exports.string().max(200).optional(),
|
|
@@ -10174,7 +11867,19 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
10174
11867
|
type: external_exports.string().max(40).optional(),
|
|
10175
11868
|
placeholder: external_exports.string().max(200).optional(),
|
|
10176
11869
|
/** Inline validation state (SDK snapshot field). */
|
|
10177
|
-
invalid: external_exports.boolean().optional()
|
|
11870
|
+
invalid: external_exports.boolean().optional(),
|
|
11871
|
+
/**
|
|
11872
|
+
* Whether the field holds anything, as its own fact rather than
|
|
11873
|
+
* an inference over `value`.
|
|
11874
|
+
*
|
|
11875
|
+
* Here because the eval posts through this schema: a fact the
|
|
11876
|
+
* SDK produces and this contract has no word for is a fact the
|
|
11877
|
+
* corpus can never grade the agent on. `invalid` and `required`
|
|
11878
|
+
* were already here; these two were the half that was missing.
|
|
11879
|
+
*/
|
|
11880
|
+
filled: external_exports.boolean().optional(),
|
|
11881
|
+
/** This input has keyboard focus right now. */
|
|
11882
|
+
focused: external_exports.boolean().optional()
|
|
10178
11883
|
})
|
|
10179
11884
|
).max(100).optional(),
|
|
10180
11885
|
buttons: external_exports.array(
|
|
@@ -10632,7 +12337,7 @@ var manifestSchema = external_exports.object({
|
|
|
10632
12337
|
navigation: navigationGraphSchema.partial().optional()
|
|
10633
12338
|
}).passthrough();
|
|
10634
12339
|
async function loadManifest(rootDir, manifestPath) {
|
|
10635
|
-
const resolvedPath =
|
|
12340
|
+
const resolvedPath = import_node_path4.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
|
|
10636
12341
|
let raw;
|
|
10637
12342
|
try {
|
|
10638
12343
|
raw = await (0, import_promises3.readFile)(resolvedPath, "utf-8");
|
|
@@ -10781,7 +12486,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10781
12486
|
const agentReadyScreens = mergedScreens.map(
|
|
10782
12487
|
(screen) => enrichScreenForAgent({
|
|
10783
12488
|
...screen,
|
|
10784
|
-
filePath: screen.filePath ?
|
|
12489
|
+
filePath: screen.filePath ? import_node_path5.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
|
|
10785
12490
|
})
|
|
10786
12491
|
);
|
|
10787
12492
|
const projectInfo = await this.getProjectInfo();
|
|
@@ -10805,19 +12510,33 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10805
12510
|
}
|
|
10806
12511
|
};
|
|
10807
12512
|
const serialized = JSON.stringify(document, null, 2);
|
|
10808
|
-
const checksum = this.calculateChecksum(
|
|
10809
|
-
const filePath =
|
|
12513
|
+
const checksum = this.calculateChecksum(serializeForChecksum(document));
|
|
12514
|
+
const filePath = import_node_path5.default.resolve(outputDir, `mcp-document.${this.options.format}`);
|
|
10810
12515
|
await (0, import_promises4.writeFile)(filePath, serialized, "utf-8");
|
|
10811
12516
|
console.log(`[MCPGenerator] Document written to: ${filePath}`);
|
|
10812
|
-
const
|
|
12517
|
+
const controlFiles = analyzed.controlEvidenceFiles ?? {};
|
|
12518
|
+
await (0, import_promises4.writeFile)(
|
|
12519
|
+
import_node_path5.default.resolve(outputDir, "control-evidence.json"),
|
|
12520
|
+
JSON.stringify({ version: 1, files: controlFiles }, null, 2),
|
|
12521
|
+
"utf-8"
|
|
12522
|
+
);
|
|
12523
|
+
const checksumFilePath = import_node_path5.default.resolve(outputDir, ".appilots-checksum");
|
|
10813
12524
|
await (0, import_promises4.writeFile)(checksumFilePath, checksum, "utf-8");
|
|
10814
12525
|
console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
|
|
12526
|
+
const evidenceCount = Object.values(controlFiles).reduce(
|
|
12527
|
+
(sum, entries) => sum + entries.length,
|
|
12528
|
+
0
|
|
12529
|
+
);
|
|
12530
|
+
console.log(
|
|
12531
|
+
`[MCPGenerator] Source evidence: ${evidenceCount} icon controls across ${Object.keys(controlFiles).length} files (runtime binding required)`
|
|
12532
|
+
);
|
|
10815
12533
|
console.log("[MCPGenerator] Generation complete!");
|
|
10816
12534
|
return {
|
|
10817
12535
|
document,
|
|
10818
12536
|
filePath,
|
|
10819
12537
|
format: this.options.format,
|
|
10820
|
-
checksum
|
|
12538
|
+
checksum,
|
|
12539
|
+
...analyzed.diagnostics ? { diagnostics: analyzed.diagnostics } : {}
|
|
10821
12540
|
};
|
|
10822
12541
|
}
|
|
10823
12542
|
/**
|
|
@@ -10829,7 +12548,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10829
12548
|
* after calling `generate()`.
|
|
10830
12549
|
*/
|
|
10831
12550
|
static async readPreviousChecksum(outputDir) {
|
|
10832
|
-
const checksumFilePath =
|
|
12551
|
+
const checksumFilePath = import_node_path5.default.resolve(outputDir, ".appilots-checksum");
|
|
10833
12552
|
try {
|
|
10834
12553
|
const content = await (0, import_promises4.readFile)(checksumFilePath, "utf-8");
|
|
10835
12554
|
return content.trim() || null;
|
|
@@ -10841,14 +12560,14 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10841
12560
|
* Calculate SHA-256 checksum of content
|
|
10842
12561
|
*/
|
|
10843
12562
|
calculateChecksum(content) {
|
|
10844
|
-
return (0,
|
|
12563
|
+
return (0, import_node_crypto2.createHash)("sha256").update(content).digest("hex");
|
|
10845
12564
|
}
|
|
10846
12565
|
/**
|
|
10847
12566
|
* Get project name and version from package.json in rootDir
|
|
10848
12567
|
*/
|
|
10849
12568
|
async getProjectInfo() {
|
|
10850
12569
|
try {
|
|
10851
|
-
const packageJsonPath =
|
|
12570
|
+
const packageJsonPath = import_node_path5.default.resolve(this.analyzerConfig.rootDir, "package.json");
|
|
10852
12571
|
const packageJsonContent = await (0, import_promises4.readFile)(packageJsonPath, "utf-8");
|
|
10853
12572
|
const packageJson = JSON.parse(packageJsonContent);
|
|
10854
12573
|
return {
|
|
@@ -10923,8 +12642,8 @@ function reportMetadataLint(document, strict) {
|
|
|
10923
12642
|
}
|
|
10924
12643
|
|
|
10925
12644
|
// src/cli/utils/remote-drift.ts
|
|
10926
|
-
var
|
|
10927
|
-
var
|
|
12645
|
+
var import_node_fs3 = require("fs");
|
|
12646
|
+
var import_node_path6 = require("path");
|
|
10928
12647
|
var STUB_RATIO = 0.5;
|
|
10929
12648
|
function compareWithRemote(local, remote) {
|
|
10930
12649
|
if (remote.screensCount === local.screens) return null;
|
|
@@ -10937,7 +12656,7 @@ function compareWithRemote(local, remote) {
|
|
|
10937
12656
|
}
|
|
10938
12657
|
function readLocalDocumentSummary(outputDir) {
|
|
10939
12658
|
try {
|
|
10940
|
-
const raw = (0,
|
|
12659
|
+
const raw = (0, import_node_fs3.readFileSync)((0, import_node_path6.join)(outputDir, "mcp-document.json"), "utf-8");
|
|
10941
12660
|
const parsed = JSON.parse(raw);
|
|
10942
12661
|
if (typeof parsed?.metadata?.totalScreens !== "number") return null;
|
|
10943
12662
|
return { screens: parsed.metadata.totalScreens };
|
|
@@ -11022,14 +12741,18 @@ function assertDocumentHasScreens(document, options = {}) {
|
|
|
11022
12741
|
}
|
|
11023
12742
|
error("No screens found \u2014 this application map would give the agent nothing to navigate.");
|
|
11024
12743
|
info("");
|
|
11025
|
-
info("The analyzer
|
|
11026
|
-
info("
|
|
12744
|
+
info("The analyzer reads React Navigation \u2014 <X.Navigator> JSX and the static");
|
|
12745
|
+
info("`createXNavigator({ screens })` config \u2014 and Expo Router directory trees.");
|
|
12746
|
+
info("It found no screen in any of them.");
|
|
11027
12747
|
info("");
|
|
11028
12748
|
info("The two usual causes:");
|
|
11029
|
-
info(" \u2022 The
|
|
11030
|
-
info("
|
|
11031
|
-
info("
|
|
11032
|
-
info("
|
|
12749
|
+
info(" \u2022 The screens live outside the analyzed paths, or are named in a way");
|
|
12750
|
+
info(" the screen filter does not match \u2014 check `include`, `exclude` and");
|
|
12751
|
+
info(" `screenPatterns` in your Appilots config. The scan summary printed");
|
|
12752
|
+
info(" above says how many files were reached and how many were filtered.");
|
|
12753
|
+
info(" \u2022 The app uses a router the analyzer does not read yet, such as");
|
|
12754
|
+
info(" react-native-navigation (Wix). Declaring the screens works for any");
|
|
12755
|
+
info(" router \u2014 see the escape hatch below.");
|
|
11033
12756
|
info("");
|
|
11034
12757
|
info("The escape hatch, which works for any router: declare your screens in");
|
|
11035
12758
|
info("an `appilots.manifest.json` at the project root. Its screens are merged");
|
|
@@ -11076,7 +12799,7 @@ function syncCommand() {
|
|
|
11076
12799
|
const outputDir = config.outputDir || ".appilots";
|
|
11077
12800
|
let previousChecksum = "";
|
|
11078
12801
|
try {
|
|
11079
|
-
const checksumPath = (0,
|
|
12802
|
+
const checksumPath = (0, import_node_path7.join)(outputDir, ".appilots-checksum");
|
|
11080
12803
|
previousChecksum = (await (0, import_promises5.readFile)(checksumPath, "utf-8")).trim();
|
|
11081
12804
|
} catch {
|
|
11082
12805
|
}
|
|
@@ -11203,7 +12926,7 @@ function syncCommand() {
|
|
|
11203
12926
|
|
|
11204
12927
|
// src/cli/commands/watch.ts
|
|
11205
12928
|
var import_commander3 = require("commander");
|
|
11206
|
-
var
|
|
12929
|
+
var import_node_fs4 = require("fs");
|
|
11207
12930
|
function watchCommand() {
|
|
11208
12931
|
return new import_commander3.Command("watch").description("Watch project for changes and auto-sync MCP documents").option("--verbose", "Enable verbose logging", false).action(async (options) => {
|
|
11209
12932
|
try {
|
|
@@ -11250,7 +12973,7 @@ function watchCommand() {
|
|
|
11250
12973
|
dim("Press Ctrl+C to stop\n");
|
|
11251
12974
|
let debounceTimer = null;
|
|
11252
12975
|
const DEBOUNCE_MS = 500;
|
|
11253
|
-
const watcher = (0,
|
|
12976
|
+
const watcher = (0, import_node_fs4.watch)(process.cwd(), { recursive: true }, async (eventType, filename) => {
|
|
11254
12977
|
if (!filename) return;
|
|
11255
12978
|
if (filename.startsWith(".") || filename.includes("node_modules") || filename.includes(config.outputDir || ".appilots")) {
|
|
11256
12979
|
return;
|
|
@@ -11302,6 +13025,95 @@ function watchCommand() {
|
|
|
11302
13025
|
|
|
11303
13026
|
// src/cli/commands/generate.ts
|
|
11304
13027
|
var import_commander4 = require("commander");
|
|
13028
|
+
|
|
13029
|
+
// src/cli/utils/reach-report.ts
|
|
13030
|
+
function actionableScreens(document) {
|
|
13031
|
+
return (document.screens ?? []).filter(
|
|
13032
|
+
(screen) => (screen.actions?.length ?? 0) > 0 || (screen.forms?.length ?? 0) > 0 || (screen.targets?.length ?? 0) > 0
|
|
13033
|
+
).length;
|
|
13034
|
+
}
|
|
13035
|
+
function pct(part, whole) {
|
|
13036
|
+
if (whole <= 0) return "\u2014";
|
|
13037
|
+
return `${Math.round(100 * part / whole)}%`;
|
|
13038
|
+
}
|
|
13039
|
+
function reportReach(input) {
|
|
13040
|
+
const { document, navigation } = input;
|
|
13041
|
+
const screens = document.screens?.length ?? 0;
|
|
13042
|
+
const useful = actionableScreens(document);
|
|
13043
|
+
const filteredOut = document.metadata?.screensFilteredOut ?? 0;
|
|
13044
|
+
info("");
|
|
13045
|
+
info("Alcance da varredura");
|
|
13046
|
+
info(
|
|
13047
|
+
` telas ${screens} de ${document.metadata?.analyzedFiles ?? 0} arquivos analisados` + (filteredOut > 0 ? `, ${filteredOut} fora pelo filtro de tela` : "")
|
|
13048
|
+
);
|
|
13049
|
+
info(` com aford\xE2ncia ${useful} (${pct(useful, screens)}) \u2014 a\xE7\xE3o, formul\xE1rio ou target`);
|
|
13050
|
+
if (navigation) {
|
|
13051
|
+
info(
|
|
13052
|
+
` navega\xE7\xE3o ${navigation.filesWithEvidence} de ${navigation.filesScanned} arquivos com evid\xEAncia, ${navigation.navigatorsWithJsx} navegador(es) no documento`
|
|
13053
|
+
);
|
|
13054
|
+
info(
|
|
13055
|
+
` rotas ${navigation.routes}, ${navigation.routesLinkedToFile} (${pct(navigation.routesLinkedToFile, navigation.routes)}) ligadas a um arquivo de tela`
|
|
13056
|
+
);
|
|
13057
|
+
if (navigation.fileBasedRouter) {
|
|
13058
|
+
info(
|
|
13059
|
+
` roteador ${navigation.fileBasedRouter.kind} em "${navigation.fileBasedRouter.routeDir}"`
|
|
13060
|
+
);
|
|
13061
|
+
}
|
|
13062
|
+
}
|
|
13063
|
+
const notes = [];
|
|
13064
|
+
if (navigation && navigation.routes > 0 && useful > navigation.routes * 5) {
|
|
13065
|
+
notes.push([
|
|
13066
|
+
`${useful} tela(s) com aford\xE2ncia para apenas ${navigation.routes} rota(s).`,
|
|
13067
|
+
"A varredura achou o que tocar em muito mais lugares do que sabe alcan\xE7ar.",
|
|
13068
|
+
`S\xF3 ${navigation.filesWithEvidence} de ${navigation.filesScanned} arquivos varridos mencionam navega\xE7\xE3o \u2014`,
|
|
13069
|
+
"se o roteador deste app mora fora deles, `navigationInclude` no `.appilotsrc`",
|
|
13070
|
+
"estende a varredura sem mexer no resto."
|
|
13071
|
+
]);
|
|
13072
|
+
}
|
|
13073
|
+
if (navigation && navigation.routes === 0 && useful > 0) {
|
|
13074
|
+
notes.push([
|
|
13075
|
+
`${useful} tela(s) com aford\xE2ncia e nenhuma rota: o agente v\xEA o que tocar e n\xE3o sabe chegar l\xE1.`,
|
|
13076
|
+
"O extractor l\xEA React Navigation (JSX e configura\xE7\xE3o est\xE1tica) e expo-router.",
|
|
13077
|
+
"Se o app usa outro roteador, declare as telas em `appilots.manifest.json` \u2014",
|
|
13078
|
+
"elas s\xE3o mescladas por cima do que a an\xE1lise achou."
|
|
13079
|
+
]);
|
|
13080
|
+
} else if (navigation && navigation.routes > 0 && navigation.routesLinkedToFile < navigation.routes / 2) {
|
|
13081
|
+
notes.push([
|
|
13082
|
+
`${navigation.routes - navigation.routesLinkedToFile} de ${navigation.routes} rotas n\xE3o dizem qual arquivo montam.`,
|
|
13083
|
+
"A liga\xE7\xE3o vem de `component={X}`. Formas indiretas \u2014 `getComponent={() => X}`,",
|
|
13084
|
+
"componente constru\xEDdo em runtime \u2014 n\xE3o resolvem, e o agente fica com destinos vazios."
|
|
13085
|
+
]);
|
|
13086
|
+
}
|
|
13087
|
+
if (screens > 0 && useful === 0) {
|
|
13088
|
+
notes.push([
|
|
13089
|
+
`${screens} tela(s) encontradas e nenhuma com aford\xE2ncia.`,
|
|
13090
|
+
"O extractor mira `onPress`/`onChangeText` com um locator endere\xE7\xE1vel:",
|
|
13091
|
+
"`testID`, `appilotsId`, `accessibilityLabel` ou r\xF3tulo literal.",
|
|
13092
|
+
"Sem nenhum deles n\xE3o h\xE1 como o agente apontar para o elemento."
|
|
13093
|
+
]);
|
|
13094
|
+
}
|
|
13095
|
+
if (filteredOut > 0 && screens > 0 && filteredOut > screens * 9) {
|
|
13096
|
+
notes.push([
|
|
13097
|
+
`${filteredOut} arquivos ficaram fora do filtro de tela para ${screens} dentro.`,
|
|
13098
|
+
"Se as telas deste app moram noutro lugar, uma linha de `screenPatterns` no",
|
|
13099
|
+
"`.appilotsrc` aponta onde \u2014 \xE9 o degrau mais barato da escada."
|
|
13100
|
+
]);
|
|
13101
|
+
}
|
|
13102
|
+
if (navigation && navigation.filesScanned > 0 && navigation.filesWithEvidence === 0) {
|
|
13103
|
+
notes.push([
|
|
13104
|
+
`Nenhum dos ${navigation.filesScanned} arquivos varridos menciona navega\xE7\xE3o.`,
|
|
13105
|
+
"Confira `include`/`exclude` no `.appilotsrc`: o extractor pode n\xE3o estar",
|
|
13106
|
+
"alcan\xE7ando o diret\xF3rio onde o roteador \xE9 declarado."
|
|
13107
|
+
]);
|
|
13108
|
+
}
|
|
13109
|
+
for (const note of notes) {
|
|
13110
|
+
info("");
|
|
13111
|
+
warn(note[0]);
|
|
13112
|
+
for (const line of note.slice(1)) info(` ${line}`);
|
|
13113
|
+
}
|
|
13114
|
+
}
|
|
13115
|
+
|
|
13116
|
+
// src/cli/commands/generate.ts
|
|
11305
13117
|
function generateCommand() {
|
|
11306
13118
|
return new import_commander4.Command("generate").description("Generate MCP documents from project source (local only)").option("-o, --output <path>", "Output directory for MCP documents", ".appilots").option("-f, --format <format>", "Output format (only json supported)", "json").option(
|
|
11307
13119
|
"--strict-metadata",
|
|
@@ -11318,7 +13130,7 @@ function generateCommand() {
|
|
|
11318
13130
|
spinner.start();
|
|
11319
13131
|
let loaded = null;
|
|
11320
13132
|
try {
|
|
11321
|
-
loaded = loadConfig(warn);
|
|
13133
|
+
loaded = loadConfig(warn, { requireApiKey: false });
|
|
11322
13134
|
} catch {
|
|
11323
13135
|
loaded = null;
|
|
11324
13136
|
}
|
|
@@ -11326,11 +13138,21 @@ function generateCommand() {
|
|
|
11326
13138
|
rootDir: process.cwd(),
|
|
11327
13139
|
outputDir: options.output || ".appilots",
|
|
11328
13140
|
format: "json",
|
|
13141
|
+
strictScreens: loaded?.strictScreens,
|
|
13142
|
+
screenPatterns: loaded?.screenPatterns,
|
|
13143
|
+
include: loaded?.include,
|
|
13144
|
+
exclude: loaded?.exclude,
|
|
13145
|
+
navigationInclude: loaded?.navigationInclude,
|
|
13146
|
+
navigationExclude: loaded?.navigationExclude,
|
|
11329
13147
|
platform: loaded?.platform,
|
|
11330
13148
|
manifestPath: loaded?.manifestPath
|
|
11331
13149
|
});
|
|
11332
13150
|
const output = await generator.generate();
|
|
11333
13151
|
spinner.stop();
|
|
13152
|
+
reportReach({
|
|
13153
|
+
document: output.document,
|
|
13154
|
+
...output.diagnostics?.navigation ? { navigation: output.diagnostics.navigation } : {}
|
|
13155
|
+
});
|
|
11334
13156
|
assertDocumentHasScreens(output.document, {
|
|
11335
13157
|
rootDir: process.cwd(),
|
|
11336
13158
|
allowEmpty: Boolean(options.allowEmpty)
|
|
@@ -11350,20 +13172,19 @@ function generateCommand() {
|
|
|
11350
13172
|
|
|
11351
13173
|
// src/cli/commands/annotate.ts
|
|
11352
13174
|
var import_promises6 = require("fs/promises");
|
|
11353
|
-
var
|
|
13175
|
+
var import_node_path9 = __toESM(require("path"));
|
|
11354
13176
|
var import_commander5 = require("commander");
|
|
11355
|
-
var import_fast_glob6 = __toESM(require("fast-glob"));
|
|
11356
13177
|
|
|
11357
13178
|
// src/annotate/sites.ts
|
|
11358
|
-
var
|
|
11359
|
-
var
|
|
13179
|
+
var import_traverse13 = __toESM(require("@babel/traverse"));
|
|
13180
|
+
var t16 = __toESM(require("@babel/types"));
|
|
11360
13181
|
|
|
11361
13182
|
// src/annotate/forwarding.ts
|
|
11362
|
-
var
|
|
11363
|
-
var
|
|
11364
|
-
var
|
|
11365
|
-
var
|
|
11366
|
-
var
|
|
13183
|
+
var import_node_path8 = __toESM(require("path"));
|
|
13184
|
+
var import_node_fs5 = require("fs");
|
|
13185
|
+
var import_traverse12 = __toESM(require("@babel/traverse"));
|
|
13186
|
+
var t15 = __toESM(require("@babel/types"));
|
|
13187
|
+
var traverse12 = import_traverse12.default.default ?? import_traverse12.default;
|
|
11367
13188
|
var RN_TESTID_CARRIERS = /* @__PURE__ */ new Set([
|
|
11368
13189
|
"View",
|
|
11369
13190
|
"Text",
|
|
@@ -11390,14 +13211,14 @@ var RN_TESTID_CARRIERS = /* @__PURE__ */ new Set([
|
|
|
11390
13211
|
]);
|
|
11391
13212
|
function collectImports(ast) {
|
|
11392
13213
|
const out = /* @__PURE__ */ new Map();
|
|
11393
|
-
|
|
13214
|
+
traverse12(ast, {
|
|
11394
13215
|
ImportDeclaration(nodePath) {
|
|
11395
13216
|
const source = nodePath.node.source.value;
|
|
11396
13217
|
for (const spec of nodePath.node.specifiers) {
|
|
11397
|
-
if (
|
|
11398
|
-
const imported =
|
|
13218
|
+
if (t15.isImportSpecifier(spec)) {
|
|
13219
|
+
const imported = t15.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
|
|
11399
13220
|
out.set(spec.local.name, { source, local: spec.local.name, imported });
|
|
11400
|
-
} else if (
|
|
13221
|
+
} else if (t15.isImportDefaultSpecifier(spec)) {
|
|
11401
13222
|
out.set(spec.local.name, { source, local: spec.local.name, imported: "default" });
|
|
11402
13223
|
}
|
|
11403
13224
|
}
|
|
@@ -11405,17 +13226,17 @@ function collectImports(ast) {
|
|
|
11405
13226
|
});
|
|
11406
13227
|
return out;
|
|
11407
13228
|
}
|
|
11408
|
-
var
|
|
13229
|
+
var EXTENSIONS2 = [".tsx", ".ts", ".jsx", ".js"];
|
|
11409
13230
|
function resolveRelativeImport(fromFile, source) {
|
|
11410
13231
|
if (!source.startsWith(".")) return void 0;
|
|
11411
|
-
const base =
|
|
11412
|
-
for (const ext of
|
|
13232
|
+
const base = import_node_path8.default.resolve(import_node_path8.default.dirname(fromFile), source);
|
|
13233
|
+
for (const ext of EXTENSIONS2) {
|
|
11413
13234
|
const candidate = `${base}${ext}`;
|
|
11414
|
-
if ((0,
|
|
13235
|
+
if ((0, import_node_fs5.existsSync)(candidate)) return candidate;
|
|
11415
13236
|
}
|
|
11416
|
-
for (const ext of
|
|
11417
|
-
const candidate =
|
|
11418
|
-
if ((0,
|
|
13237
|
+
for (const ext of EXTENSIONS2) {
|
|
13238
|
+
const candidate = import_node_path8.default.join(base, `index${ext}`);
|
|
13239
|
+
if ((0, import_node_fs5.existsSync)(candidate)) return candidate;
|
|
11419
13240
|
}
|
|
11420
13241
|
return void 0;
|
|
11421
13242
|
}
|
|
@@ -11430,7 +13251,7 @@ function inspectModule(file2, exportName, depth = 0) {
|
|
|
11430
13251
|
if (depth > 2) return none;
|
|
11431
13252
|
let source;
|
|
11432
13253
|
try {
|
|
11433
|
-
source = (0,
|
|
13254
|
+
source = (0, import_node_fs5.readFileSync)(file2, "utf-8");
|
|
11434
13255
|
} catch {
|
|
11435
13256
|
return none;
|
|
11436
13257
|
}
|
|
@@ -11445,13 +13266,13 @@ function inspectModule(file2, exportName, depth = 0) {
|
|
|
11445
13266
|
let forwards = false;
|
|
11446
13267
|
let derives = false;
|
|
11447
13268
|
let barrelTarget;
|
|
11448
|
-
|
|
13269
|
+
traverse12(ast, {
|
|
11449
13270
|
// `export { Card } from './Card'` — follow one hop.
|
|
11450
13271
|
ExportNamedDeclaration(nodePath) {
|
|
11451
13272
|
const from = nodePath.node.source?.value;
|
|
11452
13273
|
if (!from) return;
|
|
11453
13274
|
const named = nodePath.node.specifiers.some(
|
|
11454
|
-
(spec) =>
|
|
13275
|
+
(spec) => t15.isExportSpecifier(spec) && (t15.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === exportName
|
|
11455
13276
|
);
|
|
11456
13277
|
if (named) barrelTarget = from;
|
|
11457
13278
|
},
|
|
@@ -11460,11 +13281,11 @@ function inspectModule(file2, exportName, depth = 0) {
|
|
|
11460
13281
|
},
|
|
11461
13282
|
// `interface CardProps { testID?: string }` / `type Props = { testID… }`
|
|
11462
13283
|
TSPropertySignature(nodePath) {
|
|
11463
|
-
if (
|
|
13284
|
+
if (t15.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
|
|
11464
13285
|
},
|
|
11465
13286
|
// `function Card({ testID }) {}` — the destructured parameter.
|
|
11466
13287
|
ObjectProperty(nodePath) {
|
|
11467
|
-
if (
|
|
13288
|
+
if (t15.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
|
|
11468
13289
|
},
|
|
11469
13290
|
// `<View {...props} />` — anything spread onto JSX carries it along.
|
|
11470
13291
|
JSXSpreadAttribute() {
|
|
@@ -11475,17 +13296,17 @@ function inspectModule(file2, exportName, depth = 0) {
|
|
|
11475
13296
|
// that is one hardcoded name shared by every instance, which is the
|
|
11476
13297
|
// duplicate-id problem rather than a solution to it.
|
|
11477
13298
|
JSXAttribute(nodePath) {
|
|
11478
|
-
const name =
|
|
13299
|
+
const name = t15.isJSXIdentifier(nodePath.node.name) ? nodePath.node.name.name : "";
|
|
11479
13300
|
if (name !== "testID" && name !== "accessibilityLabel") return;
|
|
11480
13301
|
const value = nodePath.node.value;
|
|
11481
|
-
if (!
|
|
11482
|
-
if (
|
|
13302
|
+
if (!t15.isJSXExpressionContainer(value)) return;
|
|
13303
|
+
if (t15.isStringLiteral(value.expression)) return;
|
|
11483
13304
|
derives = true;
|
|
11484
13305
|
},
|
|
11485
13306
|
// `useAppilotsTarget(`${groupId}-${option.value}`, …)`
|
|
11486
13307
|
CallExpression(nodePath) {
|
|
11487
13308
|
const callee = nodePath.node.callee;
|
|
11488
|
-
if (
|
|
13309
|
+
if (t15.isIdentifier(callee) && IDENTITY_HOOKS.has(callee.name)) derives = true;
|
|
11489
13310
|
}
|
|
11490
13311
|
});
|
|
11491
13312
|
if (forwards || derives) return { forwards, derives };
|
|
@@ -11519,7 +13340,7 @@ function resetForwardingCache() {
|
|
|
11519
13340
|
}
|
|
11520
13341
|
|
|
11521
13342
|
// src/annotate/sites.ts
|
|
11522
|
-
var
|
|
13343
|
+
var traverse13 = import_traverse13.default.default ?? import_traverse13.default;
|
|
11523
13344
|
var LABEL_PROPS = ["title", "label", "placeholder"];
|
|
11524
13345
|
var IDENTITY_PROPS = ["testID", "appilotsId", "accessibilityLabel"];
|
|
11525
13346
|
function kebab(text) {
|
|
@@ -11570,18 +13391,18 @@ function hasLabelText(element) {
|
|
|
11570
13391
|
function namedHandler(element, prop) {
|
|
11571
13392
|
if (!prop) return void 0;
|
|
11572
13393
|
const attr = findJsxAttribute(element, prop);
|
|
11573
|
-
if (!attr || !
|
|
11574
|
-
return
|
|
13394
|
+
if (!attr || !t16.isJSXExpressionContainer(attr.value)) return void 0;
|
|
13395
|
+
return t16.isIdentifier(attr.value.expression) ? attr.value.expression.name : void 0;
|
|
11575
13396
|
}
|
|
11576
13397
|
var ANNOTATABLE_ROLES = /* @__PURE__ */ new Set(["button", "input", "select", "toggle", "date", "list"]);
|
|
11577
13398
|
function isPerItemRender(nodePath) {
|
|
11578
13399
|
let current = nodePath.parentPath;
|
|
11579
13400
|
while (current) {
|
|
11580
13401
|
const node = current.node;
|
|
11581
|
-
if (
|
|
13402
|
+
if (t16.isCallExpression(node) && t16.isMemberExpression(node.callee) && t16.isIdentifier(node.callee.property) && (node.callee.property.name === "map" || node.callee.property.name === "flatMap")) {
|
|
11582
13403
|
return true;
|
|
11583
13404
|
}
|
|
11584
|
-
if (
|
|
13405
|
+
if (t16.isJSXAttribute(node) && t16.isJSXIdentifier(node.name) && /^render[A-Z]|^ListHeaderComponent$|^ListFooterComponent$/.test(node.name.name) && node.name.name !== "renderScrollComponent") {
|
|
11585
13406
|
return /^renderItem$|^renderSectionHeader$|^renderSectionFooter$/.test(node.name.name);
|
|
11586
13407
|
}
|
|
11587
13408
|
current = current.parentPath;
|
|
@@ -11600,7 +13421,7 @@ function findAnnotationSites(source, file2) {
|
|
|
11600
13421
|
const sites = [];
|
|
11601
13422
|
const perComponent = /* @__PURE__ */ new Map();
|
|
11602
13423
|
const usedIds = /* @__PURE__ */ new Set();
|
|
11603
|
-
|
|
13424
|
+
traverse13(ast, {
|
|
11604
13425
|
JSXOpeningElement(nodePath) {
|
|
11605
13426
|
for (const prop of IDENTITY_PROPS) {
|
|
11606
13427
|
const existing = getStringAttr(nodePath.node, prop);
|
|
@@ -11608,10 +13429,10 @@ function findAnnotationSites(source, file2) {
|
|
|
11608
13429
|
}
|
|
11609
13430
|
}
|
|
11610
13431
|
});
|
|
11611
|
-
|
|
13432
|
+
traverse13(ast, {
|
|
11612
13433
|
JSXOpeningElement(nodePath) {
|
|
11613
13434
|
const element = nodePath.node;
|
|
11614
|
-
if (!
|
|
13435
|
+
if (!t16.isJSXIdentifier(element.name)) return;
|
|
11615
13436
|
const component = element.name.name;
|
|
11616
13437
|
const role = classifyJsxComponent(component, element);
|
|
11617
13438
|
if (!ANNOTATABLE_ROLES.has(role)) return;
|
|
@@ -11693,23 +13514,23 @@ function applyToFile(source, sites) {
|
|
|
11693
13514
|
}
|
|
11694
13515
|
|
|
11695
13516
|
// src/annotate/forward.ts
|
|
11696
|
-
var
|
|
13517
|
+
var import_node_fs6 = require("fs");
|
|
11697
13518
|
var import_magic_string2 = __toESM(require("magic-string"));
|
|
11698
|
-
var
|
|
11699
|
-
var
|
|
11700
|
-
var
|
|
13519
|
+
var import_traverse14 = __toESM(require("@babel/traverse"));
|
|
13520
|
+
var t17 = __toESM(require("@babel/types"));
|
|
13521
|
+
var traverse14 = import_traverse14.default.default ?? import_traverse14.default;
|
|
11701
13522
|
function findComponent(ast, name) {
|
|
11702
13523
|
let found;
|
|
11703
|
-
|
|
13524
|
+
traverse14(ast, {
|
|
11704
13525
|
FunctionDeclaration(nodePath) {
|
|
11705
13526
|
if (nodePath.node.id?.name === name) {
|
|
11706
13527
|
found ??= { params: nodePath.node.params, body: nodePath.node.body };
|
|
11707
13528
|
}
|
|
11708
13529
|
},
|
|
11709
13530
|
VariableDeclarator(nodePath) {
|
|
11710
|
-
if (!
|
|
13531
|
+
if (!t17.isIdentifier(nodePath.node.id) || nodePath.node.id.name !== name) return;
|
|
11711
13532
|
const init = nodePath.node.init;
|
|
11712
|
-
if (
|
|
13533
|
+
if (t17.isArrowFunctionExpression(init) || t17.isFunctionExpression(init)) {
|
|
11713
13534
|
found ??= { params: init.params, body: init.body };
|
|
11714
13535
|
}
|
|
11715
13536
|
}
|
|
@@ -11721,26 +13542,26 @@ function findReturnedRoots(body) {
|
|
|
11721
13542
|
let unsupported = false;
|
|
11722
13543
|
const record = (node) => {
|
|
11723
13544
|
if (!node) return;
|
|
11724
|
-
if (
|
|
11725
|
-
if (
|
|
13545
|
+
if (t17.isParenthesizedExpression(node)) return record(node.expression);
|
|
13546
|
+
if (t17.isJSXElement(node)) {
|
|
11726
13547
|
roots.push(node.openingElement);
|
|
11727
13548
|
return;
|
|
11728
13549
|
}
|
|
11729
|
-
if (
|
|
13550
|
+
if (t17.isConditionalExpression(node)) {
|
|
11730
13551
|
record(node.consequent);
|
|
11731
13552
|
record(node.alternate);
|
|
11732
13553
|
return;
|
|
11733
13554
|
}
|
|
11734
|
-
if (
|
|
13555
|
+
if (t17.isJSXFragment(node)) {
|
|
11735
13556
|
unsupported = true;
|
|
11736
13557
|
return;
|
|
11737
13558
|
}
|
|
11738
|
-
if (
|
|
13559
|
+
if (t17.isNullLiteral(node)) return;
|
|
11739
13560
|
unsupported = true;
|
|
11740
13561
|
};
|
|
11741
|
-
if (
|
|
11742
|
-
|
|
11743
|
-
|
|
13562
|
+
if (t17.isBlockStatement(body)) {
|
|
13563
|
+
traverse14(
|
|
13564
|
+
t17.file(t17.program([t17.expressionStatement(t17.functionExpression(null, [], body))])),
|
|
11744
13565
|
{
|
|
11745
13566
|
ReturnStatement(nodePath) {
|
|
11746
13567
|
record(nodePath.node.argument);
|
|
@@ -11754,7 +13575,7 @@ function findReturnedRoots(body) {
|
|
|
11754
13575
|
return roots;
|
|
11755
13576
|
}
|
|
11756
13577
|
function isCarrierRoot(element, imports) {
|
|
11757
|
-
if (!
|
|
13578
|
+
if (!t17.isJSXIdentifier(element.name)) return false;
|
|
11758
13579
|
const name = element.name.name;
|
|
11759
13580
|
const origin = imports.get(name);
|
|
11760
13581
|
if (origin?.source !== "react-native") return false;
|
|
@@ -11762,7 +13583,7 @@ function isCarrierRoot(element, imports) {
|
|
|
11762
13583
|
}
|
|
11763
13584
|
function hasProp(element, name) {
|
|
11764
13585
|
return element.attributes.some(
|
|
11765
|
-
(attr) =>
|
|
13586
|
+
(attr) => t17.isJSXAttribute(attr) && t17.isJSXIdentifier(attr.name) && attr.name.name === name
|
|
11766
13587
|
);
|
|
11767
13588
|
}
|
|
11768
13589
|
function indentOf(source, offset) {
|
|
@@ -11788,12 +13609,12 @@ ${indent}${text}` };
|
|
|
11788
13609
|
}
|
|
11789
13610
|
function followBarrel(file2, ast, component) {
|
|
11790
13611
|
let target;
|
|
11791
|
-
|
|
13612
|
+
traverse14(ast, {
|
|
11792
13613
|
ExportNamedDeclaration(nodePath) {
|
|
11793
13614
|
const from = nodePath.node.source?.value;
|
|
11794
13615
|
if (!from) return;
|
|
11795
13616
|
const named = nodePath.node.specifiers.some(
|
|
11796
|
-
(spec) =>
|
|
13617
|
+
(spec) => t17.isExportSpecifier(spec) && (t17.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === component
|
|
11797
13618
|
);
|
|
11798
13619
|
if (named) target ??= from;
|
|
11799
13620
|
}
|
|
@@ -11804,7 +13625,7 @@ function planForward(file2, component, depth = 0) {
|
|
|
11804
13625
|
const base = { file: file2, component, edits: [], roots: 0 };
|
|
11805
13626
|
let source;
|
|
11806
13627
|
try {
|
|
11807
|
-
source = (0,
|
|
13628
|
+
source = (0, import_node_fs6.readFileSync)(file2, "utf-8");
|
|
11808
13629
|
} catch {
|
|
11809
13630
|
return { ...base, refusal: `cannot read ${file2}` };
|
|
11810
13631
|
}
|
|
@@ -11821,7 +13642,7 @@ function planForward(file2, component, depth = 0) {
|
|
|
11821
13642
|
return { ...base, refusal: `no function named ${component} in this file` };
|
|
11822
13643
|
}
|
|
11823
13644
|
const param = found.params[0];
|
|
11824
|
-
if (!
|
|
13645
|
+
if (!t17.isObjectPattern(param)) {
|
|
11825
13646
|
return {
|
|
11826
13647
|
...base,
|
|
11827
13648
|
refusal: `${component} does not destructure its props, so there is no safe place to add one without knowing what the parameter is called downstream`
|
|
@@ -11837,7 +13658,7 @@ function planForward(file2, component, depth = 0) {
|
|
|
11837
13658
|
const imports = collectImports(ast);
|
|
11838
13659
|
const nonCarrier = roots.find((root) => !isCarrierRoot(root, imports));
|
|
11839
13660
|
if (nonCarrier) {
|
|
11840
|
-
const name =
|
|
13661
|
+
const name = t17.isJSXIdentifier(nonCarrier.name) ? nonCarrier.name.name : "its root";
|
|
11841
13662
|
return {
|
|
11842
13663
|
...base,
|
|
11843
13664
|
refusal: `${component} renders <${name}> at its root, which is not a React Native element known to accept testID. Forwarding into it would move the problem down a level, not fix it`
|
|
@@ -11845,30 +13666,30 @@ function planForward(file2, component, depth = 0) {
|
|
|
11845
13666
|
}
|
|
11846
13667
|
const edits = [];
|
|
11847
13668
|
const alreadyDestructured = param.properties.some(
|
|
11848
|
-
(prop) =>
|
|
13669
|
+
(prop) => t17.isObjectProperty(prop) && t17.isIdentifier(prop.key) && prop.key.name === "testID"
|
|
11849
13670
|
);
|
|
11850
13671
|
if (!alreadyDestructured) {
|
|
11851
13672
|
const insert = insertAfterLastMember(source, param.properties, "testID", param.end ?? 0, ",");
|
|
11852
13673
|
edits.push({ ...insert, what: "parameter" });
|
|
11853
13674
|
}
|
|
11854
13675
|
const annotation = param.typeAnnotation;
|
|
11855
|
-
if (
|
|
13676
|
+
if (t17.isTSTypeAnnotation(annotation)) {
|
|
11856
13677
|
const declared = annotation.typeAnnotation;
|
|
11857
13678
|
let body;
|
|
11858
13679
|
let closing = 0;
|
|
11859
|
-
if (
|
|
13680
|
+
if (t17.isTSTypeLiteral(declared)) {
|
|
11860
13681
|
body = declared.members;
|
|
11861
13682
|
closing = declared.end ?? 0;
|
|
11862
|
-
} else if (
|
|
13683
|
+
} else if (t17.isTSTypeReference(declared) && t17.isIdentifier(declared.typeName)) {
|
|
11863
13684
|
const typeName = declared.typeName.name;
|
|
11864
|
-
|
|
13685
|
+
traverse14(ast, {
|
|
11865
13686
|
TSInterfaceDeclaration(nodePath) {
|
|
11866
13687
|
if (nodePath.node.id.name !== typeName) return;
|
|
11867
13688
|
body = nodePath.node.body.body;
|
|
11868
13689
|
closing = nodePath.node.body.end ?? 0;
|
|
11869
13690
|
},
|
|
11870
13691
|
TSTypeAliasDeclaration(nodePath) {
|
|
11871
|
-
if (nodePath.node.id.name !== typeName || !
|
|
13692
|
+
if (nodePath.node.id.name !== typeName || !t17.isTSTypeLiteral(nodePath.node.typeAnnotation))
|
|
11872
13693
|
return;
|
|
11873
13694
|
body = nodePath.node.typeAnnotation.members;
|
|
11874
13695
|
closing = nodePath.node.typeAnnotation.end ?? 0;
|
|
@@ -11883,7 +13704,7 @@ function planForward(file2, component, depth = 0) {
|
|
|
11883
13704
|
}
|
|
11884
13705
|
if (body) {
|
|
11885
13706
|
const declaresTestId = body.some(
|
|
11886
|
-
(member) =>
|
|
13707
|
+
(member) => t17.isTSPropertySignature(member) && t17.isIdentifier(member.key) && member.key.name === "testID"
|
|
11887
13708
|
);
|
|
11888
13709
|
if (!declaresTestId) {
|
|
11889
13710
|
const insert = insertAfterLastMember(source, body, "testID?: string;", closing, ";");
|
|
@@ -11940,7 +13761,7 @@ function annotateCommand() {
|
|
|
11940
13761
|
const spinner = createSpinner("Scanning for unaddressable controls...");
|
|
11941
13762
|
spinner.start();
|
|
11942
13763
|
resetForwardingCache();
|
|
11943
|
-
const files = await (
|
|
13764
|
+
const files = await globSorted(include, { cwd: rootDir, ignore: exclude, absolute: true });
|
|
11944
13765
|
const scan = async () => {
|
|
11945
13766
|
const out = [];
|
|
11946
13767
|
for (const file2 of files) {
|
|
@@ -11989,9 +13810,9 @@ function annotateCommand() {
|
|
|
11989
13810
|
written: options.write === true,
|
|
11990
13811
|
annotated: written.length,
|
|
11991
13812
|
blocked: blocked.length,
|
|
11992
|
-
files: touched.map((edit) =>
|
|
13813
|
+
files: touched.map((edit) => import_node_path9.default.relative(rootDir, edit.file)),
|
|
11993
13814
|
sites: [...written, ...blocked].map((site) => ({
|
|
11994
|
-
file:
|
|
13815
|
+
file: import_node_path9.default.relative(rootDir, site.file),
|
|
11995
13816
|
line: site.line,
|
|
11996
13817
|
component: site.component,
|
|
11997
13818
|
role: site.role,
|
|
@@ -12034,9 +13855,12 @@ async function planForwards(edits, rootDir) {
|
|
|
12034
13855
|
void rootDir;
|
|
12035
13856
|
return plans;
|
|
12036
13857
|
}
|
|
13858
|
+
function escapeRegExp(value) {
|
|
13859
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13860
|
+
}
|
|
12037
13861
|
function findImportSource(source, name) {
|
|
12038
13862
|
const pattern = new RegExp(
|
|
12039
|
-
String.raw`import\s*\{[^}]*\b${name}\b[^}]*\}\s*from\s*['"]([^'"]+)['"]`
|
|
13863
|
+
String.raw`import\s*\{[^}]*\b${escapeRegExp(name)}\b[^}]*\}\s*from\s*['"]([^'"]+)['"]`
|
|
12040
13864
|
);
|
|
12041
13865
|
return pattern.exec(source)?.[1];
|
|
12042
13866
|
}
|
|
@@ -12045,7 +13869,7 @@ function reportSites(rootDir, touched, blocked, didWrite, forwards) {
|
|
|
12045
13869
|
if (written.length > 0) {
|
|
12046
13870
|
heading(didWrite ? "Added" : "Would add");
|
|
12047
13871
|
for (const edit of touched) {
|
|
12048
|
-
info(
|
|
13872
|
+
info(import_node_path9.default.relative(rootDir, edit.file));
|
|
12049
13873
|
for (const site of edit.written) {
|
|
12050
13874
|
dim(` ${String(site.line).padStart(4)} <${site.component}> testID="${site.testId}"`);
|
|
12051
13875
|
}
|
|
@@ -12057,7 +13881,7 @@ function reportSites(rootDir, touched, blocked, didWrite, forwards) {
|
|
|
12057
13881
|
if (teachable.size > 0) {
|
|
12058
13882
|
heading(didWrite ? "Taught to carry testID" : "Would teach to carry testID");
|
|
12059
13883
|
for (const plan of teachable.values()) {
|
|
12060
|
-
info(`<${plan.component}> ${
|
|
13884
|
+
info(`<${plan.component}> ${import_node_path9.default.relative(rootDir, plan.file)}`);
|
|
12061
13885
|
for (const edit of plan.edits) {
|
|
12062
13886
|
dim(` ${edit.what}: ${edit.text.trim()}`);
|
|
12063
13887
|
}
|
|
@@ -12365,10 +14189,10 @@ function matchActions(expected, actual, reply = "") {
|
|
|
12365
14189
|
var import_fs9 = require("fs");
|
|
12366
14190
|
var import_path8 = require("path");
|
|
12367
14191
|
var EMPTY_BASELINE = { generatedAt: null, gitSha: null, perScenario: {} };
|
|
12368
|
-
function loadBaseline(
|
|
12369
|
-
if (!(0, import_fs9.existsSync)(
|
|
14192
|
+
function loadBaseline(path13) {
|
|
14193
|
+
if (!(0, import_fs9.existsSync)(path13)) return { ...EMPTY_BASELINE, perScenario: {} };
|
|
12370
14194
|
try {
|
|
12371
|
-
const raw = JSON.parse((0, import_fs9.readFileSync)(
|
|
14195
|
+
const raw = JSON.parse((0, import_fs9.readFileSync)(path13, "utf-8"));
|
|
12372
14196
|
return {
|
|
12373
14197
|
generatedAt: raw.generatedAt ?? null,
|
|
12374
14198
|
gitSha: raw.gitSha ?? null,
|
|
@@ -12378,9 +14202,9 @@ function loadBaseline(path11) {
|
|
|
12378
14202
|
return { ...EMPTY_BASELINE, perScenario: {} };
|
|
12379
14203
|
}
|
|
12380
14204
|
}
|
|
12381
|
-
function saveBaseline(
|
|
12382
|
-
(0, import_fs9.mkdirSync)((0, import_path8.dirname)(
|
|
12383
|
-
(0, import_fs9.writeFileSync)(
|
|
14205
|
+
function saveBaseline(path13, baseline) {
|
|
14206
|
+
(0, import_fs9.mkdirSync)((0, import_path8.dirname)(path13), { recursive: true });
|
|
14207
|
+
(0, import_fs9.writeFileSync)(path13, `${JSON.stringify(baseline, null, 2)}
|
|
12384
14208
|
`, "utf-8");
|
|
12385
14209
|
}
|
|
12386
14210
|
function buildBaseline(outcomes, meta) {
|
|
@@ -12408,11 +14232,11 @@ function diffAgainstBaseline(outcomes, baseline, maxTokenRegression) {
|
|
|
12408
14232
|
continue;
|
|
12409
14233
|
}
|
|
12410
14234
|
if (outcome.status === "passed" && prior.status === "passed" && prior.totalTokens > 0 && outcome.totalTokens > prior.totalTokens * (1 + maxTokenRegression)) {
|
|
12411
|
-
const
|
|
14235
|
+
const pct2 = Math.round((outcome.totalTokens - prior.totalTokens) / prior.totalTokens * 100);
|
|
12412
14236
|
regressions.push({
|
|
12413
14237
|
name: outcome.name,
|
|
12414
14238
|
kind: "token-regression",
|
|
12415
|
-
detail: `tokens ${prior.totalTokens} \u2192 ${outcome.totalTokens} (+${
|
|
14239
|
+
detail: `tokens ${prior.totalTokens} \u2192 ${outcome.totalTokens} (+${pct2}%, threshold +${Math.round(maxTokenRegression * 100)}%)`
|
|
12416
14240
|
});
|
|
12417
14241
|
}
|
|
12418
14242
|
}
|
|
@@ -12673,9 +14497,145 @@ Pass rate: ${passed}/${total} (${passRatePct}%) \u2014 threshold ${minPassRatePc
|
|
|
12673
14497
|
});
|
|
12674
14498
|
}
|
|
12675
14499
|
|
|
14500
|
+
// src/cli/commands/knowledge.ts
|
|
14501
|
+
var import_commander8 = require("commander");
|
|
14502
|
+
var import_promises7 = require("fs/promises");
|
|
14503
|
+
var import_node_path10 = require("path");
|
|
14504
|
+
var import_node_crypto3 = require("crypto");
|
|
14505
|
+
var import_fast_glob2 = __toESM(require("fast-glob"));
|
|
14506
|
+
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".txt", ".html", ".htm", ".pdf"]);
|
|
14507
|
+
var MIME_MAP = {
|
|
14508
|
+
".md": "text/markdown",
|
|
14509
|
+
".txt": "text/plain",
|
|
14510
|
+
".html": "text/html",
|
|
14511
|
+
".htm": "text/html",
|
|
14512
|
+
".pdf": "application/pdf"
|
|
14513
|
+
};
|
|
14514
|
+
function knowledgeCommand() {
|
|
14515
|
+
const cmd = new import_commander8.Command("knowledge").description("Manage the project knowledge base (RAG)");
|
|
14516
|
+
cmd.command("sync").description("Sync local documentation files to the knowledge base").option("--verbose", "Enable verbose logging", false).option("--api-key <key>", "Appilots API key (overrides .appilotsrc)").option("--server <url>", "Appilots server URL (overrides .appilotsrc)").option("--json", "Output a machine-readable JSON result (implies --quiet)", false).option("--quiet", "Suppress banner and informational output", false).action(async (options) => {
|
|
14517
|
+
if (options.json) setLogMode("json");
|
|
14518
|
+
else if (options.quiet) setLogMode("quiet");
|
|
14519
|
+
try {
|
|
14520
|
+
banner();
|
|
14521
|
+
const loaded = loadConfig(warn);
|
|
14522
|
+
const apiKey = options.apiKey || loaded?.apiKey;
|
|
14523
|
+
if (!apiKey) {
|
|
14524
|
+
error("No .appilotsrc configuration found");
|
|
14525
|
+
info("Run: appilots init --api-key <your-key>");
|
|
14526
|
+
jsonOutput({ success: false, error: "No configuration found (missing API key)" });
|
|
14527
|
+
process.exit(1);
|
|
14528
|
+
return;
|
|
14529
|
+
}
|
|
14530
|
+
const config = {
|
|
14531
|
+
serverUrl: DEFAULT_SERVER_URL,
|
|
14532
|
+
...loaded,
|
|
14533
|
+
apiKey,
|
|
14534
|
+
...options.server ? { serverUrl: options.server } : {}
|
|
14535
|
+
};
|
|
14536
|
+
const sources = config.knowledge?.sources;
|
|
14537
|
+
if (!sources || sources.length === 0) {
|
|
14538
|
+
error("No knowledge sources configured");
|
|
14539
|
+
info("Add knowledge.sources to .appilotsrc:");
|
|
14540
|
+
info(' { "knowledge": { "sources": ["docs/**/*.md"] } }');
|
|
14541
|
+
jsonOutput({ success: false, error: "No knowledge sources configured" });
|
|
14542
|
+
process.exit(1);
|
|
14543
|
+
return;
|
|
14544
|
+
}
|
|
14545
|
+
const spinner = createSpinner("Scanning for knowledge documents...");
|
|
14546
|
+
spinner.start();
|
|
14547
|
+
const rootDir = process.cwd();
|
|
14548
|
+
const excludePatterns = config.knowledge?.exclude ?? [];
|
|
14549
|
+
const allFiles = [];
|
|
14550
|
+
for (const pattern of sources) {
|
|
14551
|
+
const matches = await (0, import_fast_glob2.default)(pattern, {
|
|
14552
|
+
cwd: rootDir,
|
|
14553
|
+
ignore: ["node_modules/**", ...excludePatterns],
|
|
14554
|
+
onlyFiles: true
|
|
14555
|
+
});
|
|
14556
|
+
for (const match of matches) {
|
|
14557
|
+
const ext = (0, import_node_path10.extname)(match).toLowerCase();
|
|
14558
|
+
if (SUPPORTED_EXTENSIONS.has(ext) && !allFiles.includes(match)) {
|
|
14559
|
+
allFiles.push(match);
|
|
14560
|
+
}
|
|
14561
|
+
}
|
|
14562
|
+
}
|
|
14563
|
+
spinner.stop();
|
|
14564
|
+
if (allFiles.length === 0) {
|
|
14565
|
+
warn("No supported files found matching knowledge.sources patterns");
|
|
14566
|
+
info(`Supported: ${[...SUPPORTED_EXTENSIONS].join(", ")}`);
|
|
14567
|
+
jsonOutput({ success: true, uploaded: 0, skipped: 0, errors: 0, files: 0 });
|
|
14568
|
+
return;
|
|
14569
|
+
}
|
|
14570
|
+
info(`Found ${allFiles.length} document${allFiles.length === 1 ? "" : "s"}`);
|
|
14571
|
+
const documents = [];
|
|
14572
|
+
for (const filePath of allFiles) {
|
|
14573
|
+
const absolutePath = (0, import_node_path10.resolve)(rootDir, filePath);
|
|
14574
|
+
const buffer = await (0, import_promises7.readFile)(absolutePath);
|
|
14575
|
+
const checksum = (0, import_node_crypto3.createHash)("sha256").update(buffer).digest("hex");
|
|
14576
|
+
const ext = (0, import_node_path10.extname)(filePath).toLowerCase();
|
|
14577
|
+
const mimeType = MIME_MAP[ext] ?? "text/plain";
|
|
14578
|
+
documents.push({
|
|
14579
|
+
filename: (0, import_node_path10.relative)(rootDir, absolutePath),
|
|
14580
|
+
content: buffer.toString("base64"),
|
|
14581
|
+
mimeType,
|
|
14582
|
+
checksum
|
|
14583
|
+
});
|
|
14584
|
+
}
|
|
14585
|
+
const uploadSpinner = createSpinner(
|
|
14586
|
+
`Uploading ${documents.length} document${documents.length === 1 ? "" : "s"}...`
|
|
14587
|
+
);
|
|
14588
|
+
uploadSpinner.start();
|
|
14589
|
+
const apiClient = new AppilotsAPIClient({
|
|
14590
|
+
serverUrl: config.serverUrl,
|
|
14591
|
+
apiKey: config.apiKey,
|
|
14592
|
+
timeoutMs: 12e4
|
|
14593
|
+
// Knowledge sync can take longer (embedding)
|
|
14594
|
+
});
|
|
14595
|
+
const result = await apiClient.knowledgeSync(documents);
|
|
14596
|
+
uploadSpinner.stop();
|
|
14597
|
+
if (!result.success) {
|
|
14598
|
+
error(`Knowledge sync failed: ${result.error}`);
|
|
14599
|
+
jsonOutput({ success: false, error: result.error });
|
|
14600
|
+
process.exit(1);
|
|
14601
|
+
return;
|
|
14602
|
+
}
|
|
14603
|
+
success(
|
|
14604
|
+
`Knowledge sync complete: ${result.uploaded} uploaded, ${result.skipped} skipped, ${result.errors} errors`
|
|
14605
|
+
);
|
|
14606
|
+
if (options.verbose && result.details) {
|
|
14607
|
+
for (const detail of result.details) {
|
|
14608
|
+
if (detail.status === "uploaded") {
|
|
14609
|
+
info(` + ${detail.filename}`);
|
|
14610
|
+
} else if (detail.status === "skipped") {
|
|
14611
|
+
info(` = ${detail.filename} (unchanged)`);
|
|
14612
|
+
} else {
|
|
14613
|
+
warn(` ! ${detail.filename}: ${detail.error}`);
|
|
14614
|
+
}
|
|
14615
|
+
}
|
|
14616
|
+
}
|
|
14617
|
+
jsonOutput({
|
|
14618
|
+
success: true,
|
|
14619
|
+
uploaded: result.uploaded,
|
|
14620
|
+
skipped: result.skipped,
|
|
14621
|
+
errors: result.errors,
|
|
14622
|
+
details: result.details
|
|
14623
|
+
});
|
|
14624
|
+
} catch (err) {
|
|
14625
|
+
error(`Knowledge sync failed: ${stringifyUnknown(err)}`);
|
|
14626
|
+
jsonOutput({ success: false, error: stringifyUnknown(err) });
|
|
14627
|
+
if (options.verbose) {
|
|
14628
|
+
console.error(err);
|
|
14629
|
+
}
|
|
14630
|
+
process.exit(1);
|
|
14631
|
+
}
|
|
14632
|
+
});
|
|
14633
|
+
return cmd;
|
|
14634
|
+
}
|
|
14635
|
+
|
|
12676
14636
|
// src/cli/index.ts
|
|
12677
14637
|
async function main() {
|
|
12678
|
-
const program2 = new
|
|
14638
|
+
const program2 = new import_commander9.Command();
|
|
12679
14639
|
program2.name("appilots").description(
|
|
12680
14640
|
"Analyze your React Native app and generate MCP documents for AI-powered navigation.\n\nQuick start:\n $ appilots init --api-key ak_xxx\n $ appilots sync\n $ appilots watch"
|
|
12681
14641
|
).version(CLI_VERSION).option("--verbose", "Enable verbose logging");
|
|
@@ -12686,6 +14646,7 @@ async function main() {
|
|
|
12686
14646
|
program2.addCommand(annotateCommand());
|
|
12687
14647
|
program2.addCommand(statusCommand());
|
|
12688
14648
|
program2.addCommand(evalCommand());
|
|
14649
|
+
program2.addCommand(knowledgeCommand());
|
|
12689
14650
|
const args = process.argv.slice(2);
|
|
12690
14651
|
const firstArg = args[0];
|
|
12691
14652
|
const hasCommand = firstArg !== void 0 && args.length > 0 && !firstArg.startsWith("-");
|