@appilots/cli 0.10.0 → 0.11.3
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/dist/cli/index.js +1791 -422
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +279 -5
- package/dist/index.d.ts +279 -5
- package/dist/index.js +1455 -197
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1449 -191
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
31
31
|
var import_commander8 = require("commander");
|
|
32
32
|
|
|
33
33
|
// src/version.ts
|
|
34
|
-
var CLI_VERSION = "0.
|
|
34
|
+
var CLI_VERSION = "0.11.3";
|
|
35
35
|
|
|
36
36
|
// src/config/index.ts
|
|
37
37
|
var import_fs = require("fs");
|
|
@@ -107,12 +107,12 @@ function getEnvOverrides(env = process.env) {
|
|
|
107
107
|
return trimmed ? trimmed : void 0;
|
|
108
108
|
};
|
|
109
109
|
return {
|
|
110
|
-
apiKey: clean(env.APPILOTS_API_KEY),
|
|
110
|
+
apiKey: clean(env.APPILOTS_PUBLISH_KEY) ?? clean(env.APPILOTS_API_KEY),
|
|
111
111
|
projectId: clean(env.APPILOTS_PROJECT_ID),
|
|
112
112
|
serverUrl: clean(env.APPILOTS_SERVER_URL)
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
|
-
function loadConfig(onWarn) {
|
|
115
|
+
function loadConfig(onWarn, options = {}) {
|
|
116
116
|
const configPath = getConfigPath();
|
|
117
117
|
const env = getEnvOverrides();
|
|
118
118
|
let fileConfig = {};
|
|
@@ -127,6 +127,18 @@ function loadConfig(onWarn) {
|
|
|
127
127
|
} else if (!env.apiKey) {
|
|
128
128
|
return null;
|
|
129
129
|
}
|
|
130
|
+
const legacyWarnings = [];
|
|
131
|
+
if (fileConfig.serverUrl === void 0) {
|
|
132
|
+
for (const [alias, canonical] of Object.entries(KEY_ALIASES)) {
|
|
133
|
+
if (canonical !== "serverUrl" || fileConfig[alias] === void 0) continue;
|
|
134
|
+
fileConfig.serverUrl = fileConfig[alias];
|
|
135
|
+
delete fileConfig[alias];
|
|
136
|
+
legacyWarnings.push(
|
|
137
|
+
`legacy key "${alias}" \u2014 using it as "serverUrl"; rename it in .appilotsrc`
|
|
138
|
+
);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
130
142
|
const merged = {
|
|
131
143
|
serverUrl: DEFAULT_SERVER_URL,
|
|
132
144
|
outputDir: ".appilots",
|
|
@@ -138,6 +150,24 @@ function loadConfig(onWarn) {
|
|
|
138
150
|
...env.serverUrl ? { serverUrl: env.serverUrl } : {}
|
|
139
151
|
};
|
|
140
152
|
const validation = validateConfig(merged);
|
|
153
|
+
validation.warnings.unshift(...legacyWarnings);
|
|
154
|
+
if (options.requireApiKey === false) {
|
|
155
|
+
validation.errors = validation.errors.filter((e) => !e.startsWith("apiKey"));
|
|
156
|
+
validation.valid = validation.errors.length === 0;
|
|
157
|
+
}
|
|
158
|
+
if (fileConfig.serverUrl === void 0 && env.serverUrl === void 0) {
|
|
159
|
+
for (const key of Object.keys(fileConfig)) {
|
|
160
|
+
if (KNOWN_CONFIG_KEYS.includes(key)) continue;
|
|
161
|
+
if (suggestConfigKey(key) !== "serverUrl") continue;
|
|
162
|
+
validation.warnings = validation.warnings.filter(
|
|
163
|
+
(warning) => !warning.startsWith(`unknown key "${key}"`)
|
|
164
|
+
);
|
|
165
|
+
validation.errors.push(
|
|
166
|
+
`unknown key "${key}" looks like "serverUrl" \u2014 not falling back to ${DEFAULT_SERVER_URL}; rename the key to "serverUrl"`
|
|
167
|
+
);
|
|
168
|
+
validation.valid = false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
141
171
|
if (onWarn) {
|
|
142
172
|
const source = configPath ?? "environment variables";
|
|
143
173
|
for (const warning of validation.warnings) {
|
|
@@ -737,12 +767,18 @@ function initCommand() {
|
|
|
737
767
|
// src/cli/commands/sync.ts
|
|
738
768
|
var import_commander2 = require("commander");
|
|
739
769
|
var import_promises5 = require("fs/promises");
|
|
740
|
-
var
|
|
770
|
+
var import_node_path7 = require("path");
|
|
741
771
|
|
|
742
772
|
// src/generators/MCPGenerator.ts
|
|
743
773
|
var import_promises4 = require("fs/promises");
|
|
744
774
|
var import_node_crypto = require("crypto");
|
|
745
|
-
var
|
|
775
|
+
var import_node_path5 = __toESM(require("path"));
|
|
776
|
+
|
|
777
|
+
// src/generators/checksum.ts
|
|
778
|
+
function serializeForChecksum(document) {
|
|
779
|
+
const { generatedAt: _generatedAt, ...content } = document;
|
|
780
|
+
return JSON.stringify(content, null, 2);
|
|
781
|
+
}
|
|
746
782
|
|
|
747
783
|
// src/pipeline/enrichment.ts
|
|
748
784
|
function enrichScreenForAgent(screen) {
|
|
@@ -820,7 +856,10 @@ function mergeTargets(targets) {
|
|
|
820
856
|
for (const target of targets) {
|
|
821
857
|
if (!target.id) continue;
|
|
822
858
|
const existing = byId.get(target.id);
|
|
823
|
-
byId.set(
|
|
859
|
+
byId.set(
|
|
860
|
+
target.id,
|
|
861
|
+
existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target
|
|
862
|
+
);
|
|
824
863
|
}
|
|
825
864
|
return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
826
865
|
}
|
|
@@ -865,7 +904,10 @@ function synthesizeFlows(screen, targets) {
|
|
|
865
904
|
intent: "destructive_action",
|
|
866
905
|
steps: [
|
|
867
906
|
{ type: "press", target: action.id, label: action.label },
|
|
868
|
-
{
|
|
907
|
+
{
|
|
908
|
+
type: "confirm",
|
|
909
|
+
description: "Wait for native or custom confirmation before continuing"
|
|
910
|
+
},
|
|
869
911
|
{ type: "wait", description: describeWait(action) }
|
|
870
912
|
],
|
|
871
913
|
waitPolicy: waitPolicyForAction(action),
|
|
@@ -880,10 +922,19 @@ function synthesizeFlows(screen, targets) {
|
|
|
880
922
|
title: `Act on an item in ${collection.id}`,
|
|
881
923
|
intent: "list_action",
|
|
882
924
|
steps: [
|
|
883
|
-
{
|
|
884
|
-
|
|
925
|
+
{
|
|
926
|
+
type: "choose-list-item",
|
|
927
|
+
target: collection.id,
|
|
928
|
+
description: "Resolve the user reference to a visible or searchable row"
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
type: "press",
|
|
932
|
+
description: collection.rowAction?.description ?? "Open the row action"
|
|
933
|
+
}
|
|
885
934
|
],
|
|
886
|
-
waitPolicy: {
|
|
935
|
+
waitPolicy: {
|
|
936
|
+
expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback"
|
|
937
|
+
}
|
|
887
938
|
});
|
|
888
939
|
}
|
|
889
940
|
}
|
|
@@ -891,25 +942,32 @@ function synthesizeFlows(screen, targets) {
|
|
|
891
942
|
}
|
|
892
943
|
function waitPolicyForAction(action) {
|
|
893
944
|
const expectedOutcome = action.successSignal?.type === "goBack" ? "goBack" : action.appilotsInferred?.expectedOutcome ?? (action.targetScreen ? "navigation" : void 0);
|
|
894
|
-
const signals = [action.successSignal, action.failureSignal].filter(
|
|
945
|
+
const signals = [action.successSignal, action.failureSignal].filter(
|
|
946
|
+
Boolean
|
|
947
|
+
);
|
|
948
|
+
const maxMs = action.asyncBudgetMs ?? (action.appilotsInferred?.isAsyncTrigger ? 1e4 : void 0);
|
|
895
949
|
return {
|
|
896
950
|
...expectedOutcome ? { expectedOutcome } : {},
|
|
897
951
|
...signals && signals.length > 0 ? { signals } : {},
|
|
898
|
-
...
|
|
952
|
+
...maxMs !== void 0 ? { maxMs } : {}
|
|
899
953
|
};
|
|
900
954
|
}
|
|
901
955
|
function describeWait(action) {
|
|
902
956
|
if (action.successSignal?.description) return action.successSignal.description;
|
|
903
|
-
if (action.successSignal?.type === "goBack")
|
|
957
|
+
if (action.successSignal?.type === "goBack")
|
|
958
|
+
return "Wait for the app to return to the previous screen";
|
|
904
959
|
if (action.targetScreen) return `Wait for navigation to ${action.targetScreen}`;
|
|
905
|
-
if (action.appilotsInferred?.expectedOutcome)
|
|
960
|
+
if (action.appilotsInferred?.expectedOutcome)
|
|
961
|
+
return `Wait for ${action.appilotsInferred.expectedOutcome}`;
|
|
906
962
|
return "Wait for the UI to settle";
|
|
907
963
|
}
|
|
908
964
|
function synthesizeAgentHints(screen, targets, flows) {
|
|
909
965
|
const preferredTargets = targets.filter((target) => ["submit", "button", "list"].includes(target.role)).slice(0, 8).map((target) => target.id);
|
|
910
966
|
const commonTasks = flows.slice(0, 6).map((flow) => flow.title);
|
|
911
967
|
const safetyNotes = screen.actions.filter((action) => action.destructive || action.requiresConfirmation).map((action) => `${action.id} requires confirmation`);
|
|
912
|
-
const firstAsyncAction = screen.actions.find(
|
|
968
|
+
const firstAsyncAction = screen.actions.find(
|
|
969
|
+
(action) => action.appilotsInferred?.isAsyncTrigger || action.asyncBudgetMs !== void 0
|
|
970
|
+
);
|
|
913
971
|
const hints = {
|
|
914
972
|
primaryGoal: synthesizePrimaryGoal(screen),
|
|
915
973
|
commonTasks,
|
|
@@ -936,7 +994,9 @@ function synthesizePrimaryGoal(screen) {
|
|
|
936
994
|
}
|
|
937
995
|
const fieldCount = uniqueFields.size;
|
|
938
996
|
if (fieldCount > 0) {
|
|
939
|
-
const requiredCount = Array.from(uniqueFields.values()).filter(
|
|
997
|
+
const requiredCount = Array.from(uniqueFields.values()).filter(
|
|
998
|
+
(field) => field.required
|
|
999
|
+
).length;
|
|
940
1000
|
const fieldLabel = fieldCount === 1 ? "field" : "fields";
|
|
941
1001
|
goals.push(
|
|
942
1002
|
requiredCount > 0 ? `Complete a form with ${fieldCount} ${fieldLabel} (${requiredCount} required)` : `Complete a form with ${fieldCount} ${fieldLabel}`
|
|
@@ -950,7 +1010,9 @@ function synthesizePrimaryGoal(screen) {
|
|
|
950
1010
|
if (submitCount > 0) {
|
|
951
1011
|
goals.push(`Submit ${submitCount === 1 ? "the primary form" : `${submitCount} forms/actions`}`);
|
|
952
1012
|
}
|
|
953
|
-
const asyncCount = screen.actions.filter(
|
|
1013
|
+
const asyncCount = screen.actions.filter(
|
|
1014
|
+
(action) => action.appilotsInferred?.isAsyncTrigger
|
|
1015
|
+
).length;
|
|
954
1016
|
if (asyncCount > 0) {
|
|
955
1017
|
goals.push(`Wait for ${asyncCount === 1 ? "async feedback" : "async action feedback"}`);
|
|
956
1018
|
}
|
|
@@ -960,15 +1022,23 @@ function synthesizePrimaryGoal(screen) {
|
|
|
960
1022
|
}
|
|
961
1023
|
|
|
962
1024
|
// src/analyzers/ReactNativePlatformAnalyzer.ts
|
|
963
|
-
var
|
|
964
|
-
|
|
1025
|
+
var import_node_path3 = __toESM(require("path"));
|
|
1026
|
+
|
|
1027
|
+
// src/utils/glob.ts
|
|
1028
|
+
var import_fast_glob = __toESM(require("fast-glob"));
|
|
1029
|
+
function byCodeUnit(a, b) {
|
|
1030
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
1031
|
+
}
|
|
1032
|
+
async function globSorted(patterns, options) {
|
|
1033
|
+
const files = await (0, import_fast_glob.default)(patterns, options);
|
|
1034
|
+
return files.sort(byCodeUnit);
|
|
1035
|
+
}
|
|
965
1036
|
|
|
966
1037
|
// src/analyzers/ScreenAnalyzer.ts
|
|
967
1038
|
var import_promises = __toESM(require("fs/promises"));
|
|
968
1039
|
var import_path3 = __toESM(require("path"));
|
|
969
1040
|
var import_traverse4 = __toESM(require("@babel/traverse"));
|
|
970
1041
|
var BabelTypes = __toESM(require("@babel/types"));
|
|
971
|
-
var import_fast_glob = __toESM(require("fast-glob"));
|
|
972
1042
|
|
|
973
1043
|
// src/ast/parse.ts
|
|
974
1044
|
var import_parser = require("@babel/parser");
|
|
@@ -1371,14 +1441,25 @@ var ScreenAnalyzer = class {
|
|
|
1371
1441
|
strictScreens;
|
|
1372
1442
|
/** Glob patterns that identify screen files in strict mode */
|
|
1373
1443
|
screenPatterns;
|
|
1444
|
+
/**
|
|
1445
|
+
* Arquivos que uma ROTA monta (`component={…}` resolvido pelo
|
|
1446
|
+
* `NavigationAnalyzer`). Passam pelo filtro estrito sem depender de
|
|
1447
|
+
* convenção de nome, porque um componente que uma rota monta É uma tela por
|
|
1448
|
+
* definição — não é heurística, é o que o app declarou.
|
|
1449
|
+
*
|
|
1450
|
+
* É isto que resolve o caso `rocketchat`, cujas telas se chamam `*View.tsx`
|
|
1451
|
+
* em `app/views/`, e o `coopcycle`, que não tem diretório `screens/` nenhum.
|
|
1452
|
+
*/
|
|
1453
|
+
routeTargetFiles;
|
|
1374
1454
|
/** §D: Count of screens filtered out in strict mode (available after analyze()) */
|
|
1375
1455
|
screensFilteredOut = 0;
|
|
1376
1456
|
constructor(config, options) {
|
|
1377
1457
|
this.config = config;
|
|
1458
|
+
this.routeTargetFiles = options?.routeTargetFiles ?? /* @__PURE__ */ new Set();
|
|
1378
1459
|
this.strictScreens = options?.strictScreens ?? false;
|
|
1379
1460
|
this.screenPatterns = options?.screenPatterns ?? [
|
|
1380
|
-
"**/*Screen.{ts,tsx}",
|
|
1381
|
-
"**/screens/**/*.{ts,tsx}"
|
|
1461
|
+
"**/*Screen.{ts,tsx,js,jsx}",
|
|
1462
|
+
"**/screens/**/*.{ts,tsx,js,jsx}"
|
|
1382
1463
|
];
|
|
1383
1464
|
}
|
|
1384
1465
|
/** Analyze all screens in the project */
|
|
@@ -1390,7 +1471,7 @@ var ScreenAnalyzer = class {
|
|
|
1390
1471
|
console.log(`[ScreenAnalyzer] Strict mode ON \u2014 screen patterns:`, this.screenPatterns);
|
|
1391
1472
|
}
|
|
1392
1473
|
}
|
|
1393
|
-
const files = await (
|
|
1474
|
+
const files = await globSorted(include, {
|
|
1394
1475
|
cwd: this.config.rootDir,
|
|
1395
1476
|
ignore: exclude
|
|
1396
1477
|
});
|
|
@@ -1399,7 +1480,7 @@ var ScreenAnalyzer = class {
|
|
|
1399
1480
|
}
|
|
1400
1481
|
let screenPatternFiles = null;
|
|
1401
1482
|
if (this.strictScreens) {
|
|
1402
|
-
const matched = await (
|
|
1483
|
+
const matched = await globSorted(this.screenPatterns, {
|
|
1403
1484
|
cwd: this.config.rootDir,
|
|
1404
1485
|
ignore: exclude
|
|
1405
1486
|
});
|
|
@@ -1415,7 +1496,8 @@ var ScreenAnalyzer = class {
|
|
|
1415
1496
|
if (this.strictScreens) {
|
|
1416
1497
|
const hasRegisterScreen = descriptor.__hasRegisterScreen === true;
|
|
1417
1498
|
const matchesPattern = screenPatternFiles?.has(filePath) ?? false;
|
|
1418
|
-
|
|
1499
|
+
const isRouteTarget = this.routeTargetFiles.has(filePath);
|
|
1500
|
+
if (!hasRegisterScreen && !matchesPattern && !isRouteTarget) {
|
|
1419
1501
|
this.screensFilteredOut++;
|
|
1420
1502
|
if (this.verbose) {
|
|
1421
1503
|
console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file2}`);
|
|
@@ -1623,6 +1705,8 @@ var ScreenAnalyzer = class {
|
|
|
1623
1705
|
action.riskLevel = value.value;
|
|
1624
1706
|
} else if (key === "nativeConfirmationExpected" && BabelTypes.isBooleanLiteral(value)) {
|
|
1625
1707
|
action.nativeConfirmationExpected = value.value;
|
|
1708
|
+
} else if (key === "asyncBudgetMs" && BabelTypes.isNumericLiteral(value) && Number.isInteger(value.value) && value.value > 0) {
|
|
1709
|
+
action.asyncBudgetMs = value.value;
|
|
1626
1710
|
} else if (key === "appilotsInferred" && BabelTypes.isObjectExpression(value)) {
|
|
1627
1711
|
action.appilotsInferred = this.parseAppilotsInferredObject(value);
|
|
1628
1712
|
}
|
|
@@ -2610,43 +2694,880 @@ var ScreenAnalyzer = class {
|
|
|
2610
2694
|
// src/analyzers/NavigationAnalyzer.ts
|
|
2611
2695
|
var import_fs4 = require("fs");
|
|
2612
2696
|
var import_path4 = __toESM(require("path"));
|
|
2613
|
-
var
|
|
2614
|
-
var parser = __toESM(require("@babel/parser"));
|
|
2697
|
+
var parser2 = __toESM(require("@babel/parser"));
|
|
2615
2698
|
var import_traverse5 = __toESM(require("@babel/traverse"));
|
|
2699
|
+
var t6 = __toESM(require("@babel/types"));
|
|
2700
|
+
|
|
2701
|
+
// src/ast/navigation/module-graph.ts
|
|
2702
|
+
var import_node_fs = require("fs");
|
|
2703
|
+
var import_node_path = __toESM(require("path"));
|
|
2704
|
+
var parser = __toESM(require("@babel/parser"));
|
|
2616
2705
|
var t5 = __toESM(require("@babel/types"));
|
|
2706
|
+
var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
|
|
2707
|
+
var MAX_HOPS = 8;
|
|
2708
|
+
var ModuleGraph = class {
|
|
2709
|
+
asts = /* @__PURE__ */ new Map();
|
|
2710
|
+
resolved = /* @__PURE__ */ new Map();
|
|
2711
|
+
/** `@src/*` → `<root>/src/*`, lido do tsconfig do app. */
|
|
2712
|
+
aliases;
|
|
2713
|
+
/** `uniswap` → `<repo>/packages/uniswap`, lido do workspace do monorepo. */
|
|
2714
|
+
workspacePackages;
|
|
2715
|
+
constructor(rootDir) {
|
|
2716
|
+
this.aliases = rootDir ? readTsconfigAliases(rootDir) : [];
|
|
2717
|
+
this.workspacePackages = rootDir ? readWorkspacePackages(rootDir) : [];
|
|
2718
|
+
}
|
|
2719
|
+
/** AST de um arquivo, memoizada. `null` quando não parseia. */
|
|
2720
|
+
parse(file2) {
|
|
2721
|
+
const cached = this.asts.get(file2);
|
|
2722
|
+
if (cached !== void 0) return cached;
|
|
2723
|
+
let ast = null;
|
|
2724
|
+
try {
|
|
2725
|
+
ast = parser.parse((0, import_node_fs.readFileSync)(file2, "utf-8"), {
|
|
2726
|
+
sourceType: "module",
|
|
2727
|
+
plugins: ["jsx", "typescript"]
|
|
2728
|
+
});
|
|
2729
|
+
} catch {
|
|
2730
|
+
ast = null;
|
|
2731
|
+
}
|
|
2732
|
+
this.asts.set(file2, ast);
|
|
2733
|
+
return ast;
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* `./account/Home` a partir de `src/navigation/index.tsx` → caminho absoluto.
|
|
2737
|
+
* Só resolve caminho relativo: import de pacote (`@react-navigation/native`)
|
|
2738
|
+
* é de terceiro e não tem tela nossa dentro.
|
|
2739
|
+
*/
|
|
2740
|
+
resolve(fromFile, spec) {
|
|
2741
|
+
const key = `${fromFile} ${spec}`;
|
|
2742
|
+
const cached = this.resolved.get(key);
|
|
2743
|
+
if (cached !== void 0) return cached;
|
|
2744
|
+
let base = null;
|
|
2745
|
+
if (spec.startsWith(".")) {
|
|
2746
|
+
base = import_node_path.default.resolve(import_node_path.default.dirname(fromFile), spec);
|
|
2747
|
+
} else {
|
|
2748
|
+
for (const { prefix, target } of this.aliases) {
|
|
2749
|
+
if (spec === prefix || spec.startsWith(prefix + "/")) {
|
|
2750
|
+
base = import_node_path.default.join(target, spec.slice(prefix.length));
|
|
2751
|
+
break;
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
if (!base) {
|
|
2755
|
+
for (const pkg of this.workspacePackages) {
|
|
2756
|
+
if (spec === pkg.name || spec.startsWith(pkg.name + "/")) {
|
|
2757
|
+
base = import_node_path.default.join(pkg.dir, spec.slice(pkg.name.length));
|
|
2758
|
+
break;
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
if (!base) {
|
|
2764
|
+
this.resolved.set(key, null);
|
|
2765
|
+
return null;
|
|
2766
|
+
}
|
|
2767
|
+
const candidates = [
|
|
2768
|
+
base,
|
|
2769
|
+
...EXTENSIONS.map((e) => base + e),
|
|
2770
|
+
...EXTENSIONS.map((e) => import_node_path.default.join(base, "index" + e))
|
|
2771
|
+
];
|
|
2772
|
+
let found = null;
|
|
2773
|
+
for (const c of candidates) {
|
|
2774
|
+
try {
|
|
2775
|
+
if ((0, import_node_fs.existsSync)(c) && (0, import_node_fs.statSync)(c).isFile()) {
|
|
2776
|
+
found = c;
|
|
2777
|
+
break;
|
|
2778
|
+
}
|
|
2779
|
+
} catch {
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
this.resolved.set(key, found);
|
|
2783
|
+
return found;
|
|
2784
|
+
}
|
|
2785
|
+
/** `import`s do arquivo, por nome local. */
|
|
2786
|
+
imports(file2) {
|
|
2787
|
+
const out = /* @__PURE__ */ new Map();
|
|
2788
|
+
const ast = this.parse(file2);
|
|
2789
|
+
if (!ast) return out;
|
|
2790
|
+
for (const stmt of ast.program.body) {
|
|
2791
|
+
if (!t5.isImportDeclaration(stmt)) continue;
|
|
2792
|
+
const source = stmt.source.value;
|
|
2793
|
+
for (const spec of stmt.specifiers) {
|
|
2794
|
+
if (t5.isImportDefaultSpecifier(spec)) {
|
|
2795
|
+
out.set(spec.local.name, { source, imported: "default" });
|
|
2796
|
+
} else if (t5.isImportNamespaceSpecifier(spec)) {
|
|
2797
|
+
out.set(spec.local.name, { source, imported: "*" });
|
|
2798
|
+
} else if (t5.isImportSpecifier(spec)) {
|
|
2799
|
+
const imported = t5.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
|
|
2800
|
+
out.set(spec.local.name, { source, imported });
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
return out;
|
|
2805
|
+
}
|
|
2806
|
+
/** `const X = <init>` no topo do arquivo, incluindo `export const`. */
|
|
2807
|
+
topLevelInit(file2, name) {
|
|
2808
|
+
const ast = this.parse(file2);
|
|
2809
|
+
if (!ast) return null;
|
|
2810
|
+
for (const stmt of ast.program.body) {
|
|
2811
|
+
const decl = t5.isExportNamedDeclaration(stmt) ? stmt.declaration : stmt;
|
|
2812
|
+
if (!t5.isVariableDeclaration(decl)) continue;
|
|
2813
|
+
for (const d of decl.declarations) {
|
|
2814
|
+
if (t5.isIdentifier(d.id) && d.id.name === name && d.init) {
|
|
2815
|
+
return t5.isTSAsExpression(d.init) ? d.init.expression : d.init;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
return null;
|
|
2820
|
+
}
|
|
2821
|
+
/**
|
|
2822
|
+
* Onde `name` é DEFINIDO — segue import e reexport de barrel.
|
|
2823
|
+
* Devolve o arquivo e o nome sob o qual ele é definido lá.
|
|
2824
|
+
*/
|
|
2825
|
+
resolveBinding(file2, name, hops = 0) {
|
|
2826
|
+
if (hops > MAX_HOPS) return null;
|
|
2827
|
+
if (this.topLevelInit(file2, name) !== null) return { file: file2, name };
|
|
2828
|
+
const binding = this.imports(file2).get(name);
|
|
2829
|
+
if (binding) {
|
|
2830
|
+
const target = this.resolve(file2, binding.source);
|
|
2831
|
+
if (!target) return null;
|
|
2832
|
+
const next = binding.imported === "default" || binding.imported === "*" ? name : binding.imported;
|
|
2833
|
+
const deeper = this.resolveBinding(target, next, hops + 1);
|
|
2834
|
+
return deeper ?? { file: target, name: next };
|
|
2835
|
+
}
|
|
2836
|
+
const ast = this.parse(file2);
|
|
2837
|
+
if (ast) {
|
|
2838
|
+
for (const stmt of ast.program.body) {
|
|
2839
|
+
if (!t5.isExportNamedDeclaration(stmt) || !stmt.source) continue;
|
|
2840
|
+
for (const spec of stmt.specifiers) {
|
|
2841
|
+
if (!t5.isExportSpecifier(spec)) continue;
|
|
2842
|
+
const exported = t5.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value;
|
|
2843
|
+
if (exported !== name) continue;
|
|
2844
|
+
const target = this.resolve(file2, stmt.source.value);
|
|
2845
|
+
if (!target) return null;
|
|
2846
|
+
const local = spec.local.name;
|
|
2847
|
+
return this.resolveBinding(target, local, hops + 1) ?? { file: target, name: local };
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
if (ast) {
|
|
2852
|
+
for (const stmt of ast.program.body) {
|
|
2853
|
+
if (!t5.isExportAllDeclaration(stmt)) continue;
|
|
2854
|
+
const target = this.resolve(file2, stmt.source.value);
|
|
2855
|
+
if (!target || target === file2) continue;
|
|
2856
|
+
const deeper = this.resolveBinding(target, name, hops + 1);
|
|
2857
|
+
if (deeper) return deeper;
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
if (ast) {
|
|
2861
|
+
for (const stmt of ast.program.body) {
|
|
2862
|
+
if (!t5.isExportDefaultDeclaration(stmt)) continue;
|
|
2863
|
+
if (t5.isIdentifier(stmt.declaration)) {
|
|
2864
|
+
const local = stmt.declaration.name;
|
|
2865
|
+
if (local === name) return null;
|
|
2866
|
+
return this.resolveBinding(file2, local, hops + 1) ?? { file: file2, name: local };
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
return null;
|
|
2871
|
+
}
|
|
2872
|
+
/**
|
|
2873
|
+
* O valor string de uma expressão de nome de rota, ou `null`.
|
|
2874
|
+
*
|
|
2875
|
+
* Cobre `"Chat"`, `ROUTES.CHAT`, `ROUTES.ONBOARDING.SPLASH` e `SOME_CONST` —
|
|
2876
|
+
* seguindo import quando o objeto vem de outro arquivo. NÃO cobre template
|
|
2877
|
+
* com interpolação nem valor calculado, de propósito.
|
|
2878
|
+
*/
|
|
2879
|
+
stringConstant(file2, node, hops = 0) {
|
|
2880
|
+
if (!node || hops > MAX_HOPS) return null;
|
|
2881
|
+
if (t5.isStringLiteral(node)) return node.value;
|
|
2882
|
+
if (t5.isTemplateLiteral(node)) {
|
|
2883
|
+
return node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? null : null;
|
|
2884
|
+
}
|
|
2885
|
+
if (t5.isTSAsExpression(node)) return this.stringConstant(file2, node.expression, hops + 1);
|
|
2886
|
+
const chain = memberChain(node);
|
|
2887
|
+
if (!chain) return null;
|
|
2888
|
+
const origin = this.resolveBinding(file2, chain.root);
|
|
2889
|
+
if (!origin) return null;
|
|
2890
|
+
let current = this.topLevelInit(origin.file, origin.name);
|
|
2891
|
+
if (!current) return null;
|
|
2892
|
+
for (const key of chain.path) {
|
|
2893
|
+
if (!t5.isObjectExpression(current)) return null;
|
|
2894
|
+
const prop = objectProperty(current, key);
|
|
2895
|
+
if (!prop) return null;
|
|
2896
|
+
current = t5.isTSAsExpression(prop) ? prop.expression : prop;
|
|
2897
|
+
}
|
|
2898
|
+
return t5.isStringLiteral(current) ? current.value : null;
|
|
2899
|
+
}
|
|
2900
|
+
/**
|
|
2901
|
+
* O ARQUIVO onde vive o componente de uma rota, ou `null`.
|
|
2902
|
+
*
|
|
2903
|
+
* Aceita as três formas que o corpus mostrou: identificador
|
|
2904
|
+
* (`component={Home}`), membro de barrel (`component={screens.AccountHome}`)
|
|
2905
|
+
* e componente embrulhado em HOC (`component={gestureHandlerRootHOC(Chat)}`,
|
|
2906
|
+
* que é como o `pocketpal` monta todas as telas do Drawer).
|
|
2907
|
+
*/
|
|
2908
|
+
componentFile(file2, node, hops = 0) {
|
|
2909
|
+
if (!node || hops > MAX_HOPS) return null;
|
|
2910
|
+
if (t5.isCallExpression(node)) {
|
|
2911
|
+
for (const arg of node.arguments) {
|
|
2912
|
+
if (t5.isIdentifier(arg) || t5.isMemberExpression(arg)) {
|
|
2913
|
+
const inner = this.componentFile(file2, arg, hops + 1);
|
|
2914
|
+
if (inner) return inner;
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
return null;
|
|
2918
|
+
}
|
|
2919
|
+
if (t5.isTSAsExpression(node)) return this.componentFile(file2, node.expression, hops + 1);
|
|
2920
|
+
const chain = memberChain(node);
|
|
2921
|
+
if (!chain) return null;
|
|
2922
|
+
const origin = this.resolveBinding(file2, chain.root);
|
|
2923
|
+
if (!origin) return null;
|
|
2924
|
+
if (chain.path.length === 0) return origin.file;
|
|
2925
|
+
const init = this.topLevelInit(origin.file, origin.name);
|
|
2926
|
+
if (init && t5.isObjectExpression(init)) {
|
|
2927
|
+
const prop = objectProperty(init, chain.path[0]);
|
|
2928
|
+
if (prop) return this.componentFile(origin.file, prop, hops + 1);
|
|
2929
|
+
}
|
|
2930
|
+
const viaExport = this.resolveBinding(origin.file, chain.path[0], hops + 1);
|
|
2931
|
+
return viaExport?.file ?? null;
|
|
2932
|
+
}
|
|
2933
|
+
/**
|
|
2934
|
+
* Os arquivos DO PROJETO que este arquivo renderiza como JSX.
|
|
2935
|
+
*
|
|
2936
|
+
* `<ChatView …/>` em `ChatScreen.tsx` → `components/ChatView/ChatView.tsx`.
|
|
2937
|
+
* Import de pacote devolve `null` no `resolve` e fica de fora: componente de
|
|
2938
|
+
* terceiro não tem tela nossa dentro.
|
|
2939
|
+
*
|
|
2940
|
+
* Ignora `<X.Screen>` e `<X.Navigator>` de propósito — navegação é outro
|
|
2941
|
+
* extractor, e um navegador não é conteúdo de tela.
|
|
2942
|
+
*/
|
|
2943
|
+
renderedComponentFiles(file2) {
|
|
2944
|
+
const ast = this.parse(file2);
|
|
2945
|
+
if (!ast) return [];
|
|
2946
|
+
const names = /* @__PURE__ */ new Set();
|
|
2947
|
+
const visit = (node) => {
|
|
2948
|
+
if (!node || typeof node !== "object") return;
|
|
2949
|
+
if (t5.isJSXOpeningElement(node) && t5.isJSXIdentifier(node.name)) {
|
|
2950
|
+
const n = node.name.name;
|
|
2951
|
+
if (/^[A-Z]/.test(n)) names.add(n);
|
|
2952
|
+
}
|
|
2953
|
+
for (const key of Object.keys(node)) {
|
|
2954
|
+
const value = node[key];
|
|
2955
|
+
if (Array.isArray(value)) {
|
|
2956
|
+
for (const item of value) visit(item);
|
|
2957
|
+
} else if (value && typeof value === "object" && "type" in value) {
|
|
2958
|
+
visit(value);
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
};
|
|
2962
|
+
visit(ast.program);
|
|
2963
|
+
const out = /* @__PURE__ */ new Set();
|
|
2964
|
+
for (const name of names) {
|
|
2965
|
+
const origin = this.resolveBinding(file2, name);
|
|
2966
|
+
if (origin && origin.file !== file2) out.add(origin.file);
|
|
2967
|
+
}
|
|
2968
|
+
return [...out];
|
|
2969
|
+
}
|
|
2970
|
+
};
|
|
2971
|
+
function memberChain(node) {
|
|
2972
|
+
const chain = [];
|
|
2973
|
+
let current = node;
|
|
2974
|
+
while (t5.isMemberExpression(current)) {
|
|
2975
|
+
if (current.computed) {
|
|
2976
|
+
if (!t5.isStringLiteral(current.property)) return null;
|
|
2977
|
+
chain.unshift(current.property.value);
|
|
2978
|
+
} else if (t5.isIdentifier(current.property)) {
|
|
2979
|
+
chain.unshift(current.property.name);
|
|
2980
|
+
} else {
|
|
2981
|
+
return null;
|
|
2982
|
+
}
|
|
2983
|
+
current = current.object;
|
|
2984
|
+
}
|
|
2985
|
+
return t5.isIdentifier(current) ? { root: current.name, path: chain } : null;
|
|
2986
|
+
}
|
|
2987
|
+
function objectProperty(obj, key) {
|
|
2988
|
+
for (const prop of obj.properties) {
|
|
2989
|
+
if (!t5.isObjectProperty(prop)) continue;
|
|
2990
|
+
const name = t5.isIdentifier(prop.key) ? prop.key.name : t5.isStringLiteral(prop.key) ? prop.key.value : null;
|
|
2991
|
+
if (name === key && t5.isExpression(prop.value)) return prop.value;
|
|
2992
|
+
}
|
|
2993
|
+
return null;
|
|
2994
|
+
}
|
|
2995
|
+
function readTsconfigAliases(rootDir) {
|
|
2996
|
+
const file2 = import_node_path.default.join(rootDir, "tsconfig.json");
|
|
2997
|
+
if (!(0, import_node_fs.existsSync)(file2)) return [];
|
|
2998
|
+
let parsed;
|
|
2999
|
+
try {
|
|
3000
|
+
const raw = stripJsonComments((0, import_node_fs.readFileSync)(file2, "utf-8")).replace(/,(\s*[}\]])/g, "$1");
|
|
3001
|
+
parsed = JSON.parse(raw);
|
|
3002
|
+
} catch {
|
|
3003
|
+
return [];
|
|
3004
|
+
}
|
|
3005
|
+
const paths = parsed.compilerOptions?.paths;
|
|
3006
|
+
if (!paths) return [];
|
|
3007
|
+
const baseUrl = import_node_path.default.resolve(rootDir, parsed.compilerOptions?.baseUrl ?? ".");
|
|
3008
|
+
const out = [];
|
|
3009
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
3010
|
+
const first = targets[0];
|
|
3011
|
+
if (typeof first !== "string") continue;
|
|
3012
|
+
out.push({
|
|
3013
|
+
prefix: pattern.replace(/\/?\*$/, ""),
|
|
3014
|
+
target: import_node_path.default.resolve(baseUrl, first.replace(/\/?\*$/, ""))
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
return out.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
3018
|
+
}
|
|
3019
|
+
function stripJsonComments(text) {
|
|
3020
|
+
let out = "";
|
|
3021
|
+
let inString = false;
|
|
3022
|
+
let escaped = false;
|
|
3023
|
+
for (let i = 0; i < text.length; i++) {
|
|
3024
|
+
const c = text[i];
|
|
3025
|
+
if (inString) {
|
|
3026
|
+
out += c;
|
|
3027
|
+
if (escaped) escaped = false;
|
|
3028
|
+
else if (c === "\\") escaped = true;
|
|
3029
|
+
else if (c === '"') inString = false;
|
|
3030
|
+
continue;
|
|
3031
|
+
}
|
|
3032
|
+
if (c === '"') {
|
|
3033
|
+
inString = true;
|
|
3034
|
+
out += c;
|
|
3035
|
+
continue;
|
|
3036
|
+
}
|
|
3037
|
+
if (c === "/" && text[i + 1] === "/") {
|
|
3038
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
3039
|
+
out += "\n";
|
|
3040
|
+
continue;
|
|
3041
|
+
}
|
|
3042
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
3043
|
+
i += 2;
|
|
3044
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
3045
|
+
i++;
|
|
3046
|
+
continue;
|
|
3047
|
+
}
|
|
3048
|
+
out += c;
|
|
3049
|
+
}
|
|
3050
|
+
return out;
|
|
3051
|
+
}
|
|
3052
|
+
function readWorkspacePackages(rootDir) {
|
|
3053
|
+
const start = import_node_path.default.resolve(rootDir);
|
|
3054
|
+
let dir = start;
|
|
3055
|
+
for (let i = 0; i < 6; i++) {
|
|
3056
|
+
const globs = workspaceGlobs(dir);
|
|
3057
|
+
if (globs.length > 0) {
|
|
3058
|
+
const packages = expandWorkspaceGlobs(dir, globs);
|
|
3059
|
+
const containsApp = packages.some(
|
|
3060
|
+
(p) => start === p.dir || start.startsWith(p.dir + import_node_path.default.sep)
|
|
3061
|
+
);
|
|
3062
|
+
return containsApp ? packages : [];
|
|
3063
|
+
}
|
|
3064
|
+
const parent = import_node_path.default.dirname(dir);
|
|
3065
|
+
if (parent === dir) break;
|
|
3066
|
+
dir = parent;
|
|
3067
|
+
}
|
|
3068
|
+
return [];
|
|
3069
|
+
}
|
|
3070
|
+
function workspaceGlobs(dir) {
|
|
3071
|
+
const pnpm = import_node_path.default.join(dir, "pnpm-workspace.yaml");
|
|
3072
|
+
if ((0, import_node_fs.existsSync)(pnpm)) {
|
|
3073
|
+
try {
|
|
3074
|
+
const lines = (0, import_node_fs.readFileSync)(pnpm, "utf-8").split(/\r?\n/);
|
|
3075
|
+
const out = [];
|
|
3076
|
+
let inPackages = false;
|
|
3077
|
+
for (const line of lines) {
|
|
3078
|
+
if (/^packages:/.test(line)) {
|
|
3079
|
+
inPackages = true;
|
|
3080
|
+
continue;
|
|
3081
|
+
}
|
|
3082
|
+
if (inPackages) {
|
|
3083
|
+
const m = /^\s*-\s*["']?([^"'#]+?)["']?\s*$/.exec(line);
|
|
3084
|
+
if (m) out.push(m[1].trim());
|
|
3085
|
+
else if (/^\S/.test(line)) break;
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
if (out.length) return out;
|
|
3089
|
+
} catch {
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
const pkgPath = import_node_path.default.join(dir, "package.json");
|
|
3093
|
+
if (!(0, import_node_fs.existsSync)(pkgPath)) return [];
|
|
3094
|
+
try {
|
|
3095
|
+
const pkg = JSON.parse((0, import_node_fs.readFileSync)(pkgPath, "utf-8"));
|
|
3096
|
+
const ws = pkg.workspaces;
|
|
3097
|
+
if (Array.isArray(ws)) return ws;
|
|
3098
|
+
if (ws && Array.isArray(ws.packages)) return ws.packages;
|
|
3099
|
+
} catch {
|
|
3100
|
+
}
|
|
3101
|
+
return [];
|
|
3102
|
+
}
|
|
3103
|
+
function expandWorkspaceGlobs(root, globs) {
|
|
3104
|
+
const out = [];
|
|
3105
|
+
const add = (dir) => {
|
|
3106
|
+
const pkgPath = import_node_path.default.join(dir, "package.json");
|
|
3107
|
+
if (!(0, import_node_fs.existsSync)(pkgPath)) return;
|
|
3108
|
+
try {
|
|
3109
|
+
const name = JSON.parse((0, import_node_fs.readFileSync)(pkgPath, "utf-8")).name;
|
|
3110
|
+
if (name) out.push({ name, dir });
|
|
3111
|
+
} catch {
|
|
3112
|
+
}
|
|
3113
|
+
};
|
|
3114
|
+
for (const glob of globs) {
|
|
3115
|
+
if (glob.endsWith("/*")) {
|
|
3116
|
+
const parent = import_node_path.default.join(root, glob.slice(0, -2));
|
|
3117
|
+
let entries = [];
|
|
3118
|
+
try {
|
|
3119
|
+
entries = (0, import_node_fs.readdirSync)(parent);
|
|
3120
|
+
} catch {
|
|
3121
|
+
continue;
|
|
3122
|
+
}
|
|
3123
|
+
for (const entry of entries) {
|
|
3124
|
+
const dir = import_node_path.default.join(parent, entry);
|
|
3125
|
+
try {
|
|
3126
|
+
if ((0, import_node_fs.statSync)(dir).isDirectory()) add(dir);
|
|
3127
|
+
} catch {
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
} else if (!glob.includes("*")) {
|
|
3131
|
+
add(import_node_path.default.join(root, glob));
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
return out.sort((a, b) => b.name.length - a.name.length);
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
// src/ast/navigation/expo-router.ts
|
|
3138
|
+
var import_node_fs2 = require("fs");
|
|
3139
|
+
var import_node_path2 = __toESM(require("path"));
|
|
3140
|
+
var ROUTE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
3141
|
+
var PLATFORM_SUFFIXES = [".ios", ".android", ".native", ".web"];
|
|
3142
|
+
var ROUTE_DIR_CANDIDATES = ["app", "src/app"];
|
|
3143
|
+
function baseName(file2) {
|
|
3144
|
+
let name = file2;
|
|
3145
|
+
for (const ext of ROUTE_EXTENSIONS) {
|
|
3146
|
+
if (name.endsWith(ext)) {
|
|
3147
|
+
name = name.slice(0, -ext.length);
|
|
3148
|
+
break;
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
for (const suffix of PLATFORM_SUFFIXES) {
|
|
3152
|
+
if (name.endsWith(suffix)) return name.slice(0, -suffix.length);
|
|
3153
|
+
}
|
|
3154
|
+
return name;
|
|
3155
|
+
}
|
|
3156
|
+
function isRouteFile(file2) {
|
|
3157
|
+
return ROUTE_EXTENSIONS.some((ext) => file2.endsWith(ext));
|
|
3158
|
+
}
|
|
3159
|
+
function isSpecial(base) {
|
|
3160
|
+
if (base === "_layout") return true;
|
|
3161
|
+
if (base.startsWith("+")) return base !== "+not-found";
|
|
3162
|
+
return base.startsWith("_");
|
|
3163
|
+
}
|
|
3164
|
+
function layoutType(file2) {
|
|
3165
|
+
try {
|
|
3166
|
+
const src = (0, import_node_fs2.readFileSync)(file2, "utf-8");
|
|
3167
|
+
if (/<(?:Native)?Tabs\b/.test(src)) return "tab";
|
|
3168
|
+
if (/<Drawer\b/.test(src)) return "drawer";
|
|
3169
|
+
} catch {
|
|
3170
|
+
}
|
|
3171
|
+
return "stack";
|
|
3172
|
+
}
|
|
3173
|
+
function findLayout(dir) {
|
|
3174
|
+
for (const ext of ROUTE_EXTENSIONS) {
|
|
3175
|
+
const candidate = import_node_path2.default.join(dir, "_layout" + ext);
|
|
3176
|
+
if ((0, import_node_fs2.existsSync)(candidate)) return candidate;
|
|
3177
|
+
}
|
|
3178
|
+
return null;
|
|
3179
|
+
}
|
|
3180
|
+
function analyzeExpoRouter(appRoot) {
|
|
3181
|
+
let routeDir = null;
|
|
3182
|
+
for (const candidate of ROUTE_DIR_CANDIDATES) {
|
|
3183
|
+
const full = import_node_path2.default.join(appRoot, candidate);
|
|
3184
|
+
if ((0, import_node_fs2.existsSync)(full) && findLayout(full)) {
|
|
3185
|
+
routeDir = full;
|
|
3186
|
+
break;
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
if (!routeDir) return null;
|
|
3190
|
+
const routes = [];
|
|
3191
|
+
const navigators = /* @__PURE__ */ new Map();
|
|
3192
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3193
|
+
const walk = (dir, segments, navigator) => {
|
|
3194
|
+
const layout = findLayout(dir);
|
|
3195
|
+
const current = layout ? segments.join("/") || "/" : navigator;
|
|
3196
|
+
if (layout && !navigators.has(current)) navigators.set(current, layoutType(layout));
|
|
3197
|
+
let entries;
|
|
3198
|
+
try {
|
|
3199
|
+
entries = (0, import_node_fs2.readdirSync)(dir);
|
|
3200
|
+
} catch {
|
|
3201
|
+
return;
|
|
3202
|
+
}
|
|
3203
|
+
for (const entry of entries.sort()) {
|
|
3204
|
+
const full = import_node_path2.default.join(dir, entry);
|
|
3205
|
+
let isDir = false;
|
|
3206
|
+
try {
|
|
3207
|
+
isDir = (0, import_node_fs2.statSync)(full).isDirectory();
|
|
3208
|
+
} catch {
|
|
3209
|
+
continue;
|
|
3210
|
+
}
|
|
3211
|
+
if (isDir) {
|
|
3212
|
+
if (entry === "node_modules" || entry.startsWith(".")) continue;
|
|
3213
|
+
walk(full, [...segments, entry], current);
|
|
3214
|
+
continue;
|
|
3215
|
+
}
|
|
3216
|
+
if (!isRouteFile(entry)) continue;
|
|
3217
|
+
const base = baseName(entry);
|
|
3218
|
+
if (isSpecial(base)) continue;
|
|
3219
|
+
const routeSegments = base === "index" ? segments : [...segments, base];
|
|
3220
|
+
const name = "/" + routeSegments.join("/");
|
|
3221
|
+
if (seen.has(name)) continue;
|
|
3222
|
+
seen.add(name);
|
|
3223
|
+
routes.push({
|
|
3224
|
+
name,
|
|
3225
|
+
componentFile: full,
|
|
3226
|
+
navigatorName: current,
|
|
3227
|
+
navigatorType: navigators.get(current) ?? "stack",
|
|
3228
|
+
params: routeSegments.filter((s) => s.startsWith("[") && s.endsWith("]")).map((s) => s.slice(1, -1).replace(/^\.\.\./, ""))
|
|
3229
|
+
});
|
|
3230
|
+
}
|
|
3231
|
+
};
|
|
3232
|
+
walk(routeDir, [], "/");
|
|
3233
|
+
return {
|
|
3234
|
+
routeDir: import_node_path2.default.relative(appRoot, routeDir),
|
|
3235
|
+
routes,
|
|
3236
|
+
navigators: [...navigators.entries()].map(([name, type]) => ({ name, type }))
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
// src/analyzers/NavigationAnalyzer.ts
|
|
3241
|
+
var NAVIGATOR_FACTORIES = {
|
|
3242
|
+
createStackNavigator: "stack",
|
|
3243
|
+
createNativeStackNavigator: "stack",
|
|
3244
|
+
createBottomTabNavigator: "tab",
|
|
3245
|
+
createTabNavigator: "tab",
|
|
3246
|
+
createMaterialTopTabNavigator: "tab",
|
|
3247
|
+
createMaterialBottomTabNavigator: "tab",
|
|
3248
|
+
createDrawerNavigator: "drawer"
|
|
3249
|
+
};
|
|
3250
|
+
var CUSTOM_FACTORY = /^create[A-Za-z0-9]*(Navigator|Stack|Tabs?|Drawer)$/;
|
|
3251
|
+
function unwrapStaticScreen(value) {
|
|
3252
|
+
if (t6.isCallExpression(value)) {
|
|
3253
|
+
const arg = value.arguments[0];
|
|
3254
|
+
return arg ? unwrapStaticScreen(arg) : null;
|
|
3255
|
+
}
|
|
3256
|
+
if (t6.isObjectExpression(value)) {
|
|
3257
|
+
for (const prop of value.properties) {
|
|
3258
|
+
if (!t6.isObjectProperty(prop)) continue;
|
|
3259
|
+
const key = t6.isIdentifier(prop.key) ? prop.key.name : null;
|
|
3260
|
+
if (key === "screen" && t6.isExpression(prop.value)) return prop.value;
|
|
3261
|
+
}
|
|
3262
|
+
return null;
|
|
3263
|
+
}
|
|
3264
|
+
return t6.isExpression(value) ? value : null;
|
|
3265
|
+
}
|
|
3266
|
+
function inferNavigatorType(factoryName) {
|
|
3267
|
+
if (/drawer/i.test(factoryName)) return "drawer";
|
|
3268
|
+
if (/tab/i.test(factoryName)) return "tab";
|
|
3269
|
+
return "stack";
|
|
3270
|
+
}
|
|
3271
|
+
var NAVIGATION_EVIDENCE = new RegExp(
|
|
3272
|
+
// `\.Navigator`/`\.Screen` entram porque o JSX pode estar num arquivo que
|
|
3273
|
+
// não menciona fábrica nenhuma: `comapeo` declara `RootStack` em
|
|
3274
|
+
// `Stack/RootStack.ts` e escreve todas as 120 rotas em `Stack/index.tsx`,
|
|
3275
|
+
// que não cita `createNativeStackNavigator` uma vez sequer. Sem isto a
|
|
3276
|
+
// junção entre arquivos nunca chega a ser tentada.
|
|
3277
|
+
// Sem `\\b` depois da palavra — `createNativeStackNavigatorWithAuth` não tem
|
|
3278
|
+
// fronteira ali, e exigi-la fecharia o portão para o arquivo que declara os
|
|
3279
|
+
// seis navegadores do `bluesky`.
|
|
3280
|
+
`create[A-Za-z0-9]*(?:Navigator|Stack|Tabs?|Drawer)|ParamList|\\.Navigator\\b|\\.Screen\\b`
|
|
3281
|
+
);
|
|
2617
3282
|
var NavigationAnalyzer = class {
|
|
2618
3283
|
config;
|
|
2619
3284
|
/** Extra glob patterns for navigation file discovery (added to defaults) */
|
|
2620
3285
|
navigationInclude;
|
|
2621
3286
|
/** Extra glob patterns to exclude from navigation analysis */
|
|
2622
3287
|
navigationExclude;
|
|
3288
|
+
/** Exposto para a composição reusar o cache de AST em vez de reparsear a árvore. */
|
|
3289
|
+
graph;
|
|
3290
|
+
/**
|
|
3291
|
+
* Rota → arquivo do componente que ela monta. É a única ligação entre o grafo
|
|
3292
|
+
* de navegação e a árvore de telas; sem ela as duas metades do documento
|
|
3293
|
+
* falam de coisas diferentes. Populada por `analyze()`.
|
|
3294
|
+
*/
|
|
3295
|
+
routeTargets = /* @__PURE__ */ new Map();
|
|
3296
|
+
/**
|
|
3297
|
+
* Arquivos que DECLARAM uma fábrica de navegador.
|
|
3298
|
+
*
|
|
3299
|
+
* Um navegador aninhado é montado como `component=` de uma rota — no
|
|
3300
|
+
* `apps/example-app`, `<Stack.Screen name="Main" component={MainTabNavigator} />`.
|
|
3301
|
+
* Promover esse arquivo a tela criaria seis "telas" sem uma única ação, que é
|
|
3302
|
+
* exatamente o falso positivo que o corpus existe para não repetir. Um
|
|
3303
|
+
* navegador é um contêiner de rotas; a tela está um nível abaixo.
|
|
3304
|
+
*
|
|
3305
|
+
* Entram aqui os dois lados: o arquivo que CHAMA a fábrica e o arquivo que
|
|
3306
|
+
* RENDERIZA `<X.Navigator>`. Não são o mesmo — `bluewallet` declara
|
|
3307
|
+
* `DetailViewStack` num arquivo e escreve o JSX em
|
|
3308
|
+
* `navigation/DetailViewScreensStack.tsx`, e checar só o primeiro deixava o
|
|
3309
|
+
* segundo entrar como tela.
|
|
3310
|
+
*/
|
|
3311
|
+
navigatorFiles = /* @__PURE__ */ new Set();
|
|
3312
|
+
/** Preenchido por `analyze()`. Vazio antes disso. */
|
|
3313
|
+
diagnostics = {
|
|
3314
|
+
filesScanned: 0,
|
|
3315
|
+
filesWithEvidence: 0,
|
|
3316
|
+
navigatorsDeclared: 0,
|
|
3317
|
+
navigatorsWithJsx: 0,
|
|
3318
|
+
routes: 0,
|
|
3319
|
+
routesLinkedToFile: 0
|
|
3320
|
+
};
|
|
2623
3321
|
constructor(config, options) {
|
|
2624
3322
|
this.config = config;
|
|
3323
|
+
this.graph = new ModuleGraph(config.rootDir);
|
|
2625
3324
|
this.navigationInclude = options?.navigationInclude ?? [];
|
|
2626
3325
|
this.navigationExclude = options?.navigationExclude ?? [];
|
|
2627
3326
|
}
|
|
2628
3327
|
/** Build the full navigation graph */
|
|
2629
3328
|
async analyze() {
|
|
2630
3329
|
const navigationFiles = await this.findNavigationFiles();
|
|
2631
|
-
const parsedNavigators = [];
|
|
2632
3330
|
const typeExports = [];
|
|
3331
|
+
this.routeTargets.clear();
|
|
3332
|
+
const declarations = /* @__PURE__ */ new Map();
|
|
3333
|
+
const evidenceFiles = [];
|
|
3334
|
+
this.navigatorFiles.clear();
|
|
2633
3335
|
for (const filePath of navigationFiles) {
|
|
2634
3336
|
try {
|
|
2635
3337
|
const content = await import_fs4.promises.readFile(filePath, "utf-8");
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
3338
|
+
if (!NAVIGATION_EVIDENCE.test(content)) continue;
|
|
3339
|
+
evidenceFiles.push(filePath);
|
|
3340
|
+
for (const decl of this.collectDeclarations(filePath)) {
|
|
3341
|
+
declarations.set(`${decl.file}#${decl.name}`, decl);
|
|
3342
|
+
if (decl.confident) this.navigatorFiles.add(decl.file);
|
|
3343
|
+
}
|
|
3344
|
+
typeExports.push(...this.parseParamTypes(content, filePath));
|
|
2640
3345
|
} catch (error2) {
|
|
2641
3346
|
console.warn(`Failed to parse ${filePath}:`, error2);
|
|
2642
3347
|
}
|
|
2643
3348
|
}
|
|
3349
|
+
const parsedNavigators = [];
|
|
3350
|
+
for (const filePath of evidenceFiles) {
|
|
3351
|
+
try {
|
|
3352
|
+
parsedNavigators.push(...this.parseNavigatorUsages(filePath, declarations));
|
|
3353
|
+
} catch (error2) {
|
|
3354
|
+
console.warn(`Failed to parse navigators in ${filePath}:`, error2);
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
let detachedScreens = 0;
|
|
3358
|
+
for (const filePath of evidenceFiles) {
|
|
3359
|
+
try {
|
|
3360
|
+
for (const detached of this.parseDetachedScreens(filePath, declarations)) {
|
|
3361
|
+
const existing = parsedNavigators.find(
|
|
3362
|
+
(nav) => nav.name === detached.name && nav.type === detached.type
|
|
3363
|
+
);
|
|
3364
|
+
if (!existing) {
|
|
3365
|
+
parsedNavigators.push(detached);
|
|
3366
|
+
detachedScreens += detached.screens.length;
|
|
3367
|
+
continue;
|
|
3368
|
+
}
|
|
3369
|
+
const seen = new Set(existing.screens.map((screen) => screen.name));
|
|
3370
|
+
for (const screen of detached.screens) {
|
|
3371
|
+
if (seen.has(screen.name)) continue;
|
|
3372
|
+
seen.add(screen.name);
|
|
3373
|
+
existing.screens.push(screen);
|
|
3374
|
+
detachedScreens++;
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
} catch (error2) {
|
|
3378
|
+
console.warn(`Failed to parse detached screens in ${filePath}:`, error2);
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
const expo = analyzeExpoRouter(this.config.rootDir);
|
|
3382
|
+
if (expo) {
|
|
3383
|
+
console.log(
|
|
3384
|
+
`[NavigationAnalyzer] expo-router em "${expo.routeDir}": ${expo.routes.length} rotas, ${expo.navigators.length} layouts`
|
|
3385
|
+
);
|
|
3386
|
+
this.diagnostics.fileBasedRouter = {
|
|
3387
|
+
kind: "expo-router",
|
|
3388
|
+
routeDir: expo.routeDir,
|
|
3389
|
+
routes: expo.routes.length
|
|
3390
|
+
};
|
|
3391
|
+
const byNavigator = /* @__PURE__ */ new Map();
|
|
3392
|
+
for (const nav of expo.navigators) {
|
|
3393
|
+
byNavigator.set(nav.name, { name: nav.name, type: nav.type, screens: [] });
|
|
3394
|
+
}
|
|
3395
|
+
for (const route of expo.routes) {
|
|
3396
|
+
const navigator = byNavigator.get(route.navigatorName) ?? byNavigator.set(route.navigatorName, {
|
|
3397
|
+
name: route.navigatorName,
|
|
3398
|
+
type: route.navigatorType,
|
|
3399
|
+
screens: []
|
|
3400
|
+
}).get(route.navigatorName);
|
|
3401
|
+
navigator.screens.push({
|
|
3402
|
+
name: route.name,
|
|
3403
|
+
navigatorName: route.navigatorName,
|
|
3404
|
+
navigatorType: route.navigatorType,
|
|
3405
|
+
componentFile: route.componentFile,
|
|
3406
|
+
...route.params.length ? {
|
|
3407
|
+
params: route.params.map((name) => ({ name, type: "string", required: true }))
|
|
3408
|
+
} : {}
|
|
3409
|
+
});
|
|
3410
|
+
this.routeTargets.set(route.name, route.componentFile);
|
|
3411
|
+
}
|
|
3412
|
+
parsedNavigators.push(...byNavigator.values());
|
|
3413
|
+
}
|
|
3414
|
+
let staticOnly = 0;
|
|
3415
|
+
for (const decl of declarations.values()) {
|
|
3416
|
+
if (!decl.confident || !decl.staticScreens?.length) continue;
|
|
3417
|
+
const existing = parsedNavigators.find((n) => n.name === decl.name && n.type === decl.type);
|
|
3418
|
+
if (existing) {
|
|
3419
|
+
const seen = new Set(existing.screens.map((s) => s.name));
|
|
3420
|
+
for (const screen of decl.staticScreens) {
|
|
3421
|
+
if (!seen.has(screen.name)) existing.screens.push(screen);
|
|
3422
|
+
}
|
|
3423
|
+
continue;
|
|
3424
|
+
}
|
|
3425
|
+
parsedNavigators.push({ name: decl.name, type: decl.type, screens: decl.staticScreens });
|
|
3426
|
+
staticOnly++;
|
|
3427
|
+
}
|
|
3428
|
+
const confidentDeclarations = [...declarations.values()].filter((d) => d.confident).length;
|
|
3429
|
+
const uniqueRoutes = new Set(
|
|
3430
|
+
parsedNavigators.flatMap((nav) => nav.screens.map((screen) => screen.name))
|
|
3431
|
+
);
|
|
3432
|
+
this.diagnostics = {
|
|
3433
|
+
...this.diagnostics,
|
|
3434
|
+
filesScanned: navigationFiles.length,
|
|
3435
|
+
filesWithEvidence: evidenceFiles.length,
|
|
3436
|
+
navigatorsDeclared: confidentDeclarations,
|
|
3437
|
+
navigatorsWithJsx: parsedNavigators.length,
|
|
3438
|
+
routes: uniqueRoutes.size,
|
|
3439
|
+
// `routeTargets` é indexado por nome de rota, e uma rota pode ter sido
|
|
3440
|
+
// registrada por um navegador que não sobreviveu. O mínimo evita a
|
|
3441
|
+
// cobertura acima de 100% que a medição do corpus expôs no `coopcycle`.
|
|
3442
|
+
routesLinkedToFile: Math.min(this.routeTargets.size, uniqueRoutes.size)
|
|
3443
|
+
};
|
|
3444
|
+
console.log(
|
|
3445
|
+
`[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`
|
|
3446
|
+
);
|
|
2644
3447
|
this.attachParamsToNavigators(parsedNavigators, typeExports);
|
|
2645
3448
|
return this.buildNavigationGraph(parsedNavigators);
|
|
2646
3449
|
}
|
|
2647
|
-
/**
|
|
3450
|
+
/** Fase 1: as fábricas `create*Navigator()` atribuídas a um nome neste arquivo. */
|
|
3451
|
+
collectDeclarations(filePath) {
|
|
3452
|
+
const ast = this.graph.parse(filePath);
|
|
3453
|
+
if (!ast) return [];
|
|
3454
|
+
const out = /* @__PURE__ */ new Map();
|
|
3455
|
+
(0, import_traverse5.default)(ast, {
|
|
3456
|
+
CallExpression: (nodePath) => {
|
|
3457
|
+
const callee = nodePath.node.callee;
|
|
3458
|
+
if (!t6.isIdentifier(callee)) return;
|
|
3459
|
+
const known = NAVIGATOR_FACTORIES[callee.name] ?? (CUSTOM_FACTORY.test(callee.name) ? inferNavigatorType(callee.name) : null);
|
|
3460
|
+
const type = known ?? inferNavigatorType(callee.name);
|
|
3461
|
+
const confident = known !== null;
|
|
3462
|
+
let up = nodePath.parentPath;
|
|
3463
|
+
for (let i = 0; i < 4 && up; i++) {
|
|
3464
|
+
if (t6.isVariableDeclarator(up.node)) break;
|
|
3465
|
+
if (!t6.isMemberExpression(up.node) && !t6.isCallExpression(up.node)) {
|
|
3466
|
+
up = null;
|
|
3467
|
+
break;
|
|
3468
|
+
}
|
|
3469
|
+
up = up.parentPath;
|
|
3470
|
+
}
|
|
3471
|
+
const declarator = up?.node;
|
|
3472
|
+
if (!declarator || !t6.isVariableDeclarator(declarator) || !t6.isIdentifier(declarator.id)) {
|
|
3473
|
+
return;
|
|
3474
|
+
}
|
|
3475
|
+
const name = declarator.id.name;
|
|
3476
|
+
if (out.get(name)?.confident) return;
|
|
3477
|
+
const staticScreens = confident ? this.parseStaticScreens(filePath, nodePath.node.arguments[0], name, type) : [];
|
|
3478
|
+
out.set(name, {
|
|
3479
|
+
file: filePath,
|
|
3480
|
+
name,
|
|
3481
|
+
type,
|
|
3482
|
+
confident,
|
|
3483
|
+
...staticScreens.length ? { staticScreens } : {}
|
|
3484
|
+
});
|
|
3485
|
+
}
|
|
3486
|
+
});
|
|
3487
|
+
return [...out.values()];
|
|
3488
|
+
}
|
|
3489
|
+
/**
|
|
3490
|
+
* As rotas de `createXNavigator({ screens: { … } })`.
|
|
3491
|
+
*
|
|
3492
|
+
* Quatro formas no corpus, todas em `rocketchat`:
|
|
3493
|
+
* `NewServerView` — shorthand, nome = componente
|
|
3494
|
+
* `LoginView: createNativeStackScreen({ screen })` — embrulho da própria lib
|
|
3495
|
+
* `SelectListView: SelectListViewScreen` — identificador direto
|
|
3496
|
+
* `ChatsStackNavigator: ChatsStack` — navegador aninhado
|
|
3497
|
+
*
|
|
3498
|
+
* `groups: { G: { screens: { … } } }` também entra: faz parte da API estática
|
|
3499
|
+
* e agrupar rotas não muda o que elas são.
|
|
3500
|
+
*/
|
|
3501
|
+
parseStaticScreens(filePath, config, navigatorName, navigatorType, depth = 0) {
|
|
3502
|
+
if (!config || !t6.isObjectExpression(config) || depth > 4) return [];
|
|
3503
|
+
const out = [];
|
|
3504
|
+
for (const prop of config.properties) {
|
|
3505
|
+
if (!t6.isObjectProperty(prop)) continue;
|
|
3506
|
+
const key = t6.isIdentifier(prop.key) ? prop.key.name : t6.isStringLiteral(prop.key) ? prop.key.value : null;
|
|
3507
|
+
if (!key) continue;
|
|
3508
|
+
if (key === "groups" && t6.isObjectExpression(prop.value)) {
|
|
3509
|
+
for (const group of prop.value.properties) {
|
|
3510
|
+
if (!t6.isObjectProperty(group) || !t6.isExpression(group.value)) continue;
|
|
3511
|
+
out.push(
|
|
3512
|
+
...this.parseStaticScreens(
|
|
3513
|
+
filePath,
|
|
3514
|
+
group.value,
|
|
3515
|
+
navigatorName,
|
|
3516
|
+
navigatorType,
|
|
3517
|
+
depth + 1
|
|
3518
|
+
)
|
|
3519
|
+
);
|
|
3520
|
+
}
|
|
3521
|
+
continue;
|
|
3522
|
+
}
|
|
3523
|
+
if (key !== "screens" || !t6.isObjectExpression(prop.value)) continue;
|
|
3524
|
+
for (const entry of prop.value.properties) {
|
|
3525
|
+
if (!t6.isObjectProperty(entry)) continue;
|
|
3526
|
+
const routeName = t6.isIdentifier(entry.key) ? entry.key.name : t6.isStringLiteral(entry.key) ? entry.key.value : null;
|
|
3527
|
+
if (!routeName) continue;
|
|
3528
|
+
const componentExpr = unwrapStaticScreen(entry.value);
|
|
3529
|
+
const componentFile = componentExpr ? this.graph.componentFile(filePath, componentExpr) : null;
|
|
3530
|
+
if (componentFile) this.routeTargets.set(routeName, componentFile);
|
|
3531
|
+
out.push({
|
|
3532
|
+
name: routeName,
|
|
3533
|
+
navigatorName,
|
|
3534
|
+
navigatorType,
|
|
3535
|
+
...componentFile ? { componentFile } : {}
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
return out;
|
|
3540
|
+
}
|
|
3541
|
+
/**
|
|
3542
|
+
* Todo arquivo-fonte do projeto — não os que ficam num diretório com o nome
|
|
3543
|
+
* certo.
|
|
3544
|
+
*
|
|
3545
|
+
* POR QUE ISTO MUDOU. A lista anterior era `**\/navigation/**`,
|
|
3546
|
+
* `**\/navigator*` e `**\/routes*`, o que fazia da descoberta de navegação uma
|
|
3547
|
+
* convenção de caminho. Medido contra 20 apps React Native de terceiros
|
|
3548
|
+
* (`qa/eval-corpus`), o filtro errava por motivos que nada têm a ver com o
|
|
3549
|
+
* app não ter navegação:
|
|
3550
|
+
*
|
|
3551
|
+
* - `pocketpal` declara 4 navegadores em `App.tsx` e dentro de `src/screens/`;
|
|
3552
|
+
* - `comapeo` usa `src/frontend/Navigation/` — `N` maiúsculo, e o glob é
|
|
3553
|
+
* sensível a caixa;
|
|
3554
|
+
* - `abacus` usa `src/routes/index.tsx` — `routes` é o DIRETÓRIO, e o glob
|
|
3555
|
+
* pedia um arquivo chamado `routes*`;
|
|
3556
|
+
* - `discourse` declara em `js/Discourse.js` — o glob só aceitava `.ts`/`.tsx`;
|
|
3557
|
+
* - `rainbow` tem 11 arquivos com navegador e o glob alcançava 1.
|
|
3558
|
+
*
|
|
3559
|
+
* Varrer tudo não custa uma varredura nova: `ReactNativePlatformAnalyzer` já
|
|
3560
|
+
* globa e parseia a árvore inteira para `ComponentAnalyzer` e `FormAnalyzer`.
|
|
3561
|
+
* O que segura o custo aqui é o portão por evidência em `analyze()`, que só
|
|
3562
|
+
* parseia arquivo cujo texto menciona uma fábrica de navegador.
|
|
3563
|
+
*
|
|
3564
|
+
* Os globs antigos continuam na lista: se um projeto restringir `include`, o
|
|
3565
|
+
* que era encontrado antes continua sendo.
|
|
3566
|
+
*/
|
|
2648
3567
|
async findNavigationFiles() {
|
|
2649
3568
|
const patterns = [
|
|
3569
|
+
...this.config.include ?? ["**/*.{ts,tsx,js,jsx}"],
|
|
3570
|
+
// Piso de compatibilidade — nunca encontrar MENOS que a versão anterior.
|
|
2650
3571
|
"**/navigation/**/*.{ts,tsx}",
|
|
2651
3572
|
"**/navigator*.{ts,tsx}",
|
|
2652
3573
|
"**/routes*.{ts,tsx}",
|
|
@@ -2657,91 +3578,188 @@ var NavigationAnalyzer = class {
|
|
|
2657
3578
|
"**/node_modules/**",
|
|
2658
3579
|
"**/dist/**",
|
|
2659
3580
|
"**/build/**",
|
|
3581
|
+
// Um navegador declarado dentro de um teste é fixture, não a navegação
|
|
3582
|
+
// do app. Isto passou a importar quando a varredura deixou de ser por
|
|
3583
|
+
// caminho: em `expensify`, os únicos arquivos que o glob antigo
|
|
3584
|
+
// alcançava eram três de `tests/`.
|
|
3585
|
+
"**/__tests__/**",
|
|
3586
|
+
"**/test/**",
|
|
3587
|
+
"**/tests/**",
|
|
3588
|
+
"**/*.test.{ts,tsx,js,jsx}",
|
|
3589
|
+
"**/*.tests.{ts,tsx,js,jsx}",
|
|
3590
|
+
"**/*.spec.{ts,tsx,js,jsx}",
|
|
3591
|
+
// Sufixo `Test` sem ponto: `expensify` chama os fixtures dele de
|
|
3592
|
+
// `LegalNameStepTest.tsx`, e cada um monta um `<Stack.Screen>` próprio.
|
|
3593
|
+
// Auditando as 144 rotas lidas dele contra `SCREENS.ts`, 2 vinham daqui —
|
|
3594
|
+
// destinos que existem no teste e não no app, para os quais o agente
|
|
3595
|
+
// tentaria navegar.
|
|
3596
|
+
"**/*Test.{ts,tsx,js,jsx}",
|
|
3597
|
+
"**/*Tests.{ts,tsx,js,jsx}",
|
|
2660
3598
|
...this.config.exclude || [],
|
|
2661
3599
|
// §5: Add user-configured navigation excludes
|
|
2662
3600
|
...this.navigationExclude
|
|
2663
3601
|
];
|
|
2664
|
-
const files = await (
|
|
3602
|
+
const files = await globSorted(patterns, {
|
|
2665
3603
|
cwd: this.config.rootDir,
|
|
2666
3604
|
ignore: excludePatterns
|
|
2667
3605
|
});
|
|
2668
3606
|
return files.map((file2) => import_path4.default.join(this.config.rootDir, file2));
|
|
2669
3607
|
}
|
|
2670
|
-
/**
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
(0, import_traverse5.default)(ast, {
|
|
2685
|
-
// Detect createStackNavigator / createTabNavigator / createDrawerNavigator calls
|
|
2686
|
-
CallExpression: (nodePath) => {
|
|
2687
|
-
const { node } = nodePath;
|
|
2688
|
-
const callee = node.callee;
|
|
2689
|
-
let navigatorType = null;
|
|
2690
|
-
if (t5.isIdentifier(callee) && callee.name === "createNativeStackNavigator") {
|
|
2691
|
-
navigatorType = "stack";
|
|
2692
|
-
} else if (t5.isIdentifier(callee) && callee.name === "createStackNavigator") {
|
|
2693
|
-
navigatorType = "stack";
|
|
2694
|
-
} else if (t5.isIdentifier(callee) && callee.name === "createBottomTabNavigator") {
|
|
2695
|
-
navigatorType = "tab";
|
|
2696
|
-
} else if (t5.isIdentifier(callee) && callee.name === "createTabNavigator") {
|
|
2697
|
-
navigatorType = "tab";
|
|
2698
|
-
} else if (t5.isIdentifier(callee) && callee.name === "createDrawerNavigator") {
|
|
2699
|
-
navigatorType = "drawer";
|
|
2700
|
-
}
|
|
2701
|
-
if (navigatorType) {
|
|
2702
|
-
const parent = nodePath.parent;
|
|
2703
|
-
if (t5.isVariableDeclarator(parent) && t5.isIdentifier(parent.id)) {
|
|
2704
|
-
const varName = parent.id.name;
|
|
2705
|
-
navigatorCalls.set(varName, {
|
|
2706
|
-
name: varName,
|
|
2707
|
-
type: navigatorType,
|
|
2708
|
-
screens: []
|
|
2709
|
-
});
|
|
2710
|
-
}
|
|
2711
|
-
}
|
|
2712
|
-
},
|
|
2713
|
-
// Detect Stack.Navigator / Tab.Navigator JSX elements
|
|
2714
|
-
JSXElement: (nodePath) => {
|
|
2715
|
-
const { node } = nodePath;
|
|
2716
|
-
const openingElement = node.openingElement;
|
|
2717
|
-
if (t5.isJSXMemberExpression(openingElement.name) && t5.isJSXIdentifier(openingElement.name.object)) {
|
|
2718
|
-
const objectName = openingElement.name.object.name;
|
|
2719
|
-
const propertyName = t5.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
|
|
2720
|
-
if (propertyName === "Navigator") {
|
|
2721
|
-
const navigator = navigatorCalls.get(objectName);
|
|
2722
|
-
if (navigator) {
|
|
2723
|
-
const initialRouteAttr = openingElement.attributes.find(
|
|
2724
|
-
(attr) => t5.isJSXAttribute(attr) && t5.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
|
|
2725
|
-
);
|
|
2726
|
-
if (t5.isJSXAttribute(initialRouteAttr) && t5.isStringLiteral(initialRouteAttr.value)) {
|
|
2727
|
-
navigator.initialRouteName = initialRouteAttr.value.value;
|
|
2728
|
-
}
|
|
2729
|
-
const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
|
|
2730
|
-
screensByNavigator.set(objectName, screens);
|
|
2731
|
-
}
|
|
2732
|
-
}
|
|
2733
|
-
}
|
|
3608
|
+
/**
|
|
3609
|
+
* Fase 2: o JSX `<X.Navigator>` deste arquivo, ligado à declaração de `X` —
|
|
3610
|
+
* que pode estar aqui ou em qualquer arquivo que este importe.
|
|
3611
|
+
*/
|
|
3612
|
+
parseNavigatorUsages(filePath, declarations) {
|
|
3613
|
+
const ast = this.graph.parse(filePath);
|
|
3614
|
+
if (!ast) return [];
|
|
3615
|
+
const found = /* @__PURE__ */ new Map();
|
|
3616
|
+
(0, import_traverse5.default)(ast, {
|
|
3617
|
+
JSXElement: (nodePath) => {
|
|
3618
|
+
const { node } = nodePath;
|
|
3619
|
+
const openingElement = node.openingElement;
|
|
3620
|
+
if (!t6.isJSXMemberExpression(openingElement.name) || !t6.isJSXIdentifier(openingElement.name.object)) {
|
|
3621
|
+
return;
|
|
2734
3622
|
}
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
3623
|
+
const objectName = openingElement.name.object.name;
|
|
3624
|
+
const propertyName = t6.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
|
|
3625
|
+
if (propertyName !== "Navigator") return;
|
|
3626
|
+
const decl = this.resolveNavigator(filePath, objectName, declarations);
|
|
3627
|
+
if (!decl) return;
|
|
3628
|
+
const navigator = found.get(objectName) ?? {
|
|
3629
|
+
name: objectName,
|
|
3630
|
+
type: decl.type,
|
|
3631
|
+
screens: []
|
|
3632
|
+
};
|
|
3633
|
+
const initialRouteAttr = openingElement.attributes.find(
|
|
3634
|
+
(attr) => t6.isJSXAttribute(attr) && t6.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
|
|
3635
|
+
);
|
|
3636
|
+
if (t6.isJSXAttribute(initialRouteAttr)) {
|
|
3637
|
+
const initial = this.attributeString(filePath, initialRouteAttr);
|
|
3638
|
+
if (initial) navigator.initialRouteName = initial;
|
|
3639
|
+
}
|
|
3640
|
+
this.navigatorFiles.add(filePath);
|
|
3641
|
+
this.navigatorFiles.add(decl.file);
|
|
3642
|
+
navigator.screens = this.extractScreensFromNavigator(filePath, node, objectName, decl.type);
|
|
3643
|
+
found.set(objectName, navigator);
|
|
3644
|
+
}
|
|
3645
|
+
});
|
|
3646
|
+
return [...found.values()];
|
|
3647
|
+
}
|
|
3648
|
+
/**
|
|
3649
|
+
* Rotas declaradas FORA de qualquer `<X.Navigator>`.
|
|
3650
|
+
*
|
|
3651
|
+
* O CASO. A fase 2 desce a partir do `<X.Navigator>` e lê as `<X.Screen>`
|
|
3652
|
+
* que estão DENTRO dele. Dois alvos do corpus não escrevem assim, e entre os
|
|
3653
|
+
* dois são 171 rotas invisíveis:
|
|
3654
|
+
*
|
|
3655
|
+
* `bluesky` — `function commonScreens(Stack: typeof Flat) { return (<>
|
|
3656
|
+
* <Stack.Screen name="NotFound" … /> … </>) }`, chamada de
|
|
3657
|
+
* dentro de seis navegadores diferentes. 70 rotas.
|
|
3658
|
+
* `comapeo` — `export const createAppScreens = ({intl}) => (<>
|
|
3659
|
+
* <RootStack.Group><RootStack.Screen … /></RootStack.Group></>)`,
|
|
3660
|
+
* num arquivo sem `<RootStack.Navigator>` nenhum. 101 rotas.
|
|
3661
|
+
*
|
|
3662
|
+
* A REGRA. Uma `<X.Screen name="…">` sem `<Y.Navigator>` ancestral é rota do
|
|
3663
|
+
* navegador ao qual `X` se resolve. Não há palpite em jogo: o nome da rota é
|
|
3664
|
+
* literal do fonte (ou constante que o resolvedor segue), e `X` precisa
|
|
3665
|
+
* chegar a uma declaração de navegador que já existe. O que não resolve não
|
|
3666
|
+
* vira nada.
|
|
3667
|
+
*
|
|
3668
|
+
* O ancestral é o que evita contar duas vezes — dentro do `<X.Navigator>` a
|
|
3669
|
+
* fase 2 já leu, e somar aqui duplicaria cada rota do corpus inteiro.
|
|
3670
|
+
*
|
|
3671
|
+
* DUAS FORMAS DE RESOLVER `X`, e a segunda é o que o `bluesky` exige. A
|
|
3672
|
+
* primeira é a de sempre (declaração local, ou `import` seguido até a
|
|
3673
|
+
* origem) e resolve o `comapeo`. A segunda lê a ANOTAÇÃO DE TIPO do
|
|
3674
|
+
* parâmetro: em `commonScreens(Stack: typeof Flat)`, quem diz que `Stack` é
|
|
3675
|
+
* o navegador `Flat` é o próprio app, no fonte.
|
|
3676
|
+
*
|
|
3677
|
+
* O QUE ISTO NÃO FAZ. As 70 do `bluesky` ficam atribuídas a `Flat` — que as
|
|
3678
|
+
* contém de fato (`{commonScreens(Flat, numUnread)}`) — e não aos outros
|
|
3679
|
+
* cinco stacks que também chamam a mesma função. Seguir os seis pontos de
|
|
3680
|
+
* chamada é análise interprocedural, e o ganho seria só de atribuição: o nome
|
|
3681
|
+
* da rota e o arquivo da tela, que é o que o agente usa, já saem certos.
|
|
3682
|
+
*/
|
|
3683
|
+
parseDetachedScreens(filePath, declarations) {
|
|
3684
|
+
const ast = this.graph.parse(filePath);
|
|
3685
|
+
if (!ast) return [];
|
|
3686
|
+
const byNavigator = /* @__PURE__ */ new Map();
|
|
3687
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
3688
|
+
(0, import_traverse5.default)(ast, {
|
|
3689
|
+
JSXElement: (nodePath) => {
|
|
3690
|
+
const name = nodePath.node.openingElement.name;
|
|
3691
|
+
if (!t6.isJSXMemberExpression(name) || !t6.isJSXIdentifier(name.object)) return;
|
|
3692
|
+
if (!t6.isJSXIdentifier(name.property) || name.property.name !== "Screen") return;
|
|
3693
|
+
const insideNavigator = nodePath.findParent((parent) => {
|
|
3694
|
+
if (!parent.isJSXElement()) return false;
|
|
3695
|
+
const parentName = parent.node.openingElement.name;
|
|
3696
|
+
return t6.isJSXMemberExpression(parentName) && t6.isJSXIdentifier(parentName.property) && parentName.property.name === "Navigator";
|
|
3697
|
+
});
|
|
3698
|
+
if (insideNavigator) return;
|
|
3699
|
+
const local = name.object.name;
|
|
3700
|
+
if (!resolved.has(local)) {
|
|
3701
|
+
resolved.set(
|
|
3702
|
+
local,
|
|
3703
|
+
this.resolveDetachedNavigator(filePath, local, nodePath, declarations)
|
|
3704
|
+
);
|
|
3705
|
+
}
|
|
3706
|
+
const decl = resolved.get(local);
|
|
3707
|
+
if (!decl) return;
|
|
3708
|
+
const screen = this.parseScreenElement(filePath, nodePath.node, decl.name, decl.type);
|
|
3709
|
+
if (!screen) return;
|
|
3710
|
+
const nav = byNavigator.get(decl.name) ?? {
|
|
3711
|
+
name: decl.name,
|
|
3712
|
+
type: decl.type,
|
|
3713
|
+
screens: []
|
|
3714
|
+
};
|
|
3715
|
+
if (!nav.screens.some((existing) => existing.name === screen.name))
|
|
3716
|
+
nav.screens.push(screen);
|
|
3717
|
+
byNavigator.set(decl.name, nav);
|
|
3718
|
+
}
|
|
3719
|
+
});
|
|
3720
|
+
return [...byNavigator.values()];
|
|
3721
|
+
}
|
|
3722
|
+
/**
|
|
3723
|
+
* De um `X` usado como `<X.Screen>` até a declaração do navegador.
|
|
3724
|
+
*
|
|
3725
|
+
* Além do caminho normal — declaração local ou `import` seguido até a origem
|
|
3726
|
+
* — aceita `X` como PARÂMETRO anotado com `typeof Y`. É o idioma do
|
|
3727
|
+
* `bluesky`, e a anotação é declaração do app: nada aqui é inferido do nome.
|
|
3728
|
+
*/
|
|
3729
|
+
resolveDetachedNavigator(filePath, localName, nodePath, declarations) {
|
|
3730
|
+
const direct = this.resolveNavigator(filePath, localName, declarations);
|
|
3731
|
+
if (direct) return direct;
|
|
3732
|
+
const binding = nodePath.scope.getBinding(localName);
|
|
3733
|
+
if (!binding || binding.kind !== "param") return null;
|
|
3734
|
+
const param = binding.path.node;
|
|
3735
|
+
if (!t6.isIdentifier(param) || !t6.isTSTypeAnnotation(param.typeAnnotation)) return null;
|
|
3736
|
+
const annotation = param.typeAnnotation.typeAnnotation;
|
|
3737
|
+
if (!t6.isTSTypeQuery(annotation) || !t6.isIdentifier(annotation.exprName)) return null;
|
|
3738
|
+
return this.resolveNavigator(filePath, annotation.exprName.name, declarations);
|
|
3739
|
+
}
|
|
3740
|
+
/**
|
|
3741
|
+
* De `<X.Navigator>` até a declaração de `X`.
|
|
3742
|
+
*
|
|
3743
|
+
* Primeiro no próprio arquivo; se não estiver, segue o `import` até onde `X`
|
|
3744
|
+
* é definido. Resolver pelo import é o que torna a junção entre arquivos
|
|
3745
|
+
* segura: dois `Stack` de arquivos diferentes nunca colidem, porque a chave
|
|
3746
|
+
* é o arquivo de DEFINIÇÃO.
|
|
3747
|
+
*/
|
|
3748
|
+
resolveNavigator(filePath, localName, declarations) {
|
|
3749
|
+
const local = declarations.get(`${filePath}#${localName}`);
|
|
3750
|
+
if (local) return local;
|
|
3751
|
+
const origin = this.graph.resolveBinding(filePath, localName);
|
|
3752
|
+
if (!origin) return null;
|
|
3753
|
+
return declarations.get(`${origin.file}#${origin.name}`) ?? null;
|
|
3754
|
+
}
|
|
3755
|
+
/** O valor string de um atributo JSX, resolvendo constante importada. */
|
|
3756
|
+
attributeString(filePath, attr) {
|
|
3757
|
+
const value = attr.value;
|
|
3758
|
+
if (t6.isStringLiteral(value)) return value.value;
|
|
3759
|
+
if (t6.isJSXExpressionContainer(value) && t6.isExpression(value.expression)) {
|
|
3760
|
+
return this.graph.stringConstant(filePath, value.expression);
|
|
2743
3761
|
}
|
|
2744
|
-
return
|
|
3762
|
+
return null;
|
|
2745
3763
|
}
|
|
2746
3764
|
/**
|
|
2747
3765
|
* Is this JSX element `<navigatorVarName.MEMBER …>`?
|
|
@@ -2752,9 +3770,9 @@ var NavigationAnalyzer = class {
|
|
|
2752
3770
|
// `never`. It is a question about the member name, not about the node
|
|
2753
3771
|
// kind.
|
|
2754
3772
|
isNavigatorMember(node, navigatorVarName, member) {
|
|
2755
|
-
if (!
|
|
3773
|
+
if (!t6.isJSXElement(node)) return false;
|
|
2756
3774
|
const name = node.openingElement.name;
|
|
2757
|
-
return
|
|
3775
|
+
return t6.isJSXMemberExpression(name) && t6.isJSXIdentifier(name.object) && name.object.name === navigatorVarName && t6.isJSXIdentifier(name.property) && name.property.name === member;
|
|
2758
3776
|
}
|
|
2759
3777
|
/**
|
|
2760
3778
|
* Flatten a navigator's JSX children into the `<X.Screen>` elements they
|
|
@@ -2785,37 +3803,37 @@ var NavigationAnalyzer = class {
|
|
|
2785
3803
|
found.push(...this.collectScreenElements(group.children, navigatorVarName, depth + 1));
|
|
2786
3804
|
return;
|
|
2787
3805
|
}
|
|
2788
|
-
if (
|
|
3806
|
+
if (t6.isJSXElement(node)) {
|
|
2789
3807
|
const name = node.openingElement.name;
|
|
2790
|
-
const isForeignNavigator =
|
|
3808
|
+
const isForeignNavigator = t6.isJSXMemberExpression(name) && t6.isJSXIdentifier(name.property) && name.property.name === "Navigator";
|
|
2791
3809
|
if (isForeignNavigator) return;
|
|
2792
3810
|
found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
|
|
2793
3811
|
return;
|
|
2794
3812
|
}
|
|
2795
|
-
if (
|
|
3813
|
+
if (t6.isJSXFragment(node)) {
|
|
2796
3814
|
found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
|
|
2797
3815
|
return;
|
|
2798
3816
|
}
|
|
2799
|
-
if (
|
|
3817
|
+
if (t6.isJSXExpressionContainer(node)) {
|
|
2800
3818
|
visit(node.expression);
|
|
2801
3819
|
return;
|
|
2802
3820
|
}
|
|
2803
|
-
if (
|
|
3821
|
+
if (t6.isConditionalExpression(node)) {
|
|
2804
3822
|
visit(node.consequent);
|
|
2805
3823
|
visit(node.alternate);
|
|
2806
3824
|
return;
|
|
2807
3825
|
}
|
|
2808
|
-
if (
|
|
3826
|
+
if (t6.isLogicalExpression(node)) {
|
|
2809
3827
|
visit(node.left);
|
|
2810
3828
|
visit(node.right);
|
|
2811
3829
|
return;
|
|
2812
3830
|
}
|
|
2813
|
-
if (
|
|
3831
|
+
if (t6.isCallExpression(node)) {
|
|
2814
3832
|
for (const arg of node.arguments) {
|
|
2815
|
-
if (
|
|
2816
|
-
if (
|
|
3833
|
+
if (t6.isArrowFunctionExpression(arg) || t6.isFunctionExpression(arg)) {
|
|
3834
|
+
if (t6.isBlockStatement(arg.body)) {
|
|
2817
3835
|
for (const stmt of arg.body.body) {
|
|
2818
|
-
if (
|
|
3836
|
+
if (t6.isReturnStatement(stmt)) visit(stmt.argument);
|
|
2819
3837
|
}
|
|
2820
3838
|
} else {
|
|
2821
3839
|
visit(arg.body);
|
|
@@ -2824,11 +3842,11 @@ var NavigationAnalyzer = class {
|
|
|
2824
3842
|
}
|
|
2825
3843
|
return;
|
|
2826
3844
|
}
|
|
2827
|
-
if (
|
|
3845
|
+
if (t6.isArrayExpression(node)) {
|
|
2828
3846
|
for (const el of node.elements) visit(el);
|
|
2829
3847
|
return;
|
|
2830
3848
|
}
|
|
2831
|
-
if (
|
|
3849
|
+
if (t6.isTSAsExpression(node) || t6.isTSNonNullExpression(node)) {
|
|
2832
3850
|
visit(node.expression);
|
|
2833
3851
|
}
|
|
2834
3852
|
};
|
|
@@ -2836,28 +3854,54 @@ var NavigationAnalyzer = class {
|
|
|
2836
3854
|
return found;
|
|
2837
3855
|
}
|
|
2838
3856
|
/** Extract screens from a navigator JSX element */
|
|
2839
|
-
extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
|
|
3857
|
+
extractScreensFromNavigator(filePath, navigatorElement, navigatorVarName, navigatorType) {
|
|
2840
3858
|
const screens = [];
|
|
2841
3859
|
if (!navigatorElement.children) return screens;
|
|
2842
3860
|
const seen = /* @__PURE__ */ new Set();
|
|
2843
3861
|
for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
|
|
2844
|
-
const
|
|
2845
|
-
if (!
|
|
2846
|
-
seen.add(
|
|
2847
|
-
screens.push(
|
|
2848
|
-
name: screenName,
|
|
2849
|
-
navigatorName: navigatorVarName,
|
|
2850
|
-
navigatorType
|
|
2851
|
-
});
|
|
3862
|
+
const screen = this.parseScreenElement(filePath, element, navigatorVarName, navigatorType);
|
|
3863
|
+
if (!screen || seen.has(screen.name)) continue;
|
|
3864
|
+
seen.add(screen.name);
|
|
3865
|
+
screens.push(screen);
|
|
2852
3866
|
}
|
|
2853
3867
|
return screens;
|
|
2854
3868
|
}
|
|
3869
|
+
/**
|
|
3870
|
+
* Uma `<X.Screen>` isolada até a rota que ela declara.
|
|
3871
|
+
*
|
|
3872
|
+
* Separado de `extractScreensFromNavigator` porque a MESMA leitura serve para
|
|
3873
|
+
* a `<X.Screen>` que mora dentro do `<X.Navigator>` e para a que mora fora
|
|
3874
|
+
* dele — só a forma de chegar até o elemento muda.
|
|
3875
|
+
*/
|
|
3876
|
+
parseScreenElement(filePath, element, navigatorName, navigatorType) {
|
|
3877
|
+
const attrs = element.openingElement.attributes;
|
|
3878
|
+
const nameAttr = attrs.find(
|
|
3879
|
+
(a) => t6.isJSXAttribute(a) && t6.isJSXIdentifier(a.name) && a.name.name === "name"
|
|
3880
|
+
);
|
|
3881
|
+
const screenName = t6.isJSXAttribute(nameAttr) ? this.attributeString(filePath, nameAttr) : null;
|
|
3882
|
+
if (!screenName) return null;
|
|
3883
|
+
const componentAttr = attrs.find(
|
|
3884
|
+
(a) => t6.isJSXAttribute(a) && t6.isJSXIdentifier(a.name) && a.name.name === "component"
|
|
3885
|
+
);
|
|
3886
|
+
let componentFile = null;
|
|
3887
|
+
if (t6.isJSXAttribute(componentAttr) && t6.isJSXExpressionContainer(componentAttr.value)) {
|
|
3888
|
+
const expr = componentAttr.value.expression;
|
|
3889
|
+
if (t6.isExpression(expr)) componentFile = this.graph.componentFile(filePath, expr);
|
|
3890
|
+
}
|
|
3891
|
+
if (componentFile) this.routeTargets.set(screenName, componentFile);
|
|
3892
|
+
return {
|
|
3893
|
+
name: screenName,
|
|
3894
|
+
navigatorName,
|
|
3895
|
+
navigatorType,
|
|
3896
|
+
...componentFile ? { componentFile } : {}
|
|
3897
|
+
};
|
|
3898
|
+
}
|
|
2855
3899
|
/** Extract string attribute value from JSX attributes */
|
|
2856
3900
|
extractAttributeValue(attributes, attrName) {
|
|
2857
3901
|
const attr = attributes.find(
|
|
2858
|
-
(a) =>
|
|
3902
|
+
(a) => t6.isJSXAttribute(a) && t6.isJSXIdentifier(a.name) && a.name.name === attrName
|
|
2859
3903
|
);
|
|
2860
|
-
if (
|
|
3904
|
+
if (t6.isJSXAttribute(attr) && t6.isStringLiteral(attr.value)) {
|
|
2861
3905
|
return attr.value.value;
|
|
2862
3906
|
}
|
|
2863
3907
|
return null;
|
|
@@ -2866,7 +3910,7 @@ var NavigationAnalyzer = class {
|
|
|
2866
3910
|
parseParamTypes(content, filePath) {
|
|
2867
3911
|
const types = [];
|
|
2868
3912
|
try {
|
|
2869
|
-
const ast =
|
|
3913
|
+
const ast = parser2.parse(content, {
|
|
2870
3914
|
sourceType: "module",
|
|
2871
3915
|
plugins: [
|
|
2872
3916
|
"jsx",
|
|
@@ -2947,7 +3991,7 @@ var NavigationAnalyzer = class {
|
|
|
2947
3991
|
if (type.type === "TSUndefinedKeyword") return "undefined";
|
|
2948
3992
|
if (type.type === "TSNullKeyword") return "null";
|
|
2949
3993
|
if (type.type === "TSUnionType") {
|
|
2950
|
-
return type.types.map((
|
|
3994
|
+
return type.types.map((t16) => this.typeToString(t16)).join(" | ");
|
|
2951
3995
|
}
|
|
2952
3996
|
if (type.type === "TSTypeLiteral") {
|
|
2953
3997
|
return "object";
|
|
@@ -2968,7 +4012,7 @@ var NavigationAnalyzer = class {
|
|
|
2968
4012
|
/** Attach parsed type params to navigator screens */
|
|
2969
4013
|
attachParamsToNavigators(navigators, types) {
|
|
2970
4014
|
for (const navigator of navigators) {
|
|
2971
|
-
const matchingType = types.find((
|
|
4015
|
+
const matchingType = types.find((t16) => t16.type === navigator.type);
|
|
2972
4016
|
if (matchingType) {
|
|
2973
4017
|
for (const screen of navigator.screens) {
|
|
2974
4018
|
const screenParams = matchingType.paramEntries.get(screen.name);
|
|
@@ -3046,9 +4090,9 @@ var NavigationAnalyzer = class {
|
|
|
3046
4090
|
|
|
3047
4091
|
// src/analyzers/ComponentAnalyzer.ts
|
|
3048
4092
|
var import_fs5 = require("fs");
|
|
3049
|
-
var
|
|
4093
|
+
var parser3 = __toESM(require("@babel/parser"));
|
|
3050
4094
|
var import_traverse6 = __toESM(require("@babel/traverse"));
|
|
3051
|
-
var
|
|
4095
|
+
var t7 = __toESM(require("@babel/types"));
|
|
3052
4096
|
var ComponentAnalyzer = class {
|
|
3053
4097
|
config;
|
|
3054
4098
|
constructor(config) {
|
|
@@ -3058,14 +4102,14 @@ var ComponentAnalyzer = class {
|
|
|
3058
4102
|
async analyzeFile(filePath) {
|
|
3059
4103
|
try {
|
|
3060
4104
|
const code = (0, import_fs5.readFileSync)(filePath, "utf-8");
|
|
3061
|
-
const ast =
|
|
4105
|
+
const ast = parser3.parse(code, {
|
|
3062
4106
|
sourceType: "module",
|
|
3063
4107
|
plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
|
|
3064
4108
|
});
|
|
3065
4109
|
const components = [];
|
|
3066
4110
|
(0, import_traverse6.default)(ast, {
|
|
3067
|
-
JSXElement: (
|
|
3068
|
-
const component = this.extractComponentFromJSXElement(
|
|
4111
|
+
JSXElement: (path13) => {
|
|
4112
|
+
const component = this.extractComponentFromJSXElement(path13.node);
|
|
3069
4113
|
if (component) {
|
|
3070
4114
|
components.push(component);
|
|
3071
4115
|
}
|
|
@@ -3094,19 +4138,19 @@ var ComponentAnalyzer = class {
|
|
|
3094
4138
|
};
|
|
3095
4139
|
}
|
|
3096
4140
|
getElementName(openingElement) {
|
|
3097
|
-
if (
|
|
4141
|
+
if (t7.isJSXIdentifier(openingElement.name)) {
|
|
3098
4142
|
return openingElement.name.name;
|
|
3099
4143
|
}
|
|
3100
|
-
if (
|
|
4144
|
+
if (t7.isJSXMemberExpression(openingElement.name)) {
|
|
3101
4145
|
const parts = [];
|
|
3102
4146
|
let current = openingElement.name;
|
|
3103
|
-
while (
|
|
3104
|
-
if (
|
|
4147
|
+
while (t7.isJSXMemberExpression(current)) {
|
|
4148
|
+
if (t7.isJSXIdentifier(current.property)) {
|
|
3105
4149
|
parts.unshift(current.property.name);
|
|
3106
4150
|
}
|
|
3107
4151
|
current = current.object;
|
|
3108
4152
|
}
|
|
3109
|
-
if (
|
|
4153
|
+
if (t7.isJSXIdentifier(current)) {
|
|
3110
4154
|
parts.unshift(current.name);
|
|
3111
4155
|
}
|
|
3112
4156
|
return parts.join(".");
|
|
@@ -3129,13 +4173,13 @@ var ComponentAnalyzer = class {
|
|
|
3129
4173
|
extractProps(openingElement) {
|
|
3130
4174
|
const props = {};
|
|
3131
4175
|
for (const attr of openingElement.attributes) {
|
|
3132
|
-
if (
|
|
4176
|
+
if (t7.isJSXAttribute(attr) && t7.isJSXIdentifier(attr.name)) {
|
|
3133
4177
|
const propName = attr.name.name;
|
|
3134
4178
|
if (attr.value === null) {
|
|
3135
4179
|
props[propName] = "true";
|
|
3136
|
-
} else if (
|
|
4180
|
+
} else if (t7.isStringLiteral(attr.value)) {
|
|
3137
4181
|
props[propName] = attr.value.value;
|
|
3138
|
-
} else if (
|
|
4182
|
+
} else if (t7.isJSXExpressionContainer(attr.value) && t7.isStringLiteral(attr.value.expression)) {
|
|
3139
4183
|
props[propName] = attr.value.expression.value;
|
|
3140
4184
|
}
|
|
3141
4185
|
}
|
|
@@ -3145,12 +4189,12 @@ var ComponentAnalyzer = class {
|
|
|
3145
4189
|
extractAccessibilityProps(openingElement) {
|
|
3146
4190
|
const result = {};
|
|
3147
4191
|
for (const attr of openingElement.attributes) {
|
|
3148
|
-
if (
|
|
4192
|
+
if (t7.isJSXAttribute(attr) && t7.isJSXIdentifier(attr.name)) {
|
|
3149
4193
|
const propName = attr.name.name;
|
|
3150
4194
|
if ((propName === "testID" || propName === "accessibilityLabel") && attr.value) {
|
|
3151
|
-
if (
|
|
4195
|
+
if (t7.isStringLiteral(attr.value)) {
|
|
3152
4196
|
result[propName] = attr.value.value;
|
|
3153
|
-
} else if (
|
|
4197
|
+
} else if (t7.isJSXExpressionContainer(attr.value) && t7.isStringLiteral(attr.value.expression)) {
|
|
3154
4198
|
result[propName] = attr.value.expression.value;
|
|
3155
4199
|
}
|
|
3156
4200
|
}
|
|
@@ -3162,7 +4206,7 @@ var ComponentAnalyzer = class {
|
|
|
3162
4206
|
if (depth >= maxDepth) return [];
|
|
3163
4207
|
const extracted = [];
|
|
3164
4208
|
for (const child of children) {
|
|
3165
|
-
if (
|
|
4209
|
+
if (t7.isJSXElement(child)) {
|
|
3166
4210
|
const component = this.extractComponentFromJSXElement(child);
|
|
3167
4211
|
if (component) {
|
|
3168
4212
|
extracted.push(component);
|
|
@@ -3175,10 +4219,10 @@ var ComponentAnalyzer = class {
|
|
|
3175
4219
|
|
|
3176
4220
|
// src/analyzers/FormAnalyzer.ts
|
|
3177
4221
|
var import_fs6 = require("fs");
|
|
3178
|
-
var
|
|
4222
|
+
var parser4 = __toESM(require("@babel/parser"));
|
|
3179
4223
|
var import_traverse7 = __toESM(require("@babel/traverse"));
|
|
3180
|
-
var
|
|
3181
|
-
var
|
|
4224
|
+
var t8 = __toESM(require("@babel/types"));
|
|
4225
|
+
var path5 = __toESM(require("path"));
|
|
3182
4226
|
var FormAnalyzer = class {
|
|
3183
4227
|
config;
|
|
3184
4228
|
stateVariables = /* @__PURE__ */ new Map();
|
|
@@ -3191,7 +4235,7 @@ var FormAnalyzer = class {
|
|
|
3191
4235
|
async analyzeFile(filePath) {
|
|
3192
4236
|
try {
|
|
3193
4237
|
const code = (0, import_fs6.readFileSync)(filePath, "utf-8");
|
|
3194
|
-
const ast =
|
|
4238
|
+
const ast = parser4.parse(code, {
|
|
3195
4239
|
sourceType: "module",
|
|
3196
4240
|
plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
|
|
3197
4241
|
});
|
|
@@ -3199,13 +4243,13 @@ var FormAnalyzer = class {
|
|
|
3199
4243
|
this.inputElements = [];
|
|
3200
4244
|
this.submitButtons = [];
|
|
3201
4245
|
(0, import_traverse7.default)(ast, {
|
|
3202
|
-
CallExpression: (
|
|
3203
|
-
this.extractStateVariables(
|
|
4246
|
+
CallExpression: (path13) => {
|
|
4247
|
+
this.extractStateVariables(path13.node);
|
|
3204
4248
|
}
|
|
3205
4249
|
});
|
|
3206
4250
|
(0, import_traverse7.default)(ast, {
|
|
3207
|
-
JSXElement: (
|
|
3208
|
-
this.extractFormElements(
|
|
4251
|
+
JSXElement: (path13) => {
|
|
4252
|
+
this.extractFormElements(path13.node);
|
|
3209
4253
|
}
|
|
3210
4254
|
});
|
|
3211
4255
|
const validationRules = this.extractValidationRules(ast);
|
|
@@ -3216,7 +4260,7 @@ var FormAnalyzer = class {
|
|
|
3216
4260
|
}
|
|
3217
4261
|
}
|
|
3218
4262
|
extractStateVariables(node) {
|
|
3219
|
-
if (
|
|
4263
|
+
if (t8.isIdentifier(node.callee) && node.callee.name === "useState" && node.arguments.length > 0) {
|
|
3220
4264
|
return;
|
|
3221
4265
|
}
|
|
3222
4266
|
}
|
|
@@ -3237,7 +4281,7 @@ var FormAnalyzer = class {
|
|
|
3237
4281
|
extractInputInfo(openingElement) {
|
|
3238
4282
|
const info2 = { varName: "" };
|
|
3239
4283
|
for (const attr of openingElement.attributes) {
|
|
3240
|
-
if (
|
|
4284
|
+
if (t8.isJSXAttribute(attr) && t8.isJSXIdentifier(attr.name)) {
|
|
3241
4285
|
const propName = attr.name.name;
|
|
3242
4286
|
const propValue = this.extractAttributeValue(attr.value);
|
|
3243
4287
|
switch (propName) {
|
|
@@ -3258,7 +4302,7 @@ var FormAnalyzer = class {
|
|
|
3258
4302
|
if (propValue) info2.varName = propValue;
|
|
3259
4303
|
break;
|
|
3260
4304
|
case "value":
|
|
3261
|
-
if (attr.value &&
|
|
4305
|
+
if (attr.value && t8.isJSXExpressionContainer(attr.value) && t8.isIdentifier(attr.value.expression)) {
|
|
3262
4306
|
info2.varName = attr.value.expression.name;
|
|
3263
4307
|
}
|
|
3264
4308
|
break;
|
|
@@ -3270,16 +4314,16 @@ var FormAnalyzer = class {
|
|
|
3270
4314
|
extractButtonInfo(openingElement) {
|
|
3271
4315
|
const info2 = {};
|
|
3272
4316
|
for (const attr of openingElement.attributes) {
|
|
3273
|
-
if (
|
|
4317
|
+
if (t8.isJSXAttribute(attr) && t8.isJSXIdentifier(attr.name)) {
|
|
3274
4318
|
const propName = attr.name.name;
|
|
3275
4319
|
if (propName === "title" || propName === "label") {
|
|
3276
4320
|
info2.label = this.extractAttributeValue(attr.value);
|
|
3277
4321
|
}
|
|
3278
4322
|
if (propName === "onPress") {
|
|
3279
|
-
if (attr.value &&
|
|
3280
|
-
if (
|
|
4323
|
+
if (attr.value && t8.isJSXExpressionContainer(attr.value)) {
|
|
4324
|
+
if (t8.isIdentifier(attr.value.expression)) {
|
|
3281
4325
|
info2.handler = attr.value.expression.name;
|
|
3282
|
-
} else if (
|
|
4326
|
+
} else if (t8.isArrowFunctionExpression(attr.value.expression) || t8.isFunctionExpression(attr.value.expression)) {
|
|
3283
4327
|
info2.handler = "anonymous";
|
|
3284
4328
|
}
|
|
3285
4329
|
}
|
|
@@ -3290,14 +4334,14 @@ var FormAnalyzer = class {
|
|
|
3290
4334
|
}
|
|
3291
4335
|
extractAttributeValue(value) {
|
|
3292
4336
|
if (value === null) return void 0;
|
|
3293
|
-
if (
|
|
3294
|
-
if (
|
|
4337
|
+
if (t8.isStringLiteral(value)) return value.value;
|
|
4338
|
+
if (t8.isJSXExpressionContainer(value) && t8.isStringLiteral(value.expression)) {
|
|
3295
4339
|
return value.expression.value;
|
|
3296
4340
|
}
|
|
3297
4341
|
return void 0;
|
|
3298
4342
|
}
|
|
3299
4343
|
getElementName(openingElement) {
|
|
3300
|
-
if (
|
|
4344
|
+
if (t8.isJSXIdentifier(openingElement.name)) {
|
|
3301
4345
|
return openingElement.name.name;
|
|
3302
4346
|
}
|
|
3303
4347
|
return null;
|
|
@@ -3305,8 +4349,8 @@ var FormAnalyzer = class {
|
|
|
3305
4349
|
extractValidationRules(ast) {
|
|
3306
4350
|
const rules = {};
|
|
3307
4351
|
(0, import_traverse7.default)(ast, {
|
|
3308
|
-
IfStatement: (
|
|
3309
|
-
const test =
|
|
4352
|
+
IfStatement: (path13) => {
|
|
4353
|
+
const test = path13.node.test;
|
|
3310
4354
|
const rule = this.extractRuleFromCondition(test);
|
|
3311
4355
|
if (rule) {
|
|
3312
4356
|
const { field, description } = rule;
|
|
@@ -3321,36 +4365,36 @@ var FormAnalyzer = class {
|
|
|
3321
4365
|
extractRuleFromCondition(test) {
|
|
3322
4366
|
let fieldName = "";
|
|
3323
4367
|
let description = "";
|
|
3324
|
-
if (
|
|
3325
|
-
if (
|
|
4368
|
+
if (t8.isUnaryExpression(test) && test.operator === "!") {
|
|
4369
|
+
if (t8.isCallExpression(test.argument)) {
|
|
3326
4370
|
const callExpr = test.argument;
|
|
3327
|
-
if (
|
|
4371
|
+
if (t8.isMemberExpression(callExpr.callee)) {
|
|
3328
4372
|
const memberExpr = callExpr.callee;
|
|
3329
|
-
if (
|
|
4373
|
+
if (t8.isIdentifier(memberExpr.object)) {
|
|
3330
4374
|
fieldName = memberExpr.object.name;
|
|
3331
4375
|
description = `${fieldName} is required`;
|
|
3332
4376
|
}
|
|
3333
4377
|
}
|
|
3334
|
-
} else if (
|
|
4378
|
+
} else if (t8.isIdentifier(test.argument)) {
|
|
3335
4379
|
fieldName = test.argument.name;
|
|
3336
4380
|
description = `${fieldName} is required`;
|
|
3337
4381
|
}
|
|
3338
4382
|
}
|
|
3339
|
-
if (
|
|
3340
|
-
if (
|
|
4383
|
+
if (t8.isBinaryExpression(test) && (test.operator === "<" || test.operator === "<=")) {
|
|
4384
|
+
if (t8.isMemberExpression(test.left)) {
|
|
3341
4385
|
const memberExpr = test.left;
|
|
3342
|
-
if (
|
|
4386
|
+
if (t8.isIdentifier(memberExpr.object)) {
|
|
3343
4387
|
fieldName = memberExpr.object.name;
|
|
3344
|
-
if (
|
|
4388
|
+
if (t8.isNumericLiteral(test.right)) {
|
|
3345
4389
|
description = `${fieldName} must be at least ${test.right.value} characters`;
|
|
3346
4390
|
}
|
|
3347
4391
|
}
|
|
3348
4392
|
}
|
|
3349
4393
|
}
|
|
3350
|
-
if (
|
|
3351
|
-
if (
|
|
3352
|
-
const methodName =
|
|
3353
|
-
if (methodName === "includes" &&
|
|
4394
|
+
if (t8.isUnaryExpression(test) && test.operator === "!") {
|
|
4395
|
+
if (t8.isCallExpression(test.argument) && t8.isMemberExpression(test.argument.callee)) {
|
|
4396
|
+
const methodName = t8.isIdentifier(test.argument.callee.property) ? test.argument.callee.property.name : null;
|
|
4397
|
+
if (methodName === "includes" && t8.isIdentifier(test.argument.callee.object)) {
|
|
3354
4398
|
fieldName = test.argument.callee.object.name;
|
|
3355
4399
|
description = `${fieldName} format is invalid`;
|
|
3356
4400
|
}
|
|
@@ -3360,7 +4404,7 @@ var FormAnalyzer = class {
|
|
|
3360
4404
|
}
|
|
3361
4405
|
buildForms(filePath, validationRules) {
|
|
3362
4406
|
if (this.inputElements.length === 0) return [];
|
|
3363
|
-
const fileName =
|
|
4407
|
+
const fileName = path5.basename(filePath, path5.extname(filePath));
|
|
3364
4408
|
const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
|
|
3365
4409
|
const fields = this.inputElements.map((input) => {
|
|
3366
4410
|
const fieldType = this.inferFieldType(input);
|
|
@@ -3410,14 +4454,104 @@ var FormAnalyzer = class {
|
|
|
3410
4454
|
}
|
|
3411
4455
|
};
|
|
3412
4456
|
|
|
4457
|
+
// src/pipeline/composition.ts
|
|
4458
|
+
var MAX_DEPTH = 3;
|
|
4459
|
+
var MAX_VISITED_PER_SCREEN = 60;
|
|
4460
|
+
var MAX_FAN_IN = 3;
|
|
4461
|
+
var ADDRESSABLE = /* @__PURE__ */ new Set(["testID", "appilotsId", "accessibilityLabel", "label"]);
|
|
4462
|
+
function descendants(graph, from, screenFiles) {
|
|
4463
|
+
const seen = /* @__PURE__ */ new Set([from]);
|
|
4464
|
+
const out = [];
|
|
4465
|
+
let frontier = [from];
|
|
4466
|
+
for (let depth = 0; depth < MAX_DEPTH && frontier.length > 0; depth++) {
|
|
4467
|
+
const next = [];
|
|
4468
|
+
for (const file2 of frontier) {
|
|
4469
|
+
for (const child of graph.renderedComponentFiles(file2)) {
|
|
4470
|
+
if (seen.has(child)) continue;
|
|
4471
|
+
seen.add(child);
|
|
4472
|
+
if (screenFiles.has(child)) continue;
|
|
4473
|
+
out.push(child);
|
|
4474
|
+
next.push(child);
|
|
4475
|
+
if (out.length >= MAX_VISITED_PER_SCREEN) return out;
|
|
4476
|
+
}
|
|
4477
|
+
}
|
|
4478
|
+
frontier = next;
|
|
4479
|
+
}
|
|
4480
|
+
return out;
|
|
4481
|
+
}
|
|
4482
|
+
function addressable(locator) {
|
|
4483
|
+
if (!locator || typeof locator !== "object") return false;
|
|
4484
|
+
const l = locator;
|
|
4485
|
+
if (typeof l["source"] === "string" && ADDRESSABLE.has(l["source"])) return true;
|
|
4486
|
+
return typeof l["testID"] === "string" && l["testID"].length > 0;
|
|
4487
|
+
}
|
|
4488
|
+
async function composeAffordances(options) {
|
|
4489
|
+
const { screens, graph, analyzeFile, screenFiles } = options;
|
|
4490
|
+
const byScreen = /* @__PURE__ */ new Map();
|
|
4491
|
+
const fanIn = /* @__PURE__ */ new Map();
|
|
4492
|
+
for (const screen of screens) {
|
|
4493
|
+
if (!screen.filePath) continue;
|
|
4494
|
+
const children = descendants(graph, screen.filePath, screenFiles);
|
|
4495
|
+
byScreen.set(screen, children);
|
|
4496
|
+
for (const child of children) fanIn.set(child, (fanIn.get(child) ?? 0) + 1);
|
|
4497
|
+
}
|
|
4498
|
+
const analyzed = /* @__PURE__ */ new Map();
|
|
4499
|
+
let screensEnriched = 0;
|
|
4500
|
+
let filesMerged = 0;
|
|
4501
|
+
for (const [screen, children] of byScreen) {
|
|
4502
|
+
const exclusive = children.filter((c) => (fanIn.get(c) ?? 0) <= MAX_FAN_IN);
|
|
4503
|
+
if (exclusive.length === 0) continue;
|
|
4504
|
+
const actionIds = new Set(screen.actions.map((a) => a.id));
|
|
4505
|
+
const targetIds = new Set((screen.targets ?? []).map((t16) => t16.id));
|
|
4506
|
+
const formIds = new Set(screen.forms.map((f) => f.id));
|
|
4507
|
+
let gained = false;
|
|
4508
|
+
for (const file2 of exclusive) {
|
|
4509
|
+
if (!analyzed.has(file2)) {
|
|
4510
|
+
try {
|
|
4511
|
+
analyzed.set(file2, await analyzeFile(file2));
|
|
4512
|
+
} catch {
|
|
4513
|
+
analyzed.set(file2, null);
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
const child = analyzed.get(file2);
|
|
4517
|
+
if (!child) continue;
|
|
4518
|
+
let merged = false;
|
|
4519
|
+
for (const action of child.actions) {
|
|
4520
|
+
if (!addressable(action.locator)) continue;
|
|
4521
|
+
if (actionIds.has(action.id)) continue;
|
|
4522
|
+
actionIds.add(action.id);
|
|
4523
|
+
screen.actions.push(action);
|
|
4524
|
+
merged = true;
|
|
4525
|
+
}
|
|
4526
|
+
for (const target of child.targets ?? []) {
|
|
4527
|
+
if (!addressable(target.locator)) continue;
|
|
4528
|
+
if (targetIds.has(target.id)) continue;
|
|
4529
|
+
targetIds.add(target.id);
|
|
4530
|
+
(screen.targets ??= []).push(target);
|
|
4531
|
+
merged = true;
|
|
4532
|
+
}
|
|
4533
|
+
for (const form of child.forms) {
|
|
4534
|
+
if (form.fields.length === 0) continue;
|
|
4535
|
+
const id = formIds.has(form.id) ? `${form.id}:${child.name}` : form.id;
|
|
4536
|
+
if (formIds.has(id)) continue;
|
|
4537
|
+
formIds.add(id);
|
|
4538
|
+
screen.forms.push({ ...form, id });
|
|
4539
|
+
merged = true;
|
|
4540
|
+
}
|
|
4541
|
+
if (merged) {
|
|
4542
|
+
filesMerged++;
|
|
4543
|
+
gained = true;
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
if (gained) screensEnriched++;
|
|
4547
|
+
}
|
|
4548
|
+
return { screens, screensEnriched, filesMerged };
|
|
4549
|
+
}
|
|
4550
|
+
|
|
3413
4551
|
// src/analyzers/ReactNativePlatformAnalyzer.ts
|
|
3414
4552
|
var ReactNativePlatformAnalyzer = class {
|
|
3415
4553
|
platform = "react-native";
|
|
3416
4554
|
async analyze(config, options) {
|
|
3417
|
-
const screenAnalyzer = new ScreenAnalyzer(config, {
|
|
3418
|
-
strictScreens: options.strictScreens ?? true,
|
|
3419
|
-
screenPatterns: options.screenPatterns
|
|
3420
|
-
});
|
|
3421
4555
|
const navigationAnalyzer = new NavigationAnalyzer(config, {
|
|
3422
4556
|
navigationInclude: options.navigationInclude,
|
|
3423
4557
|
navigationExclude: options.navigationExclude
|
|
@@ -3425,14 +4559,31 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3425
4559
|
const componentAnalyzer = new ComponentAnalyzer(config);
|
|
3426
4560
|
const formAnalyzer = new FormAnalyzer(config);
|
|
3427
4561
|
console.log("[ReactNativePlatformAnalyzer] Running analyzers...");
|
|
3428
|
-
const
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
4562
|
+
const navigation = await navigationAnalyzer.analyze();
|
|
4563
|
+
const screenAnalyzer = new ScreenAnalyzer(config, {
|
|
4564
|
+
strictScreens: options.strictScreens ?? true,
|
|
4565
|
+
screenPatterns: options.screenPatterns,
|
|
4566
|
+
// Menos os arquivos que declaram navegador: ver `navigatorFiles`.
|
|
4567
|
+
routeTargetFiles: new Set(
|
|
4568
|
+
[...navigationAnalyzer.routeTargets.values()].filter(
|
|
4569
|
+
(file2) => !navigationAnalyzer.navigatorFiles.has(file2)
|
|
4570
|
+
)
|
|
4571
|
+
)
|
|
4572
|
+
});
|
|
4573
|
+
const screens = await screenAnalyzer.analyze();
|
|
4574
|
+
const composed = await composeAffordances({
|
|
4575
|
+
screens,
|
|
4576
|
+
graph: navigationAnalyzer.graph,
|
|
4577
|
+
analyzeFile: (file2) => screenAnalyzer.analyzeFile(file2),
|
|
4578
|
+
screenFiles: new Set(screens.map((s) => s.filePath).filter(Boolean))
|
|
4579
|
+
});
|
|
4580
|
+
console.log(
|
|
4581
|
+
`[ReactNativePlatformAnalyzer] Composi\xE7\xE3o: ${composed.filesMerged} componente(s) exclusivo(s) fundido(s) em ${composed.screensEnriched} tela(s)`
|
|
4582
|
+
);
|
|
3432
4583
|
console.log(
|
|
3433
4584
|
`[ReactNativePlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens`
|
|
3434
4585
|
);
|
|
3435
|
-
const screenFiles = await (
|
|
4586
|
+
const screenFiles = await globSorted(config.include || ["**/*.tsx", "**/*.ts"], {
|
|
3436
4587
|
cwd: config.rootDir,
|
|
3437
4588
|
ignore: config.exclude || ["**/node_modules/**"]
|
|
3438
4589
|
});
|
|
@@ -3440,7 +4591,7 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3440
4591
|
`[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
|
|
3441
4592
|
);
|
|
3442
4593
|
const enrichmentPromises = screenFiles.map(async (file2) => {
|
|
3443
|
-
const filePath =
|
|
4594
|
+
const filePath = import_node_path3.default.resolve(config.rootDir, file2);
|
|
3444
4595
|
try {
|
|
3445
4596
|
const [components, forms] = await Promise.all([
|
|
3446
4597
|
componentAnalyzer.analyzeFile(filePath),
|
|
@@ -3495,7 +4646,8 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3495
4646
|
screens: enrichedScreens,
|
|
3496
4647
|
navigation,
|
|
3497
4648
|
analyzedFiles: screenFiles.length,
|
|
3498
|
-
...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
|
|
4649
|
+
...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {},
|
|
4650
|
+
diagnostics: { navigation: navigationAnalyzer.diagnostics }
|
|
3499
4651
|
};
|
|
3500
4652
|
}
|
|
3501
4653
|
mergeForm(target, source) {
|
|
@@ -3513,9 +4665,12 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3513
4665
|
findEquivalentField(fields, incoming) {
|
|
3514
4666
|
return fields.find((field) => {
|
|
3515
4667
|
if (field.name && incoming.name && field.name === incoming.name) return true;
|
|
3516
|
-
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
3517
|
-
|
|
3518
|
-
if (field.
|
|
4668
|
+
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
4669
|
+
return true;
|
|
4670
|
+
if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
|
|
4671
|
+
return true;
|
|
4672
|
+
if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
|
|
4673
|
+
return true;
|
|
3519
4674
|
if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
|
|
3520
4675
|
return true;
|
|
3521
4676
|
}
|
|
@@ -3548,7 +4703,9 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3548
4703
|
if (incoming.fields.length === 0) return void 0;
|
|
3549
4704
|
let best;
|
|
3550
4705
|
for (const form of forms) {
|
|
3551
|
-
const overlap = incoming.fields.filter(
|
|
4706
|
+
const overlap = incoming.fields.filter(
|
|
4707
|
+
(field) => this.findEquivalentField(form.fields, field)
|
|
4708
|
+
).length;
|
|
3552
4709
|
if (overlap > 0 && (!best || overlap > best.overlap)) {
|
|
3553
4710
|
best = { form, overlap };
|
|
3554
4711
|
}
|
|
@@ -3575,9 +4732,8 @@ var GenericPlatformAnalyzer = class {
|
|
|
3575
4732
|
// src/analyzers/web/WebScreenAnalyzer.ts
|
|
3576
4733
|
var import_promises2 = __toESM(require("fs/promises"));
|
|
3577
4734
|
var import_path5 = __toESM(require("path"));
|
|
3578
|
-
var import_fast_glob4 = __toESM(require("fast-glob"));
|
|
3579
4735
|
var import_traverse9 = __toESM(require("@babel/traverse"));
|
|
3580
|
-
var
|
|
4736
|
+
var t11 = __toESM(require("@babel/types"));
|
|
3581
4737
|
|
|
3582
4738
|
// src/ast/jsx/web/classify.ts
|
|
3583
4739
|
var VIEW_TAGS = /* @__PURE__ */ new Set([
|
|
@@ -3693,16 +4849,16 @@ function classifyWebJsxComponent(name, element) {
|
|
|
3693
4849
|
}
|
|
3694
4850
|
|
|
3695
4851
|
// src/ast/jsx/names.ts
|
|
3696
|
-
var
|
|
4852
|
+
var t9 = __toESM(require("@babel/types"));
|
|
3697
4853
|
function getJsxElementName(openingElement) {
|
|
3698
4854
|
return getJsxName(openingElement.name);
|
|
3699
4855
|
}
|
|
3700
4856
|
function getJsxName(name) {
|
|
3701
|
-
if (
|
|
3702
|
-
if (
|
|
3703
|
-
if (
|
|
4857
|
+
if (t9.isJSXIdentifier(name)) return name.name;
|
|
4858
|
+
if (t9.isJSXNamespacedName(name)) return `${name.namespace.name}:${name.name.name}`;
|
|
4859
|
+
if (t9.isJSXMemberExpression(name)) {
|
|
3704
4860
|
const objectName = getJsxName(name.object);
|
|
3705
|
-
const propertyName =
|
|
4861
|
+
const propertyName = t9.isJSXIdentifier(name.property) ? name.property.name : null;
|
|
3706
4862
|
return objectName && propertyName ? `${objectName}.${propertyName}` : null;
|
|
3707
4863
|
}
|
|
3708
4864
|
return null;
|
|
@@ -3710,32 +4866,32 @@ function getJsxName(name) {
|
|
|
3710
4866
|
|
|
3711
4867
|
// src/ast/navigation/web-calls.ts
|
|
3712
4868
|
var import_traverse8 = __toESM(require("@babel/traverse"));
|
|
3713
|
-
var
|
|
4869
|
+
var t10 = __toESM(require("@babel/types"));
|
|
3714
4870
|
var ROUTERISH_OBJECTS = /^(router|history|navigation)$/;
|
|
3715
4871
|
function staticRoutePath(node) {
|
|
3716
4872
|
if (!node) return void 0;
|
|
3717
|
-
if (
|
|
3718
|
-
if (
|
|
3719
|
-
let
|
|
4873
|
+
if (t10.isStringLiteral(node)) return node.value;
|
|
4874
|
+
if (t10.isTemplateLiteral(node)) {
|
|
4875
|
+
let path13 = "";
|
|
3720
4876
|
node.quasis.forEach((quasi, index) => {
|
|
3721
|
-
|
|
4877
|
+
path13 += quasi.value.cooked ?? quasi.value.raw;
|
|
3722
4878
|
const expr = node.expressions[index];
|
|
3723
|
-
if (expr)
|
|
4879
|
+
if (expr) path13 += `:${paramNameOf(expr)}`;
|
|
3724
4880
|
});
|
|
3725
|
-
return
|
|
4881
|
+
return path13;
|
|
3726
4882
|
}
|
|
3727
4883
|
return void 0;
|
|
3728
4884
|
}
|
|
3729
4885
|
function paramNameOf(expr) {
|
|
3730
|
-
if (
|
|
3731
|
-
if (
|
|
4886
|
+
if (t10.isIdentifier(expr)) return expr.name;
|
|
4887
|
+
if (t10.isMemberExpression(expr) && t10.isIdentifier(expr.property)) return expr.property.name;
|
|
3732
4888
|
return "param";
|
|
3733
4889
|
}
|
|
3734
4890
|
function extractWebNavigationCalls(ast) {
|
|
3735
4891
|
const calls = [];
|
|
3736
4892
|
const inspect = (node) => {
|
|
3737
|
-
if (
|
|
3738
|
-
if (
|
|
4893
|
+
if (t10.isCallExpression(node)) {
|
|
4894
|
+
if (t10.isIdentifier(node.callee) && node.callee.name === "navigate") {
|
|
3739
4895
|
const first = node.arguments[0];
|
|
3740
4896
|
const targetPath = staticRoutePath(first);
|
|
3741
4897
|
if (targetPath !== void 0) {
|
|
@@ -3743,12 +4899,12 @@ function extractWebNavigationCalls(ast) {
|
|
|
3743
4899
|
method: hasReplaceOption(node.arguments[1]) ? "replace" : "navigate",
|
|
3744
4900
|
targetPath
|
|
3745
4901
|
});
|
|
3746
|
-
} else if (
|
|
4902
|
+
} else if (t10.isNumericLiteral(first) || t10.isUnaryExpression(first) && first.operator === "-") {
|
|
3747
4903
|
calls.push({ method: "goBack" });
|
|
3748
4904
|
}
|
|
3749
4905
|
return;
|
|
3750
4906
|
}
|
|
3751
|
-
if (
|
|
4907
|
+
if (t10.isMemberExpression(node.callee) && t10.isIdentifier(node.callee.object) && ROUTERISH_OBJECTS.test(node.callee.object.name) && t10.isIdentifier(node.callee.property)) {
|
|
3752
4908
|
const method = node.callee.property.name;
|
|
3753
4909
|
const targetPath = staticRoutePath(node.arguments[0]);
|
|
3754
4910
|
if ((method === "push" || method === "navigate") && targetPath !== void 0) {
|
|
@@ -3761,25 +4917,25 @@ function extractWebNavigationCalls(ast) {
|
|
|
3761
4917
|
}
|
|
3762
4918
|
return;
|
|
3763
4919
|
}
|
|
3764
|
-
if (
|
|
4920
|
+
if (t10.isAssignmentExpression(node) && t10.isMemberExpression(node.left) && t10.isIdentifier(node.left.property) && node.left.property.name === "href" && isLocationExpression(node.left.object) && t10.isStringLiteral(node.right) && node.right.value.startsWith("/")) {
|
|
3765
4921
|
calls.push({ method: "navigate", targetPath: node.right.value });
|
|
3766
4922
|
}
|
|
3767
4923
|
};
|
|
3768
4924
|
(0, import_traverse8.default)(ast, {
|
|
3769
|
-
noScope: !
|
|
4925
|
+
noScope: !t10.isFile(ast),
|
|
3770
4926
|
enter: (nodePath) => inspect(nodePath.node)
|
|
3771
4927
|
});
|
|
3772
4928
|
return calls;
|
|
3773
4929
|
}
|
|
3774
4930
|
function hasReplaceOption(arg) {
|
|
3775
|
-
if (!arg || !
|
|
4931
|
+
if (!arg || !t10.isObjectExpression(arg)) return false;
|
|
3776
4932
|
return arg.properties.some(
|
|
3777
|
-
(prop) =>
|
|
4933
|
+
(prop) => t10.isObjectProperty(prop) && t10.isIdentifier(prop.key) && prop.key.name === "replace" && t10.isBooleanLiteral(prop.value) && prop.value.value === true
|
|
3778
4934
|
);
|
|
3779
4935
|
}
|
|
3780
4936
|
function isLocationExpression(node) {
|
|
3781
|
-
if (
|
|
3782
|
-
return
|
|
4937
|
+
if (t10.isIdentifier(node)) return node.name === "location";
|
|
4938
|
+
return t10.isMemberExpression(node) && t10.isIdentifier(node.object) && node.object.name === "window" && t10.isIdentifier(node.property) && node.property.name === "location";
|
|
3783
4939
|
}
|
|
3784
4940
|
|
|
3785
4941
|
// src/analyzers/web/WebScreenAnalyzer.ts
|
|
@@ -3807,10 +4963,10 @@ var WebScreenAnalyzer = class {
|
|
|
3807
4963
|
include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
|
|
3808
4964
|
exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
3809
4965
|
} = this.config;
|
|
3810
|
-
const files = (await (
|
|
4966
|
+
const files = (await globSorted(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
|
|
3811
4967
|
(file2) => !file2.endsWith(".d.ts")
|
|
3812
4968
|
);
|
|
3813
|
-
const patternMatches = await (
|
|
4969
|
+
const patternMatches = await globSorted(this.screenPatterns, {
|
|
3814
4970
|
cwd: this.config.rootDir,
|
|
3815
4971
|
ignore: exclude
|
|
3816
4972
|
});
|
|
@@ -3904,7 +5060,7 @@ var WebScreenAnalyzer = class {
|
|
|
3904
5060
|
CallExpression: (nodePath) => {
|
|
3905
5061
|
if (!isRegisterScreenCallee(nodePath.node.callee)) return;
|
|
3906
5062
|
const arg = nodePath.node.arguments[0];
|
|
3907
|
-
if (
|
|
5063
|
+
if (t11.isObjectExpression(arg)) {
|
|
3908
5064
|
plain = literalToPlain(arg);
|
|
3909
5065
|
}
|
|
3910
5066
|
}
|
|
@@ -3951,20 +5107,20 @@ var WebScreenAnalyzer = class {
|
|
|
3951
5107
|
(0, import_traverse9.default)(ast, {
|
|
3952
5108
|
ExportDefaultDeclaration: (nodePath) => {
|
|
3953
5109
|
const declaration = nodePath.node.declaration;
|
|
3954
|
-
if (
|
|
5110
|
+
if (t11.isFunctionDeclaration(declaration) && declaration.id?.name) {
|
|
3955
5111
|
defaultName = declaration.id.name;
|
|
3956
|
-
} else if (
|
|
5112
|
+
} else if (t11.isIdentifier(declaration)) {
|
|
3957
5113
|
defaultName = declaration.name;
|
|
3958
5114
|
}
|
|
3959
5115
|
},
|
|
3960
5116
|
ExportNamedDeclaration: (nodePath) => {
|
|
3961
5117
|
if (firstExported) return;
|
|
3962
5118
|
const declaration = nodePath.node.declaration;
|
|
3963
|
-
if (
|
|
5119
|
+
if (t11.isFunctionDeclaration(declaration) && declaration.id && /^[A-Z]/.test(declaration.id.name)) {
|
|
3964
5120
|
firstExported = declaration.id.name;
|
|
3965
|
-
} else if (
|
|
5121
|
+
} else if (t11.isVariableDeclaration(declaration)) {
|
|
3966
5122
|
for (const declarator of declaration.declarations) {
|
|
3967
|
-
if (
|
|
5123
|
+
if (t11.isIdentifier(declarator.id) && /^[A-Z]/.test(declarator.id.name) && (t11.isArrowFunctionExpression(declarator.init) || t11.isFunctionExpression(declarator.init))) {
|
|
3968
5124
|
firstExported = declarator.id.name;
|
|
3969
5125
|
break;
|
|
3970
5126
|
}
|
|
@@ -4136,15 +5292,15 @@ var WebScreenAnalyzer = class {
|
|
|
4136
5292
|
});
|
|
4137
5293
|
}
|
|
4138
5294
|
for (const attr of opening.attributes) {
|
|
4139
|
-
if (!
|
|
5295
|
+
if (!t11.isJSXAttribute(attr) || !t11.isJSXIdentifier(attr.name)) continue;
|
|
4140
5296
|
const attrName = attr.name.name;
|
|
4141
5297
|
if (attrName === "required") {
|
|
4142
5298
|
if (attr.value === null) field.required = true;
|
|
4143
|
-
else if (
|
|
5299
|
+
else if (t11.isJSXExpressionContainer(attr.value) && t11.isBooleanLiteral(attr.value.expression)) {
|
|
4144
5300
|
field.required = attr.value.expression.value;
|
|
4145
5301
|
}
|
|
4146
5302
|
}
|
|
4147
|
-
if ((attrName === "value" || attrName === "checked") && attr.value &&
|
|
5303
|
+
if ((attrName === "value" || attrName === "checked") && attr.value && t11.isJSXExpressionContainer(attr.value) && t11.isIdentifier(attr.value.expression)) {
|
|
4148
5304
|
field.valueBinding = attr.value.expression.name;
|
|
4149
5305
|
if (!field.name) field.name = attr.value.expression.name;
|
|
4150
5306
|
}
|
|
@@ -4194,7 +5350,7 @@ var WebScreenAnalyzer = class {
|
|
|
4194
5350
|
extractSelectOptions(selectElement) {
|
|
4195
5351
|
const options = [];
|
|
4196
5352
|
for (const child of selectElement.children) {
|
|
4197
|
-
if (!
|
|
5353
|
+
if (!t11.isJSXElement(child)) continue;
|
|
4198
5354
|
if (getJsxElementName(child.openingElement) !== "option") continue;
|
|
4199
5355
|
const value = getStringAttr(child.openingElement, "value");
|
|
4200
5356
|
const label = jsxTextContent(child) || value || "";
|
|
@@ -4305,14 +5461,14 @@ var WebScreenAnalyzer = class {
|
|
|
4305
5461
|
}
|
|
4306
5462
|
let handlerName;
|
|
4307
5463
|
const onClickAttr = opening.attributes.find(
|
|
4308
|
-
(attr) =>
|
|
5464
|
+
(attr) => t11.isJSXAttribute(attr) && t11.isJSXIdentifier(attr.name) && attr.name.name === "onClick"
|
|
4309
5465
|
);
|
|
4310
|
-
if (onClickAttr?.value &&
|
|
5466
|
+
if (onClickAttr?.value && t11.isJSXExpressionContainer(onClickAttr.value)) {
|
|
4311
5467
|
const expr = onClickAttr.value.expression;
|
|
4312
|
-
if (
|
|
5468
|
+
if (t11.isIdentifier(expr)) {
|
|
4313
5469
|
handlerName = expr.name;
|
|
4314
5470
|
action.handler = expr.name;
|
|
4315
|
-
} else if (!
|
|
5471
|
+
} else if (!t11.isJSXEmptyExpression(expr)) {
|
|
4316
5472
|
const inlineNavCalls = extractWebNavigationCalls(expr);
|
|
4317
5473
|
const inlineNav = inlineNavCalls.find((call) => call.targetPath);
|
|
4318
5474
|
if (inlineNav?.targetPath) {
|
|
@@ -4451,12 +5607,12 @@ var WebScreenAnalyzer = class {
|
|
|
4451
5607
|
}
|
|
4452
5608
|
handlerNameFromAttr(opening, attrName) {
|
|
4453
5609
|
const attr = opening.attributes.find(
|
|
4454
|
-
(candidate) =>
|
|
5610
|
+
(candidate) => t11.isJSXAttribute(candidate) && t11.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
|
|
4455
5611
|
);
|
|
4456
|
-
if (!attr?.value || !
|
|
5612
|
+
if (!attr?.value || !t11.isJSXExpressionContainer(attr.value)) return void 0;
|
|
4457
5613
|
const expr = attr.value.expression;
|
|
4458
|
-
if (
|
|
4459
|
-
if (
|
|
5614
|
+
if (t11.isIdentifier(expr)) return expr.name;
|
|
5615
|
+
if (t11.isArrowFunctionExpression(expr) || t11.isFunctionExpression(expr)) {
|
|
4460
5616
|
return firstCalledFunctionName(expr) ?? "anonymous";
|
|
4461
5617
|
}
|
|
4462
5618
|
return void 0;
|
|
@@ -4490,11 +5646,11 @@ var WebScreenAnalyzer = class {
|
|
|
4490
5646
|
(0, import_traverse9.default)(ast, {
|
|
4491
5647
|
JSXExpressionContainer: (nodePath) => {
|
|
4492
5648
|
const expr = nodePath.node.expression;
|
|
4493
|
-
if (!
|
|
5649
|
+
if (!t11.isCallExpression(expr) || !t11.isMemberExpression(expr.callee) || !t11.isIdentifier(expr.callee.object) || !t11.isIdentifier(expr.callee.property) || expr.callee.property.name !== "map") {
|
|
4494
5650
|
return;
|
|
4495
5651
|
}
|
|
4496
5652
|
const callback = expr.arguments[0];
|
|
4497
|
-
if (!
|
|
5653
|
+
if (!t11.isArrowFunctionExpression(callback) && !t11.isFunctionExpression(callback)) return;
|
|
4498
5654
|
const enclosing = nodePath.findParent(
|
|
4499
5655
|
(p) => p.isJSXElement()
|
|
4500
5656
|
);
|
|
@@ -4529,22 +5685,22 @@ var WebScreenAnalyzer = class {
|
|
|
4529
5685
|
}
|
|
4530
5686
|
};
|
|
4531
5687
|
function isRegisterScreenCallee(callee) {
|
|
4532
|
-
return
|
|
5688
|
+
return t11.isIdentifier(callee) && callee.name === "registerScreen" || t11.isMemberExpression(callee) && t11.isIdentifier(callee.property) && callee.property.name === "registerScreen";
|
|
4533
5689
|
}
|
|
4534
5690
|
function literalToPlain(node) {
|
|
4535
|
-
if (
|
|
5691
|
+
if (t11.isStringLiteral(node) || t11.isNumericLiteral(node) || t11.isBooleanLiteral(node)) {
|
|
4536
5692
|
return node.value;
|
|
4537
5693
|
}
|
|
4538
|
-
if (
|
|
4539
|
-
if (
|
|
4540
|
-
return node.elements.filter((el) => el !== null &&
|
|
5694
|
+
if (t11.isNullLiteral(node)) return null;
|
|
5695
|
+
if (t11.isArrayExpression(node)) {
|
|
5696
|
+
return node.elements.filter((el) => el !== null && t11.isExpression(el)).map((el) => literalToPlain(el)).filter((value) => value !== void 0);
|
|
4541
5697
|
}
|
|
4542
|
-
if (
|
|
5698
|
+
if (t11.isObjectExpression(node)) {
|
|
4543
5699
|
const out = {};
|
|
4544
5700
|
for (const prop of node.properties) {
|
|
4545
|
-
if (!
|
|
4546
|
-
const key =
|
|
4547
|
-
if (!key || !
|
|
5701
|
+
if (!t11.isObjectProperty(prop)) continue;
|
|
5702
|
+
const key = t11.isIdentifier(prop.key) ? prop.key.name : t11.isStringLiteral(prop.key) ? prop.key.value : void 0;
|
|
5703
|
+
if (!key || !t11.isExpression(prop.value)) continue;
|
|
4548
5704
|
const value = literalToPlain(prop.value);
|
|
4549
5705
|
if (value !== void 0) out[key] = value;
|
|
4550
5706
|
}
|
|
@@ -4556,12 +5712,12 @@ function jsxTextContent(element) {
|
|
|
4556
5712
|
const parts = [];
|
|
4557
5713
|
const walk = (children) => {
|
|
4558
5714
|
for (const child of children) {
|
|
4559
|
-
if (
|
|
5715
|
+
if (t11.isJSXText(child)) {
|
|
4560
5716
|
const trimmed = child.value.replace(/\s+/g, " ").trim();
|
|
4561
5717
|
if (trimmed) parts.push(trimmed);
|
|
4562
|
-
} else if (
|
|
5718
|
+
} else if (t11.isJSXExpressionContainer(child) && t11.isStringLiteral(child.expression)) {
|
|
4563
5719
|
parts.push(child.expression.value);
|
|
4564
|
-
} else if (
|
|
5720
|
+
} else if (t11.isJSXElement(child)) {
|
|
4565
5721
|
walk(child.children);
|
|
4566
5722
|
}
|
|
4567
5723
|
}
|
|
@@ -4571,26 +5727,26 @@ function jsxTextContent(element) {
|
|
|
4571
5727
|
return text.length > 0 ? text : void 0;
|
|
4572
5728
|
}
|
|
4573
5729
|
function firstCalledFunctionName(node) {
|
|
4574
|
-
if (
|
|
4575
|
-
if (
|
|
5730
|
+
if (t11.isIdentifier(node)) return node.name;
|
|
5731
|
+
if (t11.isArrowFunctionExpression(node) || t11.isFunctionExpression(node)) {
|
|
4576
5732
|
return firstCalledFunctionName(node.body);
|
|
4577
5733
|
}
|
|
4578
|
-
if (
|
|
5734
|
+
if (t11.isBlockStatement(node)) {
|
|
4579
5735
|
for (const statement of node.body) {
|
|
4580
5736
|
const handler = firstCalledFunctionName(statement);
|
|
4581
5737
|
if (handler) return handler;
|
|
4582
5738
|
}
|
|
4583
5739
|
return void 0;
|
|
4584
5740
|
}
|
|
4585
|
-
if (
|
|
4586
|
-
if (
|
|
5741
|
+
if (t11.isExpressionStatement(node)) return firstCalledFunctionName(node.expression);
|
|
5742
|
+
if (t11.isReturnStatement(node)) {
|
|
4587
5743
|
return node.argument ? firstCalledFunctionName(node.argument) : void 0;
|
|
4588
5744
|
}
|
|
4589
|
-
if (
|
|
5745
|
+
if (t11.isAwaitExpression(node) || t11.isUnaryExpression(node)) {
|
|
4590
5746
|
return firstCalledFunctionName(node.argument);
|
|
4591
5747
|
}
|
|
4592
|
-
if (
|
|
4593
|
-
if (
|
|
5748
|
+
if (t11.isCallExpression(node)) {
|
|
5749
|
+
if (t11.isIdentifier(node.callee) && !/^(navigate|confirm|alert)$/.test(node.callee.name)) {
|
|
4594
5750
|
return node.callee.name;
|
|
4595
5751
|
}
|
|
4596
5752
|
return void 0;
|
|
@@ -4605,8 +5761,8 @@ function containsWindowConfirm(body) {
|
|
|
4605
5761
|
noScope: true,
|
|
4606
5762
|
CallExpression: (nodePath) => {
|
|
4607
5763
|
const callee = nodePath.node.callee;
|
|
4608
|
-
if (
|
|
4609
|
-
if (
|
|
5764
|
+
if (t11.isIdentifier(callee) && callee.name === "confirm") found = true;
|
|
5765
|
+
if (t11.isMemberExpression(callee) && t11.isIdentifier(callee.object) && callee.object.name === "window" && t11.isIdentifier(callee.property) && callee.property.name === "confirm") {
|
|
4610
5766
|
found = true;
|
|
4611
5767
|
}
|
|
4612
5768
|
}
|
|
@@ -4621,9 +5777,9 @@ function routePathAttr(opening, attrName) {
|
|
|
4621
5777
|
const literal = getStringAttr(opening, attrName);
|
|
4622
5778
|
if (literal) return literal;
|
|
4623
5779
|
const attr = opening.attributes.find(
|
|
4624
|
-
(candidate) =>
|
|
5780
|
+
(candidate) => t11.isJSXAttribute(candidate) && t11.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
|
|
4625
5781
|
);
|
|
4626
|
-
if (!attr?.value || !
|
|
5782
|
+
if (!attr?.value || !t11.isJSXExpressionContainer(attr.value)) return void 0;
|
|
4627
5783
|
return staticRoutePath(attr.value.expression);
|
|
4628
5784
|
}
|
|
4629
5785
|
function slugify(label) {
|
|
@@ -4638,10 +5794,10 @@ function isWeakInferredFieldName(name) {
|
|
|
4638
5794
|
function collectionItemNames(callback) {
|
|
4639
5795
|
const names = /* @__PURE__ */ new Set(["item"]);
|
|
4640
5796
|
const firstParam = callback.params[0];
|
|
4641
|
-
if (
|
|
4642
|
-
if (
|
|
5797
|
+
if (t11.isIdentifier(firstParam)) names.add(firstParam.name);
|
|
5798
|
+
if (t11.isObjectPattern(firstParam)) {
|
|
4643
5799
|
for (const prop of firstParam.properties) {
|
|
4644
|
-
if (
|
|
5800
|
+
if (t11.isObjectProperty(prop) && t11.isIdentifier(prop.key) && t11.isIdentifier(prop.value)) {
|
|
4645
5801
|
names.add(prop.value.name);
|
|
4646
5802
|
}
|
|
4647
5803
|
}
|
|
@@ -4657,7 +5813,7 @@ function collectionDisplayFields(callback, itemNames) {
|
|
|
4657
5813
|
noScope: true,
|
|
4658
5814
|
MemberExpression: (nodePath) => {
|
|
4659
5815
|
const node = nodePath.node;
|
|
4660
|
-
if (
|
|
5816
|
+
if (t11.isIdentifier(node.object) && itemNames.has(node.object.name) && t11.isIdentifier(node.property)) {
|
|
4661
5817
|
fields.add(node.property.name);
|
|
4662
5818
|
}
|
|
4663
5819
|
}
|
|
@@ -4674,10 +5830,10 @@ function collectionKeyField(callback, itemNames) {
|
|
|
4674
5830
|
noScope: true,
|
|
4675
5831
|
JSXAttribute: (nodePath) => {
|
|
4676
5832
|
const attr = nodePath.node;
|
|
4677
|
-
if (!
|
|
4678
|
-
if (!attr.value || !
|
|
5833
|
+
if (!t11.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
|
|
5834
|
+
if (!attr.value || !t11.isJSXExpressionContainer(attr.value)) return;
|
|
4679
5835
|
const expr = attr.value.expression;
|
|
4680
|
-
if (
|
|
5836
|
+
if (t11.isMemberExpression(expr) && t11.isIdentifier(expr.object) && itemNames.has(expr.object.name) && t11.isIdentifier(expr.property)) {
|
|
4681
5837
|
keyField = keyField ?? expr.property.name;
|
|
4682
5838
|
}
|
|
4683
5839
|
}
|
|
@@ -4700,18 +5856,18 @@ function callbackReturnsTag(callback, tags) {
|
|
|
4700
5856
|
let found = false;
|
|
4701
5857
|
const inspect = (node) => {
|
|
4702
5858
|
if (!node || found) return;
|
|
4703
|
-
if (
|
|
5859
|
+
if (t11.isJSXElement(node)) {
|
|
4704
5860
|
const name = getJsxElementName(node.openingElement);
|
|
4705
5861
|
if (name && tags.has(name)) found = true;
|
|
4706
5862
|
return;
|
|
4707
5863
|
}
|
|
4708
|
-
if (
|
|
5864
|
+
if (t11.isBlockStatement(node)) {
|
|
4709
5865
|
for (const statement of node.body) {
|
|
4710
|
-
if (
|
|
5866
|
+
if (t11.isReturnStatement(statement)) inspect(statement.argument);
|
|
4711
5867
|
}
|
|
4712
5868
|
}
|
|
4713
|
-
if (
|
|
4714
|
-
if (
|
|
5869
|
+
if (t11.isParenthesizedExpression(node)) inspect(node.expression);
|
|
5870
|
+
if (t11.isConditionalExpression(node)) {
|
|
4715
5871
|
inspect(node.consequent);
|
|
4716
5872
|
inspect(node.alternate);
|
|
4717
5873
|
}
|
|
@@ -4738,9 +5894,8 @@ function inferIdentityFields(keyField, displayFields) {
|
|
|
4738
5894
|
// src/analyzers/web/WebNavigationAnalyzer.ts
|
|
4739
5895
|
var import_fs7 = require("fs");
|
|
4740
5896
|
var import_path6 = __toESM(require("path"));
|
|
4741
|
-
var import_fast_glob5 = __toESM(require("fast-glob"));
|
|
4742
5897
|
var import_traverse10 = __toESM(require("@babel/traverse"));
|
|
4743
|
-
var
|
|
5898
|
+
var t12 = __toESM(require("@babel/types"));
|
|
4744
5899
|
var WEB_NAVIGATOR_TYPE = "route";
|
|
4745
5900
|
var WebNavigationAnalyzer = class {
|
|
4746
5901
|
config;
|
|
@@ -4757,7 +5912,9 @@ var WebNavigationAnalyzer = class {
|
|
|
4757
5912
|
for (const filePath of files) {
|
|
4758
5913
|
try {
|
|
4759
5914
|
const content = await import_fs7.promises.readFile(filePath, "utf-8");
|
|
4760
|
-
if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
|
|
5915
|
+
if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
|
|
5916
|
+
content
|
|
5917
|
+
)) {
|
|
4761
5918
|
continue;
|
|
4762
5919
|
}
|
|
4763
5920
|
const ast = parseSource(content, this.config.parserPlugins);
|
|
@@ -4787,7 +5944,7 @@ var WebNavigationAnalyzer = class {
|
|
|
4787
5944
|
...this.config.exclude || [],
|
|
4788
5945
|
...this.navigationExclude
|
|
4789
5946
|
];
|
|
4790
|
-
const files = await (
|
|
5947
|
+
const files = await globSorted(patterns, { cwd: this.config.rootDir, ignore });
|
|
4791
5948
|
return files.map((file2) => import_path6.default.join(this.config.rootDir, file2));
|
|
4792
5949
|
}
|
|
4793
5950
|
// ── JSX <Route> style ────────────────────────────────────────────
|
|
@@ -4798,7 +5955,7 @@ var WebNavigationAnalyzer = class {
|
|
|
4798
5955
|
const name = getJsxElementName(opening);
|
|
4799
5956
|
if (name !== "Route") {
|
|
4800
5957
|
for (const child of element.children) {
|
|
4801
|
-
if (
|
|
5958
|
+
if (t12.isJSXElement(child)) visitRoute(child, parentPath);
|
|
4802
5959
|
}
|
|
4803
5960
|
return;
|
|
4804
5961
|
}
|
|
@@ -4807,13 +5964,13 @@ var WebNavigationAnalyzer = class {
|
|
|
4807
5964
|
const fullPath = this.joinPaths(parentPath, segment, isIndex);
|
|
4808
5965
|
const componentName = this.componentNameFromElementAttr(opening) ?? void 0;
|
|
4809
5966
|
const isLeaf = !element.children.some(
|
|
4810
|
-
(child) =>
|
|
5967
|
+
(child) => t12.isJSXElement(child) && getJsxElementName(child.openingElement) === "Route"
|
|
4811
5968
|
);
|
|
4812
5969
|
if ((segment || isIndex) && (componentName || isLeaf)) {
|
|
4813
5970
|
routes.push(this.buildRoute(fullPath, componentName, isIndex, !isLeaf));
|
|
4814
5971
|
}
|
|
4815
5972
|
for (const child of element.children) {
|
|
4816
|
-
if (
|
|
5973
|
+
if (t12.isJSXElement(child)) visitRoute(child, fullPath);
|
|
4817
5974
|
}
|
|
4818
5975
|
};
|
|
4819
5976
|
(0, import_traverse10.default)(ast, {
|
|
@@ -4835,13 +5992,13 @@ var WebNavigationAnalyzer = class {
|
|
|
4835
5992
|
/** `element={<VehicleList/>}` or `Component={VehicleList}`. */
|
|
4836
5993
|
componentNameFromElementAttr(opening) {
|
|
4837
5994
|
for (const attr of opening.attributes) {
|
|
4838
|
-
if (!
|
|
4839
|
-
if (attr.name.name === "element" &&
|
|
5995
|
+
if (!t12.isJSXAttribute(attr) || !t12.isJSXIdentifier(attr.name)) continue;
|
|
5996
|
+
if (attr.name.name === "element" && t12.isJSXExpressionContainer(attr.value)) {
|
|
4840
5997
|
const expr = attr.value.expression;
|
|
4841
|
-
if (
|
|
5998
|
+
if (t12.isJSXElement(expr)) return getJsxElementName(expr.openingElement);
|
|
4842
5999
|
}
|
|
4843
|
-
if (attr.name.name === "Component" &&
|
|
4844
|
-
if (
|
|
6000
|
+
if (attr.name.name === "Component" && t12.isJSXExpressionContainer(attr.value)) {
|
|
6001
|
+
if (t12.isIdentifier(attr.value.expression)) return attr.value.expression.name;
|
|
4845
6002
|
}
|
|
4846
6003
|
}
|
|
4847
6004
|
return null;
|
|
@@ -4858,9 +6015,9 @@ var WebNavigationAnalyzer = class {
|
|
|
4858
6015
|
(0, import_traverse10.default)(ast, {
|
|
4859
6016
|
CallExpression: (nodePath) => {
|
|
4860
6017
|
const callee = nodePath.node.callee;
|
|
4861
|
-
if (!
|
|
6018
|
+
if (!t12.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
|
|
4862
6019
|
const first = nodePath.node.arguments[0];
|
|
4863
|
-
if (!
|
|
6020
|
+
if (!t12.isArrayExpression(first)) return;
|
|
4864
6021
|
this.visitRouteObjects(first, "", routes);
|
|
4865
6022
|
}
|
|
4866
6023
|
});
|
|
@@ -4868,21 +6025,21 @@ var WebNavigationAnalyzer = class {
|
|
|
4868
6025
|
}
|
|
4869
6026
|
visitRouteObjects(arr, parentPath, out) {
|
|
4870
6027
|
for (const element of arr.elements) {
|
|
4871
|
-
if (!
|
|
6028
|
+
if (!t12.isObjectExpression(element)) continue;
|
|
4872
6029
|
let segment;
|
|
4873
6030
|
let isIndex = false;
|
|
4874
6031
|
let componentName;
|
|
4875
6032
|
let children;
|
|
4876
6033
|
for (const prop of element.properties) {
|
|
4877
|
-
if (!
|
|
6034
|
+
if (!t12.isObjectProperty(prop) || !t12.isIdentifier(prop.key)) continue;
|
|
4878
6035
|
const key = prop.key.name;
|
|
4879
|
-
if (key === "path" &&
|
|
4880
|
-
if (key === "index" &&
|
|
4881
|
-
if (key === "element" &&
|
|
6036
|
+
if (key === "path" && t12.isStringLiteral(prop.value)) segment = prop.value.value;
|
|
6037
|
+
if (key === "index" && t12.isBooleanLiteral(prop.value)) isIndex = prop.value.value;
|
|
6038
|
+
if (key === "element" && t12.isJSXElement(prop.value)) {
|
|
4882
6039
|
componentName = getJsxElementName(prop.value.openingElement) ?? void 0;
|
|
4883
6040
|
}
|
|
4884
|
-
if (key === "Component" &&
|
|
4885
|
-
if (key === "children" &&
|
|
6041
|
+
if (key === "Component" && t12.isIdentifier(prop.value)) componentName = prop.value.name;
|
|
6042
|
+
if (key === "children" && t12.isArrayExpression(prop.value)) children = prop.value;
|
|
4886
6043
|
}
|
|
4887
6044
|
const fullPath = this.joinPaths(parentPath, segment, isIndex);
|
|
4888
6045
|
if ((segment !== void 0 || isIndex) && (componentName || !children)) {
|
|
@@ -4960,11 +6117,13 @@ var WebNavigationAnalyzer = class {
|
|
|
4960
6117
|
return {
|
|
4961
6118
|
screens,
|
|
4962
6119
|
initialScreen: initialRoute?.screenName ?? "",
|
|
4963
|
-
navigators: routes.length > 0 ? [
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
6120
|
+
navigators: routes.length > 0 ? [
|
|
6121
|
+
{
|
|
6122
|
+
name: navigatorName,
|
|
6123
|
+
type: WEB_NAVIGATOR_TYPE,
|
|
6124
|
+
screens: screenNames
|
|
6125
|
+
}
|
|
6126
|
+
] : []
|
|
4968
6127
|
};
|
|
4969
6128
|
}
|
|
4970
6129
|
};
|
|
@@ -5083,7 +6242,7 @@ var ReactWebPlatformAnalyzer = class {
|
|
|
5083
6242
|
|
|
5084
6243
|
// src/manifest/loadManifest.ts
|
|
5085
6244
|
var import_promises3 = require("fs/promises");
|
|
5086
|
-
var
|
|
6245
|
+
var import_node_path4 = __toESM(require("path"));
|
|
5087
6246
|
|
|
5088
6247
|
// ../../node_modules/zod/v3/external.js
|
|
5089
6248
|
var external_exports = {};
|
|
@@ -5290,8 +6449,8 @@ var ZodParsedType = util.arrayToEnum([
|
|
|
5290
6449
|
"set"
|
|
5291
6450
|
]);
|
|
5292
6451
|
var getParsedType = (data) => {
|
|
5293
|
-
const
|
|
5294
|
-
switch (
|
|
6452
|
+
const t16 = typeof data;
|
|
6453
|
+
switch (t16) {
|
|
5295
6454
|
case "undefined":
|
|
5296
6455
|
return ZodParsedType.undefined;
|
|
5297
6456
|
case "string":
|
|
@@ -5563,8 +6722,8 @@ function getErrorMap() {
|
|
|
5563
6722
|
|
|
5564
6723
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
5565
6724
|
var makeIssue = (params) => {
|
|
5566
|
-
const { data, path:
|
|
5567
|
-
const fullPath = [...
|
|
6725
|
+
const { data, path: path13, errorMaps, issueData } = params;
|
|
6726
|
+
const fullPath = [...path13, ...issueData.path || []];
|
|
5568
6727
|
const fullIssue = {
|
|
5569
6728
|
...issueData,
|
|
5570
6729
|
path: fullPath
|
|
@@ -5680,11 +6839,11 @@ var errorUtil;
|
|
|
5680
6839
|
|
|
5681
6840
|
// ../../node_modules/zod/v3/types.js
|
|
5682
6841
|
var ParseInputLazyPath = class {
|
|
5683
|
-
constructor(parent, value,
|
|
6842
|
+
constructor(parent, value, path13, key) {
|
|
5684
6843
|
this._cachedPath = [];
|
|
5685
6844
|
this.parent = parent;
|
|
5686
6845
|
this.data = value;
|
|
5687
|
-
this._path =
|
|
6846
|
+
this._path = path13;
|
|
5688
6847
|
this._key = key;
|
|
5689
6848
|
}
|
|
5690
6849
|
get path() {
|
|
@@ -9126,7 +10285,18 @@ var coerce = {
|
|
|
9126
10285
|
};
|
|
9127
10286
|
var NEVER = INVALID;
|
|
9128
10287
|
|
|
9129
|
-
// ../shared/dist/chunk-
|
|
10288
|
+
// ../shared/dist/chunk-SWQOZWHP.mjs
|
|
10289
|
+
var SAFE_IMAGE_URL_RE = /^(?:https?:\/\/|data:image\/(?:png|jpe?g|gif|webp);base64,)/i;
|
|
10290
|
+
var HAS_SCHEME_RE = /^[\s\u0000-\u001f]*[a-z][a-z0-9+.-]*:/i;
|
|
10291
|
+
function isSafeImageUrl(value) {
|
|
10292
|
+
if (value === null || value === void 0) return true;
|
|
10293
|
+
const trimmed = value.trim();
|
|
10294
|
+
if (trimmed === "") return true;
|
|
10295
|
+
if (!HAS_SCHEME_RE.test(trimmed)) return true;
|
|
10296
|
+
return SAFE_IMAGE_URL_RE.test(trimmed);
|
|
10297
|
+
}
|
|
10298
|
+
|
|
10299
|
+
// ../shared/dist/chunk-NMQNNPSJ.mjs
|
|
9130
10300
|
var locatorSourceSchema = external_exports.enum([
|
|
9131
10301
|
"appilotsId",
|
|
9132
10302
|
"testID",
|
|
@@ -9305,6 +10475,23 @@ var actionDescriptorSchema = external_exports.object({
|
|
|
9305
10475
|
effect: external_exports.enum(["read", "write", "destructive"]).or(external_exports.string()).optional(),
|
|
9306
10476
|
riskLevel: external_exports.enum(["low", "medium", "high"]).or(external_exports.string()).optional(),
|
|
9307
10477
|
requiresConfirmation: external_exports.boolean().optional(),
|
|
10478
|
+
/**
|
|
10479
|
+
* How long this action's work is expected to take, in milliseconds,
|
|
10480
|
+
* DECLARED by the app — not inferred (that is `appilotsInferred`).
|
|
10481
|
+
*
|
|
10482
|
+
* The SDK's post-action wait was a constant: 6s, or 10s when the
|
|
10483
|
+
* generator could prove the handler awaits something. No constant
|
|
10484
|
+
* fits, because app operations run from ~100ms to minutes, and the
|
|
10485
|
+
* failure is silent in both directions — too short and the agent
|
|
10486
|
+
* photographs a loading screen with no controls on it, too long and
|
|
10487
|
+
* every fast press pays for the slowest one. Only the app knows.
|
|
10488
|
+
*
|
|
10489
|
+
* Absent means absent: the SDK keeps its current defaults, so a
|
|
10490
|
+
* document generated before this field existed behaves exactly as it
|
|
10491
|
+
* did. The SDK also clamps the value to its own safety ceiling — a
|
|
10492
|
+
* declared budget is a request, not a licence to hang the session.
|
|
10493
|
+
*/
|
|
10494
|
+
asyncBudgetMs: external_exports.number().int().positive().optional(),
|
|
9308
10495
|
appilotsInferred: appilotsInferredActionSchema.optional()
|
|
9309
10496
|
}).passthrough();
|
|
9310
10497
|
var screenPermissionDescriptorSchema = external_exports.object({
|
|
@@ -9452,9 +10639,16 @@ var createProjectSchema = external_exports.object({
|
|
|
9452
10639
|
// Reference to a tenant-scoped provider (preferred). Optional at
|
|
9453
10640
|
// creation so a project can be created and configured later.
|
|
9454
10641
|
providerId: external_exports.string().min(1).optional(),
|
|
9455
|
-
|
|
10642
|
+
// 16k chars (~4k tokens): um prompt de CS real não cabe em 4096 — o
|
|
10643
|
+
// primeiro cliente de produção chegou com 6.385 e tomou 'Invalid input'
|
|
10644
|
+
// sem saber por quê. O teto continua existindo porque o systemPrompt
|
|
10645
|
+
// entra em TODO hop do relay: é proteção de TPM, não de gosto.
|
|
10646
|
+
systemPrompt: external_exports.string().max(16e3).optional()
|
|
10647
|
+
});
|
|
10648
|
+
var updateProjectSchema = createProjectSchema.partial().extend({
|
|
10649
|
+
providerId: external_exports.string().min(1).nullable().optional(),
|
|
10650
|
+
systemPrompt: external_exports.string().max(16e3).nullable().optional()
|
|
9456
10651
|
});
|
|
9457
|
-
var updateProjectSchema = createProjectSchema.partial();
|
|
9458
10652
|
var providerKindSchema = external_exports.enum(["openai", "anthropic", "custom"]);
|
|
9459
10653
|
var createProviderSchema = external_exports.object({
|
|
9460
10654
|
name: external_exports.string().min(1).max(100),
|
|
@@ -9480,7 +10674,7 @@ var uploadMCPDocumentSchema = external_exports.object({
|
|
|
9480
10674
|
version: external_exports.string().default("1.0"),
|
|
9481
10675
|
content: external_exports.record(external_exports.unknown())
|
|
9482
10676
|
});
|
|
9483
|
-
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
|
|
10677
|
+
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator", "publish"]);
|
|
9484
10678
|
var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
|
|
9485
10679
|
var createApiKeySchema = external_exports.object({
|
|
9486
10680
|
name: external_exports.string().min(1).max(100),
|
|
@@ -9491,8 +10685,8 @@ var createApiKeySchema = external_exports.object({
|
|
|
9491
10685
|
scope: apiKeyScopeSchema.default("sdk"),
|
|
9492
10686
|
environment: apiKeyEnvironmentSchema.optional(),
|
|
9493
10687
|
expiresAt: external_exports.string().datetime().optional()
|
|
9494
|
-
}).refine((v) => v.scope
|
|
9495
|
-
message:
|
|
10688
|
+
}).refine((v) => v.scope === "operator" || !!v.projectId, {
|
|
10689
|
+
message: "projectId is required for SDK and publishing keys",
|
|
9496
10690
|
path: ["projectId"]
|
|
9497
10691
|
});
|
|
9498
10692
|
var boundedString = (max) => external_exports.string().max(max);
|
|
@@ -9517,6 +10711,15 @@ var snapshotInputSchema = external_exports.object({
|
|
|
9517
10711
|
type: boundedString(40).optional(),
|
|
9518
10712
|
required: external_exports.boolean().optional(),
|
|
9519
10713
|
invalid: external_exports.boolean().optional(),
|
|
10714
|
+
/**
|
|
10715
|
+
* Whether the field holds anything, as its own fact rather than an
|
|
10716
|
+
* inference over `value`. The value is the user's and is the first
|
|
10717
|
+
* thing a privacy policy withholds; the existence of a value is the
|
|
10718
|
+
* agent's and is what stops it filling the same field twice.
|
|
10719
|
+
*/
|
|
10720
|
+
filled: external_exports.boolean().optional(),
|
|
10721
|
+
/** This input has keyboard focus right now. */
|
|
10722
|
+
focused: external_exports.boolean().optional(),
|
|
9520
10723
|
inModal: external_exports.boolean().optional()
|
|
9521
10724
|
}).passthrough();
|
|
9522
10725
|
var snapshotButtonSchema = external_exports.object({
|
|
@@ -9672,6 +10875,32 @@ var agentSnapshotSchema = external_exports.object({
|
|
|
9672
10875
|
* it. Optional: older SDKs never clamp and never send it.
|
|
9673
10876
|
*/
|
|
9674
10877
|
truncated: external_exports.boolean().optional(),
|
|
10878
|
+
/**
|
|
10879
|
+
* What moved since the previous observation, computed on the device
|
|
10880
|
+
* because that is the only side holding both snapshots.
|
|
10881
|
+
*
|
|
10882
|
+
* Shape only — counts, booleans, and ids the app declared itself — so
|
|
10883
|
+
* it survives an observation whose content was withheld. ABSENT means
|
|
10884
|
+
* there was no previous observation to compare against; `unchanged:
|
|
10885
|
+
* true` means we compared and nothing moved, which is the strongest
|
|
10886
|
+
* evidence available that an action did nothing. The two must not be
|
|
10887
|
+
* collapsed, for the same reason `truncated` exists.
|
|
10888
|
+
*/
|
|
10889
|
+
delta: external_exports.object({
|
|
10890
|
+
routeChanged: external_exports.boolean().optional(),
|
|
10891
|
+
modalOpened: external_exports.boolean().optional(),
|
|
10892
|
+
modalClosed: external_exports.boolean().optional(),
|
|
10893
|
+
loadingStarted: external_exports.boolean().optional(),
|
|
10894
|
+
loadingFinished: external_exports.boolean().optional(),
|
|
10895
|
+
textsAdded: external_exports.number().int().nonnegative().optional(),
|
|
10896
|
+
textsRemoved: external_exports.number().int().nonnegative().optional(),
|
|
10897
|
+
visibleRowsDelta: external_exports.number().int().optional(),
|
|
10898
|
+
totalRowsDelta: external_exports.number().int().optional(),
|
|
10899
|
+
fieldsNewlyFilled: external_exports.array(boundedString(160)).max(12).optional(),
|
|
10900
|
+
fieldsCleared: external_exports.array(boundedString(160)).max(12).optional(),
|
|
10901
|
+
invalidAppeared: external_exports.boolean().optional(),
|
|
10902
|
+
unchanged: external_exports.boolean().optional()
|
|
10903
|
+
}).passthrough().optional(),
|
|
9675
10904
|
/**
|
|
9676
10905
|
* How many of this screen's controls the client could name, split by
|
|
9677
10906
|
* `identityProvenance`. Diagnostic only — the relay never grounds an
|
|
@@ -9705,7 +10934,19 @@ var agentContextSchema = external_exports.object({
|
|
|
9705
10934
|
rootRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
|
|
9706
10935
|
currentRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
|
|
9707
10936
|
routeNames: external_exports.array(boundedString(200)).max(500).optional(),
|
|
9708
|
-
canGoBack: external_exports.boolean().optional()
|
|
10937
|
+
canGoBack: external_exports.boolean().optional(),
|
|
10938
|
+
/**
|
|
10939
|
+
* The stack a back press pops through, oldest first, ending on
|
|
10940
|
+
* the current screen. `canGoBack` says a back exists; this says
|
|
10941
|
+
* where it goes.
|
|
10942
|
+
*/
|
|
10943
|
+
backStack: external_exports.array(boundedString(200)).max(50).optional(),
|
|
10944
|
+
/**
|
|
10945
|
+
* Screens the session has been on, oldest first. Names only —
|
|
10946
|
+
* params carry record ids and often personal data, and a route
|
|
10947
|
+
* name is structure.
|
|
10948
|
+
*/
|
|
10949
|
+
visited: external_exports.array(boundedString(200)).max(8).optional()
|
|
9709
10950
|
}).passthrough().optional(),
|
|
9710
10951
|
screenMetadata: external_exports.object({
|
|
9711
10952
|
name: boundedString(200).optional(),
|
|
@@ -9811,7 +11052,7 @@ var formFillPayloadSchema = external_exports.object({
|
|
|
9811
11052
|
submitAfterFill: external_exports.boolean().optional()
|
|
9812
11053
|
}).passthrough();
|
|
9813
11054
|
var uiInteractionPayloadSchema = external_exports.object({
|
|
9814
|
-
action: external_exports.enum(["press", "longPress", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
|
|
11055
|
+
action: external_exports.enum(["press", "longPress", "toggle", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
|
|
9815
11056
|
// `targetId` is the canonical server/LLM field. SDK runtimes still
|
|
9816
11057
|
// accept `componentId` as a compatibility alias.
|
|
9817
11058
|
targetId: external_exports.string().min(1),
|
|
@@ -9979,8 +11220,15 @@ var continueAgentSchema = external_exports.object({
|
|
|
9979
11220
|
// issue #169 — the observation the server grounds targets against, now
|
|
9980
11221
|
// validated + bounded at the border instead of z.record(z.unknown()).
|
|
9981
11222
|
context: agentContextSchema.optional(),
|
|
9982
|
-
/**
|
|
9983
|
-
|
|
11223
|
+
/**
|
|
11224
|
+
* Hop counter — server enforces a cap to prevent runaway loops.
|
|
11225
|
+
* The max here must stay ABOVE the server's `MAX_AGENT_HOPS` (10 since
|
|
11226
|
+
* #503, apps/api/src/modules/agents/routes.ts): the first over-budget
|
|
11227
|
+
* hop has to get through validation so the route can answer it with
|
|
11228
|
+
* the friendly automation-limit message instead of a 422. The +3
|
|
11229
|
+
* headroom mirrors what 7/10 was before the recalibration.
|
|
11230
|
+
*/
|
|
11231
|
+
hop: external_exports.number().int().min(1).max(13).optional()
|
|
9984
11232
|
});
|
|
9985
11233
|
var agentAccessLevelSchema = external_exports.enum(["read", "write", "none"]);
|
|
9986
11234
|
var screenPermissionSchema = external_exports.object({
|
|
@@ -10086,15 +11334,18 @@ var themeTokensSchema = external_exports.object({
|
|
|
10086
11334
|
*/
|
|
10087
11335
|
mode: external_exports.enum(["auto", "light", "dark"]).optional()
|
|
10088
11336
|
}).strict();
|
|
11337
|
+
var imageSourceField = external_exports.string().max(500).refine(isSafeImageUrl, {
|
|
11338
|
+
message: "Only http(s) URLs, base64 image data URIs, emoji or asset ids are allowed here"
|
|
11339
|
+
});
|
|
10089
11340
|
var projectPersonalizationSchema = external_exports.object({
|
|
10090
11341
|
/** Tone / persona instructions appended to the system prompt. */
|
|
10091
11342
|
personaPrompt: external_exports.string().max(2048).nullable(),
|
|
10092
11343
|
/** Display name in the chat header (e.g. "Aria"). */
|
|
10093
11344
|
assistantName: external_exports.string().max(60).nullable(),
|
|
10094
11345
|
/** URL or remote asset id for the avatar shown next to assistant turns. */
|
|
10095
|
-
assistantAvatar:
|
|
11346
|
+
assistantAvatar: imageSourceField.nullable(),
|
|
10096
11347
|
/** Emoji or image URL for the empty-state icon. Auto-detected by prefix. */
|
|
10097
|
-
emptyStateIcon:
|
|
11348
|
+
emptyStateIcon: imageSourceField.nullable(),
|
|
10098
11349
|
/** First-message text shown in the empty state. */
|
|
10099
11350
|
welcomeMessage: external_exports.string().max(500).nullable(),
|
|
10100
11351
|
/** Title rendered at the top of the chat. */
|
|
@@ -10108,20 +11359,20 @@ var projectPersonalizationSchema = external_exports.object({
|
|
|
10108
11359
|
/** Chat-open FAB background color. Null falls back to theme.colors.primary. */
|
|
10109
11360
|
triggerButtonColor: hexColorSchema.nullable(),
|
|
10110
11361
|
/** Image URL rendered inside the FAB instead of the default chat-bubble icon. */
|
|
10111
|
-
triggerButtonImageUrl:
|
|
11362
|
+
triggerButtonImageUrl: imageSourceField.nullable()
|
|
10112
11363
|
});
|
|
10113
11364
|
var updateProjectPersonalizationSchema = external_exports.object({
|
|
10114
11365
|
personaPrompt: external_exports.string().max(2048).nullable().optional(),
|
|
10115
11366
|
assistantName: external_exports.string().max(60).nullable().optional(),
|
|
10116
|
-
assistantAvatar:
|
|
10117
|
-
emptyStateIcon:
|
|
11367
|
+
assistantAvatar: imageSourceField.nullable().optional(),
|
|
11368
|
+
emptyStateIcon: imageSourceField.nullable().optional(),
|
|
10118
11369
|
welcomeMessage: external_exports.string().max(500).nullable().optional(),
|
|
10119
11370
|
chatTitle: external_exports.string().max(60).nullable().optional(),
|
|
10120
11371
|
poweredByVisible: external_exports.boolean().optional(),
|
|
10121
11372
|
theme: themeTokensSchema.nullable().optional(),
|
|
10122
11373
|
defaultLocale: localeSchema.nullable().optional(),
|
|
10123
11374
|
triggerButtonColor: hexColorSchema.nullable().optional(),
|
|
10124
|
-
triggerButtonImageUrl:
|
|
11375
|
+
triggerButtonImageUrl: imageSourceField.nullable().optional()
|
|
10125
11376
|
}).strict();
|
|
10126
11377
|
var sandboxObservationSchema = external_exports.object({
|
|
10127
11378
|
route: external_exports.string().max(200).optional(),
|
|
@@ -10141,7 +11392,19 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
10141
11392
|
type: external_exports.string().max(40).optional(),
|
|
10142
11393
|
placeholder: external_exports.string().max(200).optional(),
|
|
10143
11394
|
/** Inline validation state (SDK snapshot field). */
|
|
10144
|
-
invalid: external_exports.boolean().optional()
|
|
11395
|
+
invalid: external_exports.boolean().optional(),
|
|
11396
|
+
/**
|
|
11397
|
+
* Whether the field holds anything, as its own fact rather than
|
|
11398
|
+
* an inference over `value`.
|
|
11399
|
+
*
|
|
11400
|
+
* Here because the eval posts through this schema: a fact the
|
|
11401
|
+
* SDK produces and this contract has no word for is a fact the
|
|
11402
|
+
* corpus can never grade the agent on. `invalid` and `required`
|
|
11403
|
+
* were already here; these two were the half that was missing.
|
|
11404
|
+
*/
|
|
11405
|
+
filled: external_exports.boolean().optional(),
|
|
11406
|
+
/** This input has keyboard focus right now. */
|
|
11407
|
+
focused: external_exports.boolean().optional()
|
|
10145
11408
|
})
|
|
10146
11409
|
).max(100).optional(),
|
|
10147
11410
|
buttons: external_exports.array(
|
|
@@ -10599,7 +11862,7 @@ var manifestSchema = external_exports.object({
|
|
|
10599
11862
|
navigation: navigationGraphSchema.partial().optional()
|
|
10600
11863
|
}).passthrough();
|
|
10601
11864
|
async function loadManifest(rootDir, manifestPath) {
|
|
10602
|
-
const resolvedPath =
|
|
11865
|
+
const resolvedPath = import_node_path4.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
|
|
10603
11866
|
let raw;
|
|
10604
11867
|
try {
|
|
10605
11868
|
raw = await (0, import_promises3.readFile)(resolvedPath, "utf-8");
|
|
@@ -10748,7 +12011,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10748
12011
|
const agentReadyScreens = mergedScreens.map(
|
|
10749
12012
|
(screen) => enrichScreenForAgent({
|
|
10750
12013
|
...screen,
|
|
10751
|
-
filePath: screen.filePath ?
|
|
12014
|
+
filePath: screen.filePath ? import_node_path5.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
|
|
10752
12015
|
})
|
|
10753
12016
|
);
|
|
10754
12017
|
const projectInfo = await this.getProjectInfo();
|
|
@@ -10772,11 +12035,11 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10772
12035
|
}
|
|
10773
12036
|
};
|
|
10774
12037
|
const serialized = JSON.stringify(document, null, 2);
|
|
10775
|
-
const checksum = this.calculateChecksum(
|
|
10776
|
-
const filePath =
|
|
12038
|
+
const checksum = this.calculateChecksum(serializeForChecksum(document));
|
|
12039
|
+
const filePath = import_node_path5.default.resolve(outputDir, `mcp-document.${this.options.format}`);
|
|
10777
12040
|
await (0, import_promises4.writeFile)(filePath, serialized, "utf-8");
|
|
10778
12041
|
console.log(`[MCPGenerator] Document written to: ${filePath}`);
|
|
10779
|
-
const checksumFilePath =
|
|
12042
|
+
const checksumFilePath = import_node_path5.default.resolve(outputDir, ".appilots-checksum");
|
|
10780
12043
|
await (0, import_promises4.writeFile)(checksumFilePath, checksum, "utf-8");
|
|
10781
12044
|
console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
|
|
10782
12045
|
console.log("[MCPGenerator] Generation complete!");
|
|
@@ -10784,7 +12047,8 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10784
12047
|
document,
|
|
10785
12048
|
filePath,
|
|
10786
12049
|
format: this.options.format,
|
|
10787
|
-
checksum
|
|
12050
|
+
checksum,
|
|
12051
|
+
...analyzed.diagnostics ? { diagnostics: analyzed.diagnostics } : {}
|
|
10788
12052
|
};
|
|
10789
12053
|
}
|
|
10790
12054
|
/**
|
|
@@ -10796,7 +12060,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10796
12060
|
* after calling `generate()`.
|
|
10797
12061
|
*/
|
|
10798
12062
|
static async readPreviousChecksum(outputDir) {
|
|
10799
|
-
const checksumFilePath =
|
|
12063
|
+
const checksumFilePath = import_node_path5.default.resolve(outputDir, ".appilots-checksum");
|
|
10800
12064
|
try {
|
|
10801
12065
|
const content = await (0, import_promises4.readFile)(checksumFilePath, "utf-8");
|
|
10802
12066
|
return content.trim() || null;
|
|
@@ -10815,7 +12079,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10815
12079
|
*/
|
|
10816
12080
|
async getProjectInfo() {
|
|
10817
12081
|
try {
|
|
10818
|
-
const packageJsonPath =
|
|
12082
|
+
const packageJsonPath = import_node_path5.default.resolve(this.analyzerConfig.rootDir, "package.json");
|
|
10819
12083
|
const packageJsonContent = await (0, import_promises4.readFile)(packageJsonPath, "utf-8");
|
|
10820
12084
|
const packageJson = JSON.parse(packageJsonContent);
|
|
10821
12085
|
return {
|
|
@@ -10890,8 +12154,8 @@ function reportMetadataLint(document, strict) {
|
|
|
10890
12154
|
}
|
|
10891
12155
|
|
|
10892
12156
|
// src/cli/utils/remote-drift.ts
|
|
10893
|
-
var
|
|
10894
|
-
var
|
|
12157
|
+
var import_node_fs3 = require("fs");
|
|
12158
|
+
var import_node_path6 = require("path");
|
|
10895
12159
|
var STUB_RATIO = 0.5;
|
|
10896
12160
|
function compareWithRemote(local, remote) {
|
|
10897
12161
|
if (remote.screensCount === local.screens) return null;
|
|
@@ -10904,7 +12168,7 @@ function compareWithRemote(local, remote) {
|
|
|
10904
12168
|
}
|
|
10905
12169
|
function readLocalDocumentSummary(outputDir) {
|
|
10906
12170
|
try {
|
|
10907
|
-
const raw = (0,
|
|
12171
|
+
const raw = (0, import_node_fs3.readFileSync)((0, import_node_path6.join)(outputDir, "mcp-document.json"), "utf-8");
|
|
10908
12172
|
const parsed = JSON.parse(raw);
|
|
10909
12173
|
if (typeof parsed?.metadata?.totalScreens !== "number") return null;
|
|
10910
12174
|
return { screens: parsed.metadata.totalScreens };
|
|
@@ -10989,14 +12253,18 @@ function assertDocumentHasScreens(document, options = {}) {
|
|
|
10989
12253
|
}
|
|
10990
12254
|
error("No screens found \u2014 this application map would give the agent nothing to navigate.");
|
|
10991
12255
|
info("");
|
|
10992
|
-
info("The analyzer
|
|
10993
|
-
info("
|
|
12256
|
+
info("The analyzer reads React Navigation \u2014 <X.Navigator> JSX and the static");
|
|
12257
|
+
info("`createXNavigator({ screens })` config \u2014 and Expo Router directory trees.");
|
|
12258
|
+
info("It found no screen in any of them.");
|
|
10994
12259
|
info("");
|
|
10995
12260
|
info("The two usual causes:");
|
|
10996
|
-
info(" \u2022 The
|
|
10997
|
-
info("
|
|
10998
|
-
info("
|
|
10999
|
-
info("
|
|
12261
|
+
info(" \u2022 The screens live outside the analyzed paths, or are named in a way");
|
|
12262
|
+
info(" the screen filter does not match \u2014 check `include`, `exclude` and");
|
|
12263
|
+
info(" `screenPatterns` in your Appilots config. The scan summary printed");
|
|
12264
|
+
info(" above says how many files were reached and how many were filtered.");
|
|
12265
|
+
info(" \u2022 The app uses a router the analyzer does not read yet, such as");
|
|
12266
|
+
info(" react-native-navigation (Wix). Declaring the screens works for any");
|
|
12267
|
+
info(" router \u2014 see the escape hatch below.");
|
|
11000
12268
|
info("");
|
|
11001
12269
|
info("The escape hatch, which works for any router: declare your screens in");
|
|
11002
12270
|
info("an `appilots.manifest.json` at the project root. Its screens are merged");
|
|
@@ -11043,7 +12311,7 @@ function syncCommand() {
|
|
|
11043
12311
|
const outputDir = config.outputDir || ".appilots";
|
|
11044
12312
|
let previousChecksum = "";
|
|
11045
12313
|
try {
|
|
11046
|
-
const checksumPath = (0,
|
|
12314
|
+
const checksumPath = (0, import_node_path7.join)(outputDir, ".appilots-checksum");
|
|
11047
12315
|
previousChecksum = (await (0, import_promises5.readFile)(checksumPath, "utf-8")).trim();
|
|
11048
12316
|
} catch {
|
|
11049
12317
|
}
|
|
@@ -11170,7 +12438,7 @@ function syncCommand() {
|
|
|
11170
12438
|
|
|
11171
12439
|
// src/cli/commands/watch.ts
|
|
11172
12440
|
var import_commander3 = require("commander");
|
|
11173
|
-
var
|
|
12441
|
+
var import_node_fs4 = require("fs");
|
|
11174
12442
|
function watchCommand() {
|
|
11175
12443
|
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) => {
|
|
11176
12444
|
try {
|
|
@@ -11217,7 +12485,7 @@ function watchCommand() {
|
|
|
11217
12485
|
dim("Press Ctrl+C to stop\n");
|
|
11218
12486
|
let debounceTimer = null;
|
|
11219
12487
|
const DEBOUNCE_MS = 500;
|
|
11220
|
-
const watcher = (0,
|
|
12488
|
+
const watcher = (0, import_node_fs4.watch)(process.cwd(), { recursive: true }, async (eventType, filename) => {
|
|
11221
12489
|
if (!filename) return;
|
|
11222
12490
|
if (filename.startsWith(".") || filename.includes("node_modules") || filename.includes(config.outputDir || ".appilots")) {
|
|
11223
12491
|
return;
|
|
@@ -11269,6 +12537,95 @@ function watchCommand() {
|
|
|
11269
12537
|
|
|
11270
12538
|
// src/cli/commands/generate.ts
|
|
11271
12539
|
var import_commander4 = require("commander");
|
|
12540
|
+
|
|
12541
|
+
// src/cli/utils/reach-report.ts
|
|
12542
|
+
function actionableScreens(document) {
|
|
12543
|
+
return (document.screens ?? []).filter(
|
|
12544
|
+
(screen) => (screen.actions?.length ?? 0) > 0 || (screen.forms?.length ?? 0) > 0 || (screen.targets?.length ?? 0) > 0
|
|
12545
|
+
).length;
|
|
12546
|
+
}
|
|
12547
|
+
function pct(part, whole) {
|
|
12548
|
+
if (whole <= 0) return "\u2014";
|
|
12549
|
+
return `${Math.round(100 * part / whole)}%`;
|
|
12550
|
+
}
|
|
12551
|
+
function reportReach(input) {
|
|
12552
|
+
const { document, navigation } = input;
|
|
12553
|
+
const screens = document.screens?.length ?? 0;
|
|
12554
|
+
const useful = actionableScreens(document);
|
|
12555
|
+
const filteredOut = document.metadata?.screensFilteredOut ?? 0;
|
|
12556
|
+
info("");
|
|
12557
|
+
info("Alcance da varredura");
|
|
12558
|
+
info(
|
|
12559
|
+
` telas ${screens} de ${document.metadata?.analyzedFiles ?? 0} arquivos analisados` + (filteredOut > 0 ? `, ${filteredOut} fora pelo filtro de tela` : "")
|
|
12560
|
+
);
|
|
12561
|
+
info(` com aford\xE2ncia ${useful} (${pct(useful, screens)}) \u2014 a\xE7\xE3o, formul\xE1rio ou target`);
|
|
12562
|
+
if (navigation) {
|
|
12563
|
+
info(
|
|
12564
|
+
` navega\xE7\xE3o ${navigation.filesWithEvidence} de ${navigation.filesScanned} arquivos com evid\xEAncia, ${navigation.navigatorsWithJsx} navegador(es) no documento`
|
|
12565
|
+
);
|
|
12566
|
+
info(
|
|
12567
|
+
` rotas ${navigation.routes}, ${navigation.routesLinkedToFile} (${pct(navigation.routesLinkedToFile, navigation.routes)}) ligadas a um arquivo de tela`
|
|
12568
|
+
);
|
|
12569
|
+
if (navigation.fileBasedRouter) {
|
|
12570
|
+
info(
|
|
12571
|
+
` roteador ${navigation.fileBasedRouter.kind} em "${navigation.fileBasedRouter.routeDir}"`
|
|
12572
|
+
);
|
|
12573
|
+
}
|
|
12574
|
+
}
|
|
12575
|
+
const notes = [];
|
|
12576
|
+
if (navigation && navigation.routes > 0 && useful > navigation.routes * 5) {
|
|
12577
|
+
notes.push([
|
|
12578
|
+
`${useful} tela(s) com aford\xE2ncia para apenas ${navigation.routes} rota(s).`,
|
|
12579
|
+
"A varredura achou o que tocar em muito mais lugares do que sabe alcan\xE7ar.",
|
|
12580
|
+
`S\xF3 ${navigation.filesWithEvidence} de ${navigation.filesScanned} arquivos varridos mencionam navega\xE7\xE3o \u2014`,
|
|
12581
|
+
"se o roteador deste app mora fora deles, `navigationInclude` no `.appilotsrc`",
|
|
12582
|
+
"estende a varredura sem mexer no resto."
|
|
12583
|
+
]);
|
|
12584
|
+
}
|
|
12585
|
+
if (navigation && navigation.routes === 0 && useful > 0) {
|
|
12586
|
+
notes.push([
|
|
12587
|
+
`${useful} tela(s) com aford\xE2ncia e nenhuma rota: o agente v\xEA o que tocar e n\xE3o sabe chegar l\xE1.`,
|
|
12588
|
+
"O extractor l\xEA React Navigation (JSX e configura\xE7\xE3o est\xE1tica) e expo-router.",
|
|
12589
|
+
"Se o app usa outro roteador, declare as telas em `appilots.manifest.json` \u2014",
|
|
12590
|
+
"elas s\xE3o mescladas por cima do que a an\xE1lise achou."
|
|
12591
|
+
]);
|
|
12592
|
+
} else if (navigation && navigation.routes > 0 && navigation.routesLinkedToFile < navigation.routes / 2) {
|
|
12593
|
+
notes.push([
|
|
12594
|
+
`${navigation.routes - navigation.routesLinkedToFile} de ${navigation.routes} rotas n\xE3o dizem qual arquivo montam.`,
|
|
12595
|
+
"A liga\xE7\xE3o vem de `component={X}`. Formas indiretas \u2014 `getComponent={() => X}`,",
|
|
12596
|
+
"componente constru\xEDdo em runtime \u2014 n\xE3o resolvem, e o agente fica com destinos vazios."
|
|
12597
|
+
]);
|
|
12598
|
+
}
|
|
12599
|
+
if (screens > 0 && useful === 0) {
|
|
12600
|
+
notes.push([
|
|
12601
|
+
`${screens} tela(s) encontradas e nenhuma com aford\xE2ncia.`,
|
|
12602
|
+
"O extractor mira `onPress`/`onChangeText` com um locator endere\xE7\xE1vel:",
|
|
12603
|
+
"`testID`, `appilotsId`, `accessibilityLabel` ou r\xF3tulo literal.",
|
|
12604
|
+
"Sem nenhum deles n\xE3o h\xE1 como o agente apontar para o elemento."
|
|
12605
|
+
]);
|
|
12606
|
+
}
|
|
12607
|
+
if (filteredOut > 0 && screens > 0 && filteredOut > screens * 9) {
|
|
12608
|
+
notes.push([
|
|
12609
|
+
`${filteredOut} arquivos ficaram fora do filtro de tela para ${screens} dentro.`,
|
|
12610
|
+
"Se as telas deste app moram noutro lugar, uma linha de `screenPatterns` no",
|
|
12611
|
+
"`.appilotsrc` aponta onde \u2014 \xE9 o degrau mais barato da escada."
|
|
12612
|
+
]);
|
|
12613
|
+
}
|
|
12614
|
+
if (navigation && navigation.filesScanned > 0 && navigation.filesWithEvidence === 0) {
|
|
12615
|
+
notes.push([
|
|
12616
|
+
`Nenhum dos ${navigation.filesScanned} arquivos varridos menciona navega\xE7\xE3o.`,
|
|
12617
|
+
"Confira `include`/`exclude` no `.appilotsrc`: o extractor pode n\xE3o estar",
|
|
12618
|
+
"alcan\xE7ando o diret\xF3rio onde o roteador \xE9 declarado."
|
|
12619
|
+
]);
|
|
12620
|
+
}
|
|
12621
|
+
for (const note of notes) {
|
|
12622
|
+
info("");
|
|
12623
|
+
warn(note[0]);
|
|
12624
|
+
for (const line of note.slice(1)) info(` ${line}`);
|
|
12625
|
+
}
|
|
12626
|
+
}
|
|
12627
|
+
|
|
12628
|
+
// src/cli/commands/generate.ts
|
|
11272
12629
|
function generateCommand() {
|
|
11273
12630
|
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(
|
|
11274
12631
|
"--strict-metadata",
|
|
@@ -11285,7 +12642,7 @@ function generateCommand() {
|
|
|
11285
12642
|
spinner.start();
|
|
11286
12643
|
let loaded = null;
|
|
11287
12644
|
try {
|
|
11288
|
-
loaded = loadConfig(warn);
|
|
12645
|
+
loaded = loadConfig(warn, { requireApiKey: false });
|
|
11289
12646
|
} catch {
|
|
11290
12647
|
loaded = null;
|
|
11291
12648
|
}
|
|
@@ -11293,11 +12650,21 @@ function generateCommand() {
|
|
|
11293
12650
|
rootDir: process.cwd(),
|
|
11294
12651
|
outputDir: options.output || ".appilots",
|
|
11295
12652
|
format: "json",
|
|
12653
|
+
strictScreens: loaded?.strictScreens,
|
|
12654
|
+
screenPatterns: loaded?.screenPatterns,
|
|
12655
|
+
include: loaded?.include,
|
|
12656
|
+
exclude: loaded?.exclude,
|
|
12657
|
+
navigationInclude: loaded?.navigationInclude,
|
|
12658
|
+
navigationExclude: loaded?.navigationExclude,
|
|
11296
12659
|
platform: loaded?.platform,
|
|
11297
12660
|
manifestPath: loaded?.manifestPath
|
|
11298
12661
|
});
|
|
11299
12662
|
const output = await generator.generate();
|
|
11300
12663
|
spinner.stop();
|
|
12664
|
+
reportReach({
|
|
12665
|
+
document: output.document,
|
|
12666
|
+
...output.diagnostics?.navigation ? { navigation: output.diagnostics.navigation } : {}
|
|
12667
|
+
});
|
|
11301
12668
|
assertDocumentHasScreens(output.document, {
|
|
11302
12669
|
rootDir: process.cwd(),
|
|
11303
12670
|
allowEmpty: Boolean(options.allowEmpty)
|
|
@@ -11317,19 +12684,18 @@ function generateCommand() {
|
|
|
11317
12684
|
|
|
11318
12685
|
// src/cli/commands/annotate.ts
|
|
11319
12686
|
var import_promises6 = require("fs/promises");
|
|
11320
|
-
var
|
|
12687
|
+
var import_node_path9 = __toESM(require("path"));
|
|
11321
12688
|
var import_commander5 = require("commander");
|
|
11322
|
-
var import_fast_glob6 = __toESM(require("fast-glob"));
|
|
11323
12689
|
|
|
11324
12690
|
// src/annotate/sites.ts
|
|
11325
12691
|
var import_traverse12 = __toESM(require("@babel/traverse"));
|
|
11326
|
-
var
|
|
12692
|
+
var t14 = __toESM(require("@babel/types"));
|
|
11327
12693
|
|
|
11328
12694
|
// src/annotate/forwarding.ts
|
|
11329
|
-
var
|
|
11330
|
-
var
|
|
12695
|
+
var import_node_path8 = __toESM(require("path"));
|
|
12696
|
+
var import_node_fs5 = require("fs");
|
|
11331
12697
|
var import_traverse11 = __toESM(require("@babel/traverse"));
|
|
11332
|
-
var
|
|
12698
|
+
var t13 = __toESM(require("@babel/types"));
|
|
11333
12699
|
var traverse11 = import_traverse11.default.default ?? import_traverse11.default;
|
|
11334
12700
|
var RN_TESTID_CARRIERS = /* @__PURE__ */ new Set([
|
|
11335
12701
|
"View",
|
|
@@ -11361,10 +12727,10 @@ function collectImports(ast) {
|
|
|
11361
12727
|
ImportDeclaration(nodePath) {
|
|
11362
12728
|
const source = nodePath.node.source.value;
|
|
11363
12729
|
for (const spec of nodePath.node.specifiers) {
|
|
11364
|
-
if (
|
|
11365
|
-
const imported =
|
|
12730
|
+
if (t13.isImportSpecifier(spec)) {
|
|
12731
|
+
const imported = t13.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
|
|
11366
12732
|
out.set(spec.local.name, { source, local: spec.local.name, imported });
|
|
11367
|
-
} else if (
|
|
12733
|
+
} else if (t13.isImportDefaultSpecifier(spec)) {
|
|
11368
12734
|
out.set(spec.local.name, { source, local: spec.local.name, imported: "default" });
|
|
11369
12735
|
}
|
|
11370
12736
|
}
|
|
@@ -11372,17 +12738,17 @@ function collectImports(ast) {
|
|
|
11372
12738
|
});
|
|
11373
12739
|
return out;
|
|
11374
12740
|
}
|
|
11375
|
-
var
|
|
12741
|
+
var EXTENSIONS2 = [".tsx", ".ts", ".jsx", ".js"];
|
|
11376
12742
|
function resolveRelativeImport(fromFile, source) {
|
|
11377
12743
|
if (!source.startsWith(".")) return void 0;
|
|
11378
|
-
const base =
|
|
11379
|
-
for (const ext of
|
|
12744
|
+
const base = import_node_path8.default.resolve(import_node_path8.default.dirname(fromFile), source);
|
|
12745
|
+
for (const ext of EXTENSIONS2) {
|
|
11380
12746
|
const candidate = `${base}${ext}`;
|
|
11381
|
-
if ((0,
|
|
12747
|
+
if ((0, import_node_fs5.existsSync)(candidate)) return candidate;
|
|
11382
12748
|
}
|
|
11383
|
-
for (const ext of
|
|
11384
|
-
const candidate =
|
|
11385
|
-
if ((0,
|
|
12749
|
+
for (const ext of EXTENSIONS2) {
|
|
12750
|
+
const candidate = import_node_path8.default.join(base, `index${ext}`);
|
|
12751
|
+
if ((0, import_node_fs5.existsSync)(candidate)) return candidate;
|
|
11386
12752
|
}
|
|
11387
12753
|
return void 0;
|
|
11388
12754
|
}
|
|
@@ -11397,7 +12763,7 @@ function inspectModule(file2, exportName, depth = 0) {
|
|
|
11397
12763
|
if (depth > 2) return none;
|
|
11398
12764
|
let source;
|
|
11399
12765
|
try {
|
|
11400
|
-
source = (0,
|
|
12766
|
+
source = (0, import_node_fs5.readFileSync)(file2, "utf-8");
|
|
11401
12767
|
} catch {
|
|
11402
12768
|
return none;
|
|
11403
12769
|
}
|
|
@@ -11418,7 +12784,7 @@ function inspectModule(file2, exportName, depth = 0) {
|
|
|
11418
12784
|
const from = nodePath.node.source?.value;
|
|
11419
12785
|
if (!from) return;
|
|
11420
12786
|
const named = nodePath.node.specifiers.some(
|
|
11421
|
-
(spec) =>
|
|
12787
|
+
(spec) => t13.isExportSpecifier(spec) && (t13.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === exportName
|
|
11422
12788
|
);
|
|
11423
12789
|
if (named) barrelTarget = from;
|
|
11424
12790
|
},
|
|
@@ -11427,11 +12793,11 @@ function inspectModule(file2, exportName, depth = 0) {
|
|
|
11427
12793
|
},
|
|
11428
12794
|
// `interface CardProps { testID?: string }` / `type Props = { testID… }`
|
|
11429
12795
|
TSPropertySignature(nodePath) {
|
|
11430
|
-
if (
|
|
12796
|
+
if (t13.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
|
|
11431
12797
|
},
|
|
11432
12798
|
// `function Card({ testID }) {}` — the destructured parameter.
|
|
11433
12799
|
ObjectProperty(nodePath) {
|
|
11434
|
-
if (
|
|
12800
|
+
if (t13.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
|
|
11435
12801
|
},
|
|
11436
12802
|
// `<View {...props} />` — anything spread onto JSX carries it along.
|
|
11437
12803
|
JSXSpreadAttribute() {
|
|
@@ -11442,17 +12808,17 @@ function inspectModule(file2, exportName, depth = 0) {
|
|
|
11442
12808
|
// that is one hardcoded name shared by every instance, which is the
|
|
11443
12809
|
// duplicate-id problem rather than a solution to it.
|
|
11444
12810
|
JSXAttribute(nodePath) {
|
|
11445
|
-
const name =
|
|
12811
|
+
const name = t13.isJSXIdentifier(nodePath.node.name) ? nodePath.node.name.name : "";
|
|
11446
12812
|
if (name !== "testID" && name !== "accessibilityLabel") return;
|
|
11447
12813
|
const value = nodePath.node.value;
|
|
11448
|
-
if (!
|
|
11449
|
-
if (
|
|
12814
|
+
if (!t13.isJSXExpressionContainer(value)) return;
|
|
12815
|
+
if (t13.isStringLiteral(value.expression)) return;
|
|
11450
12816
|
derives = true;
|
|
11451
12817
|
},
|
|
11452
12818
|
// `useAppilotsTarget(`${groupId}-${option.value}`, …)`
|
|
11453
12819
|
CallExpression(nodePath) {
|
|
11454
12820
|
const callee = nodePath.node.callee;
|
|
11455
|
-
if (
|
|
12821
|
+
if (t13.isIdentifier(callee) && IDENTITY_HOOKS.has(callee.name)) derives = true;
|
|
11456
12822
|
}
|
|
11457
12823
|
});
|
|
11458
12824
|
if (forwards || derives) return { forwards, derives };
|
|
@@ -11537,18 +12903,18 @@ function hasLabelText(element) {
|
|
|
11537
12903
|
function namedHandler(element, prop) {
|
|
11538
12904
|
if (!prop) return void 0;
|
|
11539
12905
|
const attr = findJsxAttribute(element, prop);
|
|
11540
|
-
if (!attr || !
|
|
11541
|
-
return
|
|
12906
|
+
if (!attr || !t14.isJSXExpressionContainer(attr.value)) return void 0;
|
|
12907
|
+
return t14.isIdentifier(attr.value.expression) ? attr.value.expression.name : void 0;
|
|
11542
12908
|
}
|
|
11543
12909
|
var ANNOTATABLE_ROLES = /* @__PURE__ */ new Set(["button", "input", "select", "toggle", "date", "list"]);
|
|
11544
12910
|
function isPerItemRender(nodePath) {
|
|
11545
12911
|
let current = nodePath.parentPath;
|
|
11546
12912
|
while (current) {
|
|
11547
12913
|
const node = current.node;
|
|
11548
|
-
if (
|
|
12914
|
+
if (t14.isCallExpression(node) && t14.isMemberExpression(node.callee) && t14.isIdentifier(node.callee.property) && (node.callee.property.name === "map" || node.callee.property.name === "flatMap")) {
|
|
11549
12915
|
return true;
|
|
11550
12916
|
}
|
|
11551
|
-
if (
|
|
12917
|
+
if (t14.isJSXAttribute(node) && t14.isJSXIdentifier(node.name) && /^render[A-Z]|^ListHeaderComponent$|^ListFooterComponent$/.test(node.name.name) && node.name.name !== "renderScrollComponent") {
|
|
11552
12918
|
return /^renderItem$|^renderSectionHeader$|^renderSectionFooter$/.test(node.name.name);
|
|
11553
12919
|
}
|
|
11554
12920
|
current = current.parentPath;
|
|
@@ -11578,7 +12944,7 @@ function findAnnotationSites(source, file2) {
|
|
|
11578
12944
|
traverse12(ast, {
|
|
11579
12945
|
JSXOpeningElement(nodePath) {
|
|
11580
12946
|
const element = nodePath.node;
|
|
11581
|
-
if (!
|
|
12947
|
+
if (!t14.isJSXIdentifier(element.name)) return;
|
|
11582
12948
|
const component = element.name.name;
|
|
11583
12949
|
const role = classifyJsxComponent(component, element);
|
|
11584
12950
|
if (!ANNOTATABLE_ROLES.has(role)) return;
|
|
@@ -11660,10 +13026,10 @@ function applyToFile(source, sites) {
|
|
|
11660
13026
|
}
|
|
11661
13027
|
|
|
11662
13028
|
// src/annotate/forward.ts
|
|
11663
|
-
var
|
|
13029
|
+
var import_node_fs6 = require("fs");
|
|
11664
13030
|
var import_magic_string2 = __toESM(require("magic-string"));
|
|
11665
13031
|
var import_traverse13 = __toESM(require("@babel/traverse"));
|
|
11666
|
-
var
|
|
13032
|
+
var t15 = __toESM(require("@babel/types"));
|
|
11667
13033
|
var traverse13 = import_traverse13.default.default ?? import_traverse13.default;
|
|
11668
13034
|
function findComponent(ast, name) {
|
|
11669
13035
|
let found;
|
|
@@ -11674,9 +13040,9 @@ function findComponent(ast, name) {
|
|
|
11674
13040
|
}
|
|
11675
13041
|
},
|
|
11676
13042
|
VariableDeclarator(nodePath) {
|
|
11677
|
-
if (!
|
|
13043
|
+
if (!t15.isIdentifier(nodePath.node.id) || nodePath.node.id.name !== name) return;
|
|
11678
13044
|
const init = nodePath.node.init;
|
|
11679
|
-
if (
|
|
13045
|
+
if (t15.isArrowFunctionExpression(init) || t15.isFunctionExpression(init)) {
|
|
11680
13046
|
found ??= { params: init.params, body: init.body };
|
|
11681
13047
|
}
|
|
11682
13048
|
}
|
|
@@ -11688,26 +13054,26 @@ function findReturnedRoots(body) {
|
|
|
11688
13054
|
let unsupported = false;
|
|
11689
13055
|
const record = (node) => {
|
|
11690
13056
|
if (!node) return;
|
|
11691
|
-
if (
|
|
11692
|
-
if (
|
|
13057
|
+
if (t15.isParenthesizedExpression(node)) return record(node.expression);
|
|
13058
|
+
if (t15.isJSXElement(node)) {
|
|
11693
13059
|
roots.push(node.openingElement);
|
|
11694
13060
|
return;
|
|
11695
13061
|
}
|
|
11696
|
-
if (
|
|
13062
|
+
if (t15.isConditionalExpression(node)) {
|
|
11697
13063
|
record(node.consequent);
|
|
11698
13064
|
record(node.alternate);
|
|
11699
13065
|
return;
|
|
11700
13066
|
}
|
|
11701
|
-
if (
|
|
13067
|
+
if (t15.isJSXFragment(node)) {
|
|
11702
13068
|
unsupported = true;
|
|
11703
13069
|
return;
|
|
11704
13070
|
}
|
|
11705
|
-
if (
|
|
13071
|
+
if (t15.isNullLiteral(node)) return;
|
|
11706
13072
|
unsupported = true;
|
|
11707
13073
|
};
|
|
11708
|
-
if (
|
|
13074
|
+
if (t15.isBlockStatement(body)) {
|
|
11709
13075
|
traverse13(
|
|
11710
|
-
|
|
13076
|
+
t15.file(t15.program([t15.expressionStatement(t15.functionExpression(null, [], body))])),
|
|
11711
13077
|
{
|
|
11712
13078
|
ReturnStatement(nodePath) {
|
|
11713
13079
|
record(nodePath.node.argument);
|
|
@@ -11721,7 +13087,7 @@ function findReturnedRoots(body) {
|
|
|
11721
13087
|
return roots;
|
|
11722
13088
|
}
|
|
11723
13089
|
function isCarrierRoot(element, imports) {
|
|
11724
|
-
if (!
|
|
13090
|
+
if (!t15.isJSXIdentifier(element.name)) return false;
|
|
11725
13091
|
const name = element.name.name;
|
|
11726
13092
|
const origin = imports.get(name);
|
|
11727
13093
|
if (origin?.source !== "react-native") return false;
|
|
@@ -11729,7 +13095,7 @@ function isCarrierRoot(element, imports) {
|
|
|
11729
13095
|
}
|
|
11730
13096
|
function hasProp(element, name) {
|
|
11731
13097
|
return element.attributes.some(
|
|
11732
|
-
(attr) =>
|
|
13098
|
+
(attr) => t15.isJSXAttribute(attr) && t15.isJSXIdentifier(attr.name) && attr.name.name === name
|
|
11733
13099
|
);
|
|
11734
13100
|
}
|
|
11735
13101
|
function indentOf(source, offset) {
|
|
@@ -11760,7 +13126,7 @@ function followBarrel(file2, ast, component) {
|
|
|
11760
13126
|
const from = nodePath.node.source?.value;
|
|
11761
13127
|
if (!from) return;
|
|
11762
13128
|
const named = nodePath.node.specifiers.some(
|
|
11763
|
-
(spec) =>
|
|
13129
|
+
(spec) => t15.isExportSpecifier(spec) && (t15.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === component
|
|
11764
13130
|
);
|
|
11765
13131
|
if (named) target ??= from;
|
|
11766
13132
|
}
|
|
@@ -11771,7 +13137,7 @@ function planForward(file2, component, depth = 0) {
|
|
|
11771
13137
|
const base = { file: file2, component, edits: [], roots: 0 };
|
|
11772
13138
|
let source;
|
|
11773
13139
|
try {
|
|
11774
|
-
source = (0,
|
|
13140
|
+
source = (0, import_node_fs6.readFileSync)(file2, "utf-8");
|
|
11775
13141
|
} catch {
|
|
11776
13142
|
return { ...base, refusal: `cannot read ${file2}` };
|
|
11777
13143
|
}
|
|
@@ -11788,7 +13154,7 @@ function planForward(file2, component, depth = 0) {
|
|
|
11788
13154
|
return { ...base, refusal: `no function named ${component} in this file` };
|
|
11789
13155
|
}
|
|
11790
13156
|
const param = found.params[0];
|
|
11791
|
-
if (!
|
|
13157
|
+
if (!t15.isObjectPattern(param)) {
|
|
11792
13158
|
return {
|
|
11793
13159
|
...base,
|
|
11794
13160
|
refusal: `${component} does not destructure its props, so there is no safe place to add one without knowing what the parameter is called downstream`
|
|
@@ -11804,7 +13170,7 @@ function planForward(file2, component, depth = 0) {
|
|
|
11804
13170
|
const imports = collectImports(ast);
|
|
11805
13171
|
const nonCarrier = roots.find((root) => !isCarrierRoot(root, imports));
|
|
11806
13172
|
if (nonCarrier) {
|
|
11807
|
-
const name =
|
|
13173
|
+
const name = t15.isJSXIdentifier(nonCarrier.name) ? nonCarrier.name.name : "its root";
|
|
11808
13174
|
return {
|
|
11809
13175
|
...base,
|
|
11810
13176
|
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`
|
|
@@ -11812,21 +13178,21 @@ function planForward(file2, component, depth = 0) {
|
|
|
11812
13178
|
}
|
|
11813
13179
|
const edits = [];
|
|
11814
13180
|
const alreadyDestructured = param.properties.some(
|
|
11815
|
-
(prop) =>
|
|
13181
|
+
(prop) => t15.isObjectProperty(prop) && t15.isIdentifier(prop.key) && prop.key.name === "testID"
|
|
11816
13182
|
);
|
|
11817
13183
|
if (!alreadyDestructured) {
|
|
11818
13184
|
const insert = insertAfterLastMember(source, param.properties, "testID", param.end ?? 0, ",");
|
|
11819
13185
|
edits.push({ ...insert, what: "parameter" });
|
|
11820
13186
|
}
|
|
11821
13187
|
const annotation = param.typeAnnotation;
|
|
11822
|
-
if (
|
|
13188
|
+
if (t15.isTSTypeAnnotation(annotation)) {
|
|
11823
13189
|
const declared = annotation.typeAnnotation;
|
|
11824
13190
|
let body;
|
|
11825
13191
|
let closing = 0;
|
|
11826
|
-
if (
|
|
13192
|
+
if (t15.isTSTypeLiteral(declared)) {
|
|
11827
13193
|
body = declared.members;
|
|
11828
13194
|
closing = declared.end ?? 0;
|
|
11829
|
-
} else if (
|
|
13195
|
+
} else if (t15.isTSTypeReference(declared) && t15.isIdentifier(declared.typeName)) {
|
|
11830
13196
|
const typeName = declared.typeName.name;
|
|
11831
13197
|
traverse13(ast, {
|
|
11832
13198
|
TSInterfaceDeclaration(nodePath) {
|
|
@@ -11835,7 +13201,7 @@ function planForward(file2, component, depth = 0) {
|
|
|
11835
13201
|
closing = nodePath.node.body.end ?? 0;
|
|
11836
13202
|
},
|
|
11837
13203
|
TSTypeAliasDeclaration(nodePath) {
|
|
11838
|
-
if (nodePath.node.id.name !== typeName || !
|
|
13204
|
+
if (nodePath.node.id.name !== typeName || !t15.isTSTypeLiteral(nodePath.node.typeAnnotation))
|
|
11839
13205
|
return;
|
|
11840
13206
|
body = nodePath.node.typeAnnotation.members;
|
|
11841
13207
|
closing = nodePath.node.typeAnnotation.end ?? 0;
|
|
@@ -11850,7 +13216,7 @@ function planForward(file2, component, depth = 0) {
|
|
|
11850
13216
|
}
|
|
11851
13217
|
if (body) {
|
|
11852
13218
|
const declaresTestId = body.some(
|
|
11853
|
-
(member) =>
|
|
13219
|
+
(member) => t15.isTSPropertySignature(member) && t15.isIdentifier(member.key) && member.key.name === "testID"
|
|
11854
13220
|
);
|
|
11855
13221
|
if (!declaresTestId) {
|
|
11856
13222
|
const insert = insertAfterLastMember(source, body, "testID?: string;", closing, ";");
|
|
@@ -11907,7 +13273,7 @@ function annotateCommand() {
|
|
|
11907
13273
|
const spinner = createSpinner("Scanning for unaddressable controls...");
|
|
11908
13274
|
spinner.start();
|
|
11909
13275
|
resetForwardingCache();
|
|
11910
|
-
const files = await (
|
|
13276
|
+
const files = await globSorted(include, { cwd: rootDir, ignore: exclude, absolute: true });
|
|
11911
13277
|
const scan = async () => {
|
|
11912
13278
|
const out = [];
|
|
11913
13279
|
for (const file2 of files) {
|
|
@@ -11956,9 +13322,9 @@ function annotateCommand() {
|
|
|
11956
13322
|
written: options.write === true,
|
|
11957
13323
|
annotated: written.length,
|
|
11958
13324
|
blocked: blocked.length,
|
|
11959
|
-
files: touched.map((edit) =>
|
|
13325
|
+
files: touched.map((edit) => import_node_path9.default.relative(rootDir, edit.file)),
|
|
11960
13326
|
sites: [...written, ...blocked].map((site) => ({
|
|
11961
|
-
file:
|
|
13327
|
+
file: import_node_path9.default.relative(rootDir, site.file),
|
|
11962
13328
|
line: site.line,
|
|
11963
13329
|
component: site.component,
|
|
11964
13330
|
role: site.role,
|
|
@@ -12001,9 +13367,12 @@ async function planForwards(edits, rootDir) {
|
|
|
12001
13367
|
void rootDir;
|
|
12002
13368
|
return plans;
|
|
12003
13369
|
}
|
|
13370
|
+
function escapeRegExp(value) {
|
|
13371
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13372
|
+
}
|
|
12004
13373
|
function findImportSource(source, name) {
|
|
12005
13374
|
const pattern = new RegExp(
|
|
12006
|
-
String.raw`import\s*\{[^}]*\b${name}\b[^}]*\}\s*from\s*['"]([^'"]+)['"]`
|
|
13375
|
+
String.raw`import\s*\{[^}]*\b${escapeRegExp(name)}\b[^}]*\}\s*from\s*['"]([^'"]+)['"]`
|
|
12007
13376
|
);
|
|
12008
13377
|
return pattern.exec(source)?.[1];
|
|
12009
13378
|
}
|
|
@@ -12012,7 +13381,7 @@ function reportSites(rootDir, touched, blocked, didWrite, forwards) {
|
|
|
12012
13381
|
if (written.length > 0) {
|
|
12013
13382
|
heading(didWrite ? "Added" : "Would add");
|
|
12014
13383
|
for (const edit of touched) {
|
|
12015
|
-
info(
|
|
13384
|
+
info(import_node_path9.default.relative(rootDir, edit.file));
|
|
12016
13385
|
for (const site of edit.written) {
|
|
12017
13386
|
dim(` ${String(site.line).padStart(4)} <${site.component}> testID="${site.testId}"`);
|
|
12018
13387
|
}
|
|
@@ -12024,7 +13393,7 @@ function reportSites(rootDir, touched, blocked, didWrite, forwards) {
|
|
|
12024
13393
|
if (teachable.size > 0) {
|
|
12025
13394
|
heading(didWrite ? "Taught to carry testID" : "Would teach to carry testID");
|
|
12026
13395
|
for (const plan of teachable.values()) {
|
|
12027
|
-
info(`<${plan.component}> ${
|
|
13396
|
+
info(`<${plan.component}> ${import_node_path9.default.relative(rootDir, plan.file)}`);
|
|
12028
13397
|
for (const edit of plan.edits) {
|
|
12029
13398
|
dim(` ${edit.what}: ${edit.text.trim()}`);
|
|
12030
13399
|
}
|
|
@@ -12332,10 +13701,10 @@ function matchActions(expected, actual, reply = "") {
|
|
|
12332
13701
|
var import_fs9 = require("fs");
|
|
12333
13702
|
var import_path8 = require("path");
|
|
12334
13703
|
var EMPTY_BASELINE = { generatedAt: null, gitSha: null, perScenario: {} };
|
|
12335
|
-
function loadBaseline(
|
|
12336
|
-
if (!(0, import_fs9.existsSync)(
|
|
13704
|
+
function loadBaseline(path13) {
|
|
13705
|
+
if (!(0, import_fs9.existsSync)(path13)) return { ...EMPTY_BASELINE, perScenario: {} };
|
|
12337
13706
|
try {
|
|
12338
|
-
const raw = JSON.parse((0, import_fs9.readFileSync)(
|
|
13707
|
+
const raw = JSON.parse((0, import_fs9.readFileSync)(path13, "utf-8"));
|
|
12339
13708
|
return {
|
|
12340
13709
|
generatedAt: raw.generatedAt ?? null,
|
|
12341
13710
|
gitSha: raw.gitSha ?? null,
|
|
@@ -12345,9 +13714,9 @@ function loadBaseline(path11) {
|
|
|
12345
13714
|
return { ...EMPTY_BASELINE, perScenario: {} };
|
|
12346
13715
|
}
|
|
12347
13716
|
}
|
|
12348
|
-
function saveBaseline(
|
|
12349
|
-
(0, import_fs9.mkdirSync)((0, import_path8.dirname)(
|
|
12350
|
-
(0, import_fs9.writeFileSync)(
|
|
13717
|
+
function saveBaseline(path13, baseline) {
|
|
13718
|
+
(0, import_fs9.mkdirSync)((0, import_path8.dirname)(path13), { recursive: true });
|
|
13719
|
+
(0, import_fs9.writeFileSync)(path13, `${JSON.stringify(baseline, null, 2)}
|
|
12351
13720
|
`, "utf-8");
|
|
12352
13721
|
}
|
|
12353
13722
|
function buildBaseline(outcomes, meta) {
|
|
@@ -12375,11 +13744,11 @@ function diffAgainstBaseline(outcomes, baseline, maxTokenRegression) {
|
|
|
12375
13744
|
continue;
|
|
12376
13745
|
}
|
|
12377
13746
|
if (outcome.status === "passed" && prior.status === "passed" && prior.totalTokens > 0 && outcome.totalTokens > prior.totalTokens * (1 + maxTokenRegression)) {
|
|
12378
|
-
const
|
|
13747
|
+
const pct2 = Math.round((outcome.totalTokens - prior.totalTokens) / prior.totalTokens * 100);
|
|
12379
13748
|
regressions.push({
|
|
12380
13749
|
name: outcome.name,
|
|
12381
13750
|
kind: "token-regression",
|
|
12382
|
-
detail: `tokens ${prior.totalTokens} \u2192 ${outcome.totalTokens} (+${
|
|
13751
|
+
detail: `tokens ${prior.totalTokens} \u2192 ${outcome.totalTokens} (+${pct2}%, threshold +${Math.round(maxTokenRegression * 100)}%)`
|
|
12383
13752
|
});
|
|
12384
13753
|
}
|
|
12385
13754
|
}
|