@appilots/cli 0.11.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 +1756 -420
- 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 +1420 -195
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1414 -189
- 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.11.
|
|
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 = {};
|
|
@@ -151,6 +151,10 @@ function loadConfig(onWarn) {
|
|
|
151
151
|
};
|
|
152
152
|
const validation = validateConfig(merged);
|
|
153
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
|
+
}
|
|
154
158
|
if (fileConfig.serverUrl === void 0 && env.serverUrl === void 0) {
|
|
155
159
|
for (const key of Object.keys(fileConfig)) {
|
|
156
160
|
if (KNOWN_CONFIG_KEYS.includes(key)) continue;
|
|
@@ -763,12 +767,18 @@ function initCommand() {
|
|
|
763
767
|
// src/cli/commands/sync.ts
|
|
764
768
|
var import_commander2 = require("commander");
|
|
765
769
|
var import_promises5 = require("fs/promises");
|
|
766
|
-
var
|
|
770
|
+
var import_node_path7 = require("path");
|
|
767
771
|
|
|
768
772
|
// src/generators/MCPGenerator.ts
|
|
769
773
|
var import_promises4 = require("fs/promises");
|
|
770
774
|
var import_node_crypto = require("crypto");
|
|
771
|
-
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
|
+
}
|
|
772
782
|
|
|
773
783
|
// src/pipeline/enrichment.ts
|
|
774
784
|
function enrichScreenForAgent(screen) {
|
|
@@ -846,7 +856,10 @@ function mergeTargets(targets) {
|
|
|
846
856
|
for (const target of targets) {
|
|
847
857
|
if (!target.id) continue;
|
|
848
858
|
const existing = byId.get(target.id);
|
|
849
|
-
byId.set(
|
|
859
|
+
byId.set(
|
|
860
|
+
target.id,
|
|
861
|
+
existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target
|
|
862
|
+
);
|
|
850
863
|
}
|
|
851
864
|
return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
852
865
|
}
|
|
@@ -891,7 +904,10 @@ function synthesizeFlows(screen, targets) {
|
|
|
891
904
|
intent: "destructive_action",
|
|
892
905
|
steps: [
|
|
893
906
|
{ type: "press", target: action.id, label: action.label },
|
|
894
|
-
{
|
|
907
|
+
{
|
|
908
|
+
type: "confirm",
|
|
909
|
+
description: "Wait for native or custom confirmation before continuing"
|
|
910
|
+
},
|
|
895
911
|
{ type: "wait", description: describeWait(action) }
|
|
896
912
|
],
|
|
897
913
|
waitPolicy: waitPolicyForAction(action),
|
|
@@ -906,10 +922,19 @@ function synthesizeFlows(screen, targets) {
|
|
|
906
922
|
title: `Act on an item in ${collection.id}`,
|
|
907
923
|
intent: "list_action",
|
|
908
924
|
steps: [
|
|
909
|
-
{
|
|
910
|
-
|
|
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
|
+
}
|
|
911
934
|
],
|
|
912
|
-
waitPolicy: {
|
|
935
|
+
waitPolicy: {
|
|
936
|
+
expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback"
|
|
937
|
+
}
|
|
913
938
|
});
|
|
914
939
|
}
|
|
915
940
|
}
|
|
@@ -917,25 +942,32 @@ function synthesizeFlows(screen, targets) {
|
|
|
917
942
|
}
|
|
918
943
|
function waitPolicyForAction(action) {
|
|
919
944
|
const expectedOutcome = action.successSignal?.type === "goBack" ? "goBack" : action.appilotsInferred?.expectedOutcome ?? (action.targetScreen ? "navigation" : void 0);
|
|
920
|
-
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);
|
|
921
949
|
return {
|
|
922
950
|
...expectedOutcome ? { expectedOutcome } : {},
|
|
923
951
|
...signals && signals.length > 0 ? { signals } : {},
|
|
924
|
-
...
|
|
952
|
+
...maxMs !== void 0 ? { maxMs } : {}
|
|
925
953
|
};
|
|
926
954
|
}
|
|
927
955
|
function describeWait(action) {
|
|
928
956
|
if (action.successSignal?.description) return action.successSignal.description;
|
|
929
|
-
if (action.successSignal?.type === "goBack")
|
|
957
|
+
if (action.successSignal?.type === "goBack")
|
|
958
|
+
return "Wait for the app to return to the previous screen";
|
|
930
959
|
if (action.targetScreen) return `Wait for navigation to ${action.targetScreen}`;
|
|
931
|
-
if (action.appilotsInferred?.expectedOutcome)
|
|
960
|
+
if (action.appilotsInferred?.expectedOutcome)
|
|
961
|
+
return `Wait for ${action.appilotsInferred.expectedOutcome}`;
|
|
932
962
|
return "Wait for the UI to settle";
|
|
933
963
|
}
|
|
934
964
|
function synthesizeAgentHints(screen, targets, flows) {
|
|
935
965
|
const preferredTargets = targets.filter((target) => ["submit", "button", "list"].includes(target.role)).slice(0, 8).map((target) => target.id);
|
|
936
966
|
const commonTasks = flows.slice(0, 6).map((flow) => flow.title);
|
|
937
967
|
const safetyNotes = screen.actions.filter((action) => action.destructive || action.requiresConfirmation).map((action) => `${action.id} requires confirmation`);
|
|
938
|
-
const firstAsyncAction = screen.actions.find(
|
|
968
|
+
const firstAsyncAction = screen.actions.find(
|
|
969
|
+
(action) => action.appilotsInferred?.isAsyncTrigger || action.asyncBudgetMs !== void 0
|
|
970
|
+
);
|
|
939
971
|
const hints = {
|
|
940
972
|
primaryGoal: synthesizePrimaryGoal(screen),
|
|
941
973
|
commonTasks,
|
|
@@ -962,7 +994,9 @@ function synthesizePrimaryGoal(screen) {
|
|
|
962
994
|
}
|
|
963
995
|
const fieldCount = uniqueFields.size;
|
|
964
996
|
if (fieldCount > 0) {
|
|
965
|
-
const requiredCount = Array.from(uniqueFields.values()).filter(
|
|
997
|
+
const requiredCount = Array.from(uniqueFields.values()).filter(
|
|
998
|
+
(field) => field.required
|
|
999
|
+
).length;
|
|
966
1000
|
const fieldLabel = fieldCount === 1 ? "field" : "fields";
|
|
967
1001
|
goals.push(
|
|
968
1002
|
requiredCount > 0 ? `Complete a form with ${fieldCount} ${fieldLabel} (${requiredCount} required)` : `Complete a form with ${fieldCount} ${fieldLabel}`
|
|
@@ -976,7 +1010,9 @@ function synthesizePrimaryGoal(screen) {
|
|
|
976
1010
|
if (submitCount > 0) {
|
|
977
1011
|
goals.push(`Submit ${submitCount === 1 ? "the primary form" : `${submitCount} forms/actions`}`);
|
|
978
1012
|
}
|
|
979
|
-
const asyncCount = screen.actions.filter(
|
|
1013
|
+
const asyncCount = screen.actions.filter(
|
|
1014
|
+
(action) => action.appilotsInferred?.isAsyncTrigger
|
|
1015
|
+
).length;
|
|
980
1016
|
if (asyncCount > 0) {
|
|
981
1017
|
goals.push(`Wait for ${asyncCount === 1 ? "async feedback" : "async action feedback"}`);
|
|
982
1018
|
}
|
|
@@ -986,15 +1022,23 @@ function synthesizePrimaryGoal(screen) {
|
|
|
986
1022
|
}
|
|
987
1023
|
|
|
988
1024
|
// src/analyzers/ReactNativePlatformAnalyzer.ts
|
|
989
|
-
var
|
|
990
|
-
|
|
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
|
+
}
|
|
991
1036
|
|
|
992
1037
|
// src/analyzers/ScreenAnalyzer.ts
|
|
993
1038
|
var import_promises = __toESM(require("fs/promises"));
|
|
994
1039
|
var import_path3 = __toESM(require("path"));
|
|
995
1040
|
var import_traverse4 = __toESM(require("@babel/traverse"));
|
|
996
1041
|
var BabelTypes = __toESM(require("@babel/types"));
|
|
997
|
-
var import_fast_glob = __toESM(require("fast-glob"));
|
|
998
1042
|
|
|
999
1043
|
// src/ast/parse.ts
|
|
1000
1044
|
var import_parser = require("@babel/parser");
|
|
@@ -1397,14 +1441,25 @@ var ScreenAnalyzer = class {
|
|
|
1397
1441
|
strictScreens;
|
|
1398
1442
|
/** Glob patterns that identify screen files in strict mode */
|
|
1399
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;
|
|
1400
1454
|
/** §D: Count of screens filtered out in strict mode (available after analyze()) */
|
|
1401
1455
|
screensFilteredOut = 0;
|
|
1402
1456
|
constructor(config, options) {
|
|
1403
1457
|
this.config = config;
|
|
1458
|
+
this.routeTargetFiles = options?.routeTargetFiles ?? /* @__PURE__ */ new Set();
|
|
1404
1459
|
this.strictScreens = options?.strictScreens ?? false;
|
|
1405
1460
|
this.screenPatterns = options?.screenPatterns ?? [
|
|
1406
|
-
"**/*Screen.{ts,tsx}",
|
|
1407
|
-
"**/screens/**/*.{ts,tsx}"
|
|
1461
|
+
"**/*Screen.{ts,tsx,js,jsx}",
|
|
1462
|
+
"**/screens/**/*.{ts,tsx,js,jsx}"
|
|
1408
1463
|
];
|
|
1409
1464
|
}
|
|
1410
1465
|
/** Analyze all screens in the project */
|
|
@@ -1416,7 +1471,7 @@ var ScreenAnalyzer = class {
|
|
|
1416
1471
|
console.log(`[ScreenAnalyzer] Strict mode ON \u2014 screen patterns:`, this.screenPatterns);
|
|
1417
1472
|
}
|
|
1418
1473
|
}
|
|
1419
|
-
const files = await (
|
|
1474
|
+
const files = await globSorted(include, {
|
|
1420
1475
|
cwd: this.config.rootDir,
|
|
1421
1476
|
ignore: exclude
|
|
1422
1477
|
});
|
|
@@ -1425,7 +1480,7 @@ var ScreenAnalyzer = class {
|
|
|
1425
1480
|
}
|
|
1426
1481
|
let screenPatternFiles = null;
|
|
1427
1482
|
if (this.strictScreens) {
|
|
1428
|
-
const matched = await (
|
|
1483
|
+
const matched = await globSorted(this.screenPatterns, {
|
|
1429
1484
|
cwd: this.config.rootDir,
|
|
1430
1485
|
ignore: exclude
|
|
1431
1486
|
});
|
|
@@ -1441,7 +1496,8 @@ var ScreenAnalyzer = class {
|
|
|
1441
1496
|
if (this.strictScreens) {
|
|
1442
1497
|
const hasRegisterScreen = descriptor.__hasRegisterScreen === true;
|
|
1443
1498
|
const matchesPattern = screenPatternFiles?.has(filePath) ?? false;
|
|
1444
|
-
|
|
1499
|
+
const isRouteTarget = this.routeTargetFiles.has(filePath);
|
|
1500
|
+
if (!hasRegisterScreen && !matchesPattern && !isRouteTarget) {
|
|
1445
1501
|
this.screensFilteredOut++;
|
|
1446
1502
|
if (this.verbose) {
|
|
1447
1503
|
console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file2}`);
|
|
@@ -1649,6 +1705,8 @@ var ScreenAnalyzer = class {
|
|
|
1649
1705
|
action.riskLevel = value.value;
|
|
1650
1706
|
} else if (key === "nativeConfirmationExpected" && BabelTypes.isBooleanLiteral(value)) {
|
|
1651
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;
|
|
1652
1710
|
} else if (key === "appilotsInferred" && BabelTypes.isObjectExpression(value)) {
|
|
1653
1711
|
action.appilotsInferred = this.parseAppilotsInferredObject(value);
|
|
1654
1712
|
}
|
|
@@ -2636,43 +2694,880 @@ var ScreenAnalyzer = class {
|
|
|
2636
2694
|
// src/analyzers/NavigationAnalyzer.ts
|
|
2637
2695
|
var import_fs4 = require("fs");
|
|
2638
2696
|
var import_path4 = __toESM(require("path"));
|
|
2639
|
-
var
|
|
2640
|
-
var parser = __toESM(require("@babel/parser"));
|
|
2697
|
+
var parser2 = __toESM(require("@babel/parser"));
|
|
2641
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"));
|
|
2642
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
|
+
);
|
|
2643
3282
|
var NavigationAnalyzer = class {
|
|
2644
3283
|
config;
|
|
2645
3284
|
/** Extra glob patterns for navigation file discovery (added to defaults) */
|
|
2646
3285
|
navigationInclude;
|
|
2647
3286
|
/** Extra glob patterns to exclude from navigation analysis */
|
|
2648
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
|
+
};
|
|
2649
3321
|
constructor(config, options) {
|
|
2650
3322
|
this.config = config;
|
|
3323
|
+
this.graph = new ModuleGraph(config.rootDir);
|
|
2651
3324
|
this.navigationInclude = options?.navigationInclude ?? [];
|
|
2652
3325
|
this.navigationExclude = options?.navigationExclude ?? [];
|
|
2653
3326
|
}
|
|
2654
3327
|
/** Build the full navigation graph */
|
|
2655
3328
|
async analyze() {
|
|
2656
3329
|
const navigationFiles = await this.findNavigationFiles();
|
|
2657
|
-
const parsedNavigators = [];
|
|
2658
3330
|
const typeExports = [];
|
|
3331
|
+
this.routeTargets.clear();
|
|
3332
|
+
const declarations = /* @__PURE__ */ new Map();
|
|
3333
|
+
const evidenceFiles = [];
|
|
3334
|
+
this.navigatorFiles.clear();
|
|
2659
3335
|
for (const filePath of navigationFiles) {
|
|
2660
3336
|
try {
|
|
2661
3337
|
const content = await import_fs4.promises.readFile(filePath, "utf-8");
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
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));
|
|
2666
3345
|
} catch (error2) {
|
|
2667
3346
|
console.warn(`Failed to parse ${filePath}:`, error2);
|
|
2668
3347
|
}
|
|
2669
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
|
+
);
|
|
2670
3447
|
this.attachParamsToNavigators(parsedNavigators, typeExports);
|
|
2671
3448
|
return this.buildNavigationGraph(parsedNavigators);
|
|
2672
3449
|
}
|
|
2673
|
-
/**
|
|
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
|
+
*/
|
|
2674
3567
|
async findNavigationFiles() {
|
|
2675
3568
|
const patterns = [
|
|
3569
|
+
...this.config.include ?? ["**/*.{ts,tsx,js,jsx}"],
|
|
3570
|
+
// Piso de compatibilidade — nunca encontrar MENOS que a versão anterior.
|
|
2676
3571
|
"**/navigation/**/*.{ts,tsx}",
|
|
2677
3572
|
"**/navigator*.{ts,tsx}",
|
|
2678
3573
|
"**/routes*.{ts,tsx}",
|
|
@@ -2683,91 +3578,188 @@ var NavigationAnalyzer = class {
|
|
|
2683
3578
|
"**/node_modules/**",
|
|
2684
3579
|
"**/dist/**",
|
|
2685
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}",
|
|
2686
3598
|
...this.config.exclude || [],
|
|
2687
3599
|
// §5: Add user-configured navigation excludes
|
|
2688
3600
|
...this.navigationExclude
|
|
2689
3601
|
];
|
|
2690
|
-
const files = await (
|
|
3602
|
+
const files = await globSorted(patterns, {
|
|
2691
3603
|
cwd: this.config.rootDir,
|
|
2692
3604
|
ignore: excludePatterns
|
|
2693
3605
|
});
|
|
2694
3606
|
return files.map((file2) => import_path4.default.join(this.config.rootDir, file2));
|
|
2695
3607
|
}
|
|
2696
|
-
/**
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
(0, import_traverse5.default)(ast, {
|
|
2711
|
-
// Detect createStackNavigator / createTabNavigator / createDrawerNavigator calls
|
|
2712
|
-
CallExpression: (nodePath) => {
|
|
2713
|
-
const { node } = nodePath;
|
|
2714
|
-
const callee = node.callee;
|
|
2715
|
-
let navigatorType = null;
|
|
2716
|
-
if (t5.isIdentifier(callee) && callee.name === "createNativeStackNavigator") {
|
|
2717
|
-
navigatorType = "stack";
|
|
2718
|
-
} else if (t5.isIdentifier(callee) && callee.name === "createStackNavigator") {
|
|
2719
|
-
navigatorType = "stack";
|
|
2720
|
-
} else if (t5.isIdentifier(callee) && callee.name === "createBottomTabNavigator") {
|
|
2721
|
-
navigatorType = "tab";
|
|
2722
|
-
} else if (t5.isIdentifier(callee) && callee.name === "createTabNavigator") {
|
|
2723
|
-
navigatorType = "tab";
|
|
2724
|
-
} else if (t5.isIdentifier(callee) && callee.name === "createDrawerNavigator") {
|
|
2725
|
-
navigatorType = "drawer";
|
|
2726
|
-
}
|
|
2727
|
-
if (navigatorType) {
|
|
2728
|
-
const parent = nodePath.parent;
|
|
2729
|
-
if (t5.isVariableDeclarator(parent) && t5.isIdentifier(parent.id)) {
|
|
2730
|
-
const varName = parent.id.name;
|
|
2731
|
-
navigatorCalls.set(varName, {
|
|
2732
|
-
name: varName,
|
|
2733
|
-
type: navigatorType,
|
|
2734
|
-
screens: []
|
|
2735
|
-
});
|
|
2736
|
-
}
|
|
2737
|
-
}
|
|
2738
|
-
},
|
|
2739
|
-
// Detect Stack.Navigator / Tab.Navigator JSX elements
|
|
2740
|
-
JSXElement: (nodePath) => {
|
|
2741
|
-
const { node } = nodePath;
|
|
2742
|
-
const openingElement = node.openingElement;
|
|
2743
|
-
if (t5.isJSXMemberExpression(openingElement.name) && t5.isJSXIdentifier(openingElement.name.object)) {
|
|
2744
|
-
const objectName = openingElement.name.object.name;
|
|
2745
|
-
const propertyName = t5.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
|
|
2746
|
-
if (propertyName === "Navigator") {
|
|
2747
|
-
const navigator = navigatorCalls.get(objectName);
|
|
2748
|
-
if (navigator) {
|
|
2749
|
-
const initialRouteAttr = openingElement.attributes.find(
|
|
2750
|
-
(attr) => t5.isJSXAttribute(attr) && t5.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
|
|
2751
|
-
);
|
|
2752
|
-
if (t5.isJSXAttribute(initialRouteAttr) && t5.isStringLiteral(initialRouteAttr.value)) {
|
|
2753
|
-
navigator.initialRouteName = initialRouteAttr.value.value;
|
|
2754
|
-
}
|
|
2755
|
-
const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
|
|
2756
|
-
screensByNavigator.set(objectName, screens);
|
|
2757
|
-
}
|
|
2758
|
-
}
|
|
2759
|
-
}
|
|
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;
|
|
2760
3622
|
}
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
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);
|
|
2769
3761
|
}
|
|
2770
|
-
return
|
|
3762
|
+
return null;
|
|
2771
3763
|
}
|
|
2772
3764
|
/**
|
|
2773
3765
|
* Is this JSX element `<navigatorVarName.MEMBER …>`?
|
|
@@ -2778,9 +3770,9 @@ var NavigationAnalyzer = class {
|
|
|
2778
3770
|
// `never`. It is a question about the member name, not about the node
|
|
2779
3771
|
// kind.
|
|
2780
3772
|
isNavigatorMember(node, navigatorVarName, member) {
|
|
2781
|
-
if (!
|
|
3773
|
+
if (!t6.isJSXElement(node)) return false;
|
|
2782
3774
|
const name = node.openingElement.name;
|
|
2783
|
-
return
|
|
3775
|
+
return t6.isJSXMemberExpression(name) && t6.isJSXIdentifier(name.object) && name.object.name === navigatorVarName && t6.isJSXIdentifier(name.property) && name.property.name === member;
|
|
2784
3776
|
}
|
|
2785
3777
|
/**
|
|
2786
3778
|
* Flatten a navigator's JSX children into the `<X.Screen>` elements they
|
|
@@ -2811,37 +3803,37 @@ var NavigationAnalyzer = class {
|
|
|
2811
3803
|
found.push(...this.collectScreenElements(group.children, navigatorVarName, depth + 1));
|
|
2812
3804
|
return;
|
|
2813
3805
|
}
|
|
2814
|
-
if (
|
|
3806
|
+
if (t6.isJSXElement(node)) {
|
|
2815
3807
|
const name = node.openingElement.name;
|
|
2816
|
-
const isForeignNavigator =
|
|
3808
|
+
const isForeignNavigator = t6.isJSXMemberExpression(name) && t6.isJSXIdentifier(name.property) && name.property.name === "Navigator";
|
|
2817
3809
|
if (isForeignNavigator) return;
|
|
2818
3810
|
found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
|
|
2819
3811
|
return;
|
|
2820
3812
|
}
|
|
2821
|
-
if (
|
|
3813
|
+
if (t6.isJSXFragment(node)) {
|
|
2822
3814
|
found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
|
|
2823
3815
|
return;
|
|
2824
3816
|
}
|
|
2825
|
-
if (
|
|
3817
|
+
if (t6.isJSXExpressionContainer(node)) {
|
|
2826
3818
|
visit(node.expression);
|
|
2827
3819
|
return;
|
|
2828
3820
|
}
|
|
2829
|
-
if (
|
|
3821
|
+
if (t6.isConditionalExpression(node)) {
|
|
2830
3822
|
visit(node.consequent);
|
|
2831
3823
|
visit(node.alternate);
|
|
2832
3824
|
return;
|
|
2833
3825
|
}
|
|
2834
|
-
if (
|
|
3826
|
+
if (t6.isLogicalExpression(node)) {
|
|
2835
3827
|
visit(node.left);
|
|
2836
3828
|
visit(node.right);
|
|
2837
3829
|
return;
|
|
2838
3830
|
}
|
|
2839
|
-
if (
|
|
3831
|
+
if (t6.isCallExpression(node)) {
|
|
2840
3832
|
for (const arg of node.arguments) {
|
|
2841
|
-
if (
|
|
2842
|
-
if (
|
|
3833
|
+
if (t6.isArrowFunctionExpression(arg) || t6.isFunctionExpression(arg)) {
|
|
3834
|
+
if (t6.isBlockStatement(arg.body)) {
|
|
2843
3835
|
for (const stmt of arg.body.body) {
|
|
2844
|
-
if (
|
|
3836
|
+
if (t6.isReturnStatement(stmt)) visit(stmt.argument);
|
|
2845
3837
|
}
|
|
2846
3838
|
} else {
|
|
2847
3839
|
visit(arg.body);
|
|
@@ -2850,11 +3842,11 @@ var NavigationAnalyzer = class {
|
|
|
2850
3842
|
}
|
|
2851
3843
|
return;
|
|
2852
3844
|
}
|
|
2853
|
-
if (
|
|
3845
|
+
if (t6.isArrayExpression(node)) {
|
|
2854
3846
|
for (const el of node.elements) visit(el);
|
|
2855
3847
|
return;
|
|
2856
3848
|
}
|
|
2857
|
-
if (
|
|
3849
|
+
if (t6.isTSAsExpression(node) || t6.isTSNonNullExpression(node)) {
|
|
2858
3850
|
visit(node.expression);
|
|
2859
3851
|
}
|
|
2860
3852
|
};
|
|
@@ -2862,28 +3854,54 @@ var NavigationAnalyzer = class {
|
|
|
2862
3854
|
return found;
|
|
2863
3855
|
}
|
|
2864
3856
|
/** Extract screens from a navigator JSX element */
|
|
2865
|
-
extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
|
|
3857
|
+
extractScreensFromNavigator(filePath, navigatorElement, navigatorVarName, navigatorType) {
|
|
2866
3858
|
const screens = [];
|
|
2867
3859
|
if (!navigatorElement.children) return screens;
|
|
2868
3860
|
const seen = /* @__PURE__ */ new Set();
|
|
2869
3861
|
for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
|
|
2870
|
-
const
|
|
2871
|
-
if (!
|
|
2872
|
-
seen.add(
|
|
2873
|
-
screens.push(
|
|
2874
|
-
name: screenName,
|
|
2875
|
-
navigatorName: navigatorVarName,
|
|
2876
|
-
navigatorType
|
|
2877
|
-
});
|
|
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);
|
|
2878
3866
|
}
|
|
2879
3867
|
return screens;
|
|
2880
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
|
+
}
|
|
2881
3899
|
/** Extract string attribute value from JSX attributes */
|
|
2882
3900
|
extractAttributeValue(attributes, attrName) {
|
|
2883
3901
|
const attr = attributes.find(
|
|
2884
|
-
(a) =>
|
|
3902
|
+
(a) => t6.isJSXAttribute(a) && t6.isJSXIdentifier(a.name) && a.name.name === attrName
|
|
2885
3903
|
);
|
|
2886
|
-
if (
|
|
3904
|
+
if (t6.isJSXAttribute(attr) && t6.isStringLiteral(attr.value)) {
|
|
2887
3905
|
return attr.value.value;
|
|
2888
3906
|
}
|
|
2889
3907
|
return null;
|
|
@@ -2892,7 +3910,7 @@ var NavigationAnalyzer = class {
|
|
|
2892
3910
|
parseParamTypes(content, filePath) {
|
|
2893
3911
|
const types = [];
|
|
2894
3912
|
try {
|
|
2895
|
-
const ast =
|
|
3913
|
+
const ast = parser2.parse(content, {
|
|
2896
3914
|
sourceType: "module",
|
|
2897
3915
|
plugins: [
|
|
2898
3916
|
"jsx",
|
|
@@ -2973,7 +3991,7 @@ var NavigationAnalyzer = class {
|
|
|
2973
3991
|
if (type.type === "TSUndefinedKeyword") return "undefined";
|
|
2974
3992
|
if (type.type === "TSNullKeyword") return "null";
|
|
2975
3993
|
if (type.type === "TSUnionType") {
|
|
2976
|
-
return type.types.map((
|
|
3994
|
+
return type.types.map((t16) => this.typeToString(t16)).join(" | ");
|
|
2977
3995
|
}
|
|
2978
3996
|
if (type.type === "TSTypeLiteral") {
|
|
2979
3997
|
return "object";
|
|
@@ -2994,7 +4012,7 @@ var NavigationAnalyzer = class {
|
|
|
2994
4012
|
/** Attach parsed type params to navigator screens */
|
|
2995
4013
|
attachParamsToNavigators(navigators, types) {
|
|
2996
4014
|
for (const navigator of navigators) {
|
|
2997
|
-
const matchingType = types.find((
|
|
4015
|
+
const matchingType = types.find((t16) => t16.type === navigator.type);
|
|
2998
4016
|
if (matchingType) {
|
|
2999
4017
|
for (const screen of navigator.screens) {
|
|
3000
4018
|
const screenParams = matchingType.paramEntries.get(screen.name);
|
|
@@ -3072,9 +4090,9 @@ var NavigationAnalyzer = class {
|
|
|
3072
4090
|
|
|
3073
4091
|
// src/analyzers/ComponentAnalyzer.ts
|
|
3074
4092
|
var import_fs5 = require("fs");
|
|
3075
|
-
var
|
|
4093
|
+
var parser3 = __toESM(require("@babel/parser"));
|
|
3076
4094
|
var import_traverse6 = __toESM(require("@babel/traverse"));
|
|
3077
|
-
var
|
|
4095
|
+
var t7 = __toESM(require("@babel/types"));
|
|
3078
4096
|
var ComponentAnalyzer = class {
|
|
3079
4097
|
config;
|
|
3080
4098
|
constructor(config) {
|
|
@@ -3084,14 +4102,14 @@ var ComponentAnalyzer = class {
|
|
|
3084
4102
|
async analyzeFile(filePath) {
|
|
3085
4103
|
try {
|
|
3086
4104
|
const code = (0, import_fs5.readFileSync)(filePath, "utf-8");
|
|
3087
|
-
const ast =
|
|
4105
|
+
const ast = parser3.parse(code, {
|
|
3088
4106
|
sourceType: "module",
|
|
3089
4107
|
plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
|
|
3090
4108
|
});
|
|
3091
4109
|
const components = [];
|
|
3092
4110
|
(0, import_traverse6.default)(ast, {
|
|
3093
|
-
JSXElement: (
|
|
3094
|
-
const component = this.extractComponentFromJSXElement(
|
|
4111
|
+
JSXElement: (path13) => {
|
|
4112
|
+
const component = this.extractComponentFromJSXElement(path13.node);
|
|
3095
4113
|
if (component) {
|
|
3096
4114
|
components.push(component);
|
|
3097
4115
|
}
|
|
@@ -3120,19 +4138,19 @@ var ComponentAnalyzer = class {
|
|
|
3120
4138
|
};
|
|
3121
4139
|
}
|
|
3122
4140
|
getElementName(openingElement) {
|
|
3123
|
-
if (
|
|
4141
|
+
if (t7.isJSXIdentifier(openingElement.name)) {
|
|
3124
4142
|
return openingElement.name.name;
|
|
3125
4143
|
}
|
|
3126
|
-
if (
|
|
4144
|
+
if (t7.isJSXMemberExpression(openingElement.name)) {
|
|
3127
4145
|
const parts = [];
|
|
3128
4146
|
let current = openingElement.name;
|
|
3129
|
-
while (
|
|
3130
|
-
if (
|
|
4147
|
+
while (t7.isJSXMemberExpression(current)) {
|
|
4148
|
+
if (t7.isJSXIdentifier(current.property)) {
|
|
3131
4149
|
parts.unshift(current.property.name);
|
|
3132
4150
|
}
|
|
3133
4151
|
current = current.object;
|
|
3134
4152
|
}
|
|
3135
|
-
if (
|
|
4153
|
+
if (t7.isJSXIdentifier(current)) {
|
|
3136
4154
|
parts.unshift(current.name);
|
|
3137
4155
|
}
|
|
3138
4156
|
return parts.join(".");
|
|
@@ -3155,13 +4173,13 @@ var ComponentAnalyzer = class {
|
|
|
3155
4173
|
extractProps(openingElement) {
|
|
3156
4174
|
const props = {};
|
|
3157
4175
|
for (const attr of openingElement.attributes) {
|
|
3158
|
-
if (
|
|
4176
|
+
if (t7.isJSXAttribute(attr) && t7.isJSXIdentifier(attr.name)) {
|
|
3159
4177
|
const propName = attr.name.name;
|
|
3160
4178
|
if (attr.value === null) {
|
|
3161
4179
|
props[propName] = "true";
|
|
3162
|
-
} else if (
|
|
4180
|
+
} else if (t7.isStringLiteral(attr.value)) {
|
|
3163
4181
|
props[propName] = attr.value.value;
|
|
3164
|
-
} else if (
|
|
4182
|
+
} else if (t7.isJSXExpressionContainer(attr.value) && t7.isStringLiteral(attr.value.expression)) {
|
|
3165
4183
|
props[propName] = attr.value.expression.value;
|
|
3166
4184
|
}
|
|
3167
4185
|
}
|
|
@@ -3171,12 +4189,12 @@ var ComponentAnalyzer = class {
|
|
|
3171
4189
|
extractAccessibilityProps(openingElement) {
|
|
3172
4190
|
const result = {};
|
|
3173
4191
|
for (const attr of openingElement.attributes) {
|
|
3174
|
-
if (
|
|
4192
|
+
if (t7.isJSXAttribute(attr) && t7.isJSXIdentifier(attr.name)) {
|
|
3175
4193
|
const propName = attr.name.name;
|
|
3176
4194
|
if ((propName === "testID" || propName === "accessibilityLabel") && attr.value) {
|
|
3177
|
-
if (
|
|
4195
|
+
if (t7.isStringLiteral(attr.value)) {
|
|
3178
4196
|
result[propName] = attr.value.value;
|
|
3179
|
-
} else if (
|
|
4197
|
+
} else if (t7.isJSXExpressionContainer(attr.value) && t7.isStringLiteral(attr.value.expression)) {
|
|
3180
4198
|
result[propName] = attr.value.expression.value;
|
|
3181
4199
|
}
|
|
3182
4200
|
}
|
|
@@ -3188,7 +4206,7 @@ var ComponentAnalyzer = class {
|
|
|
3188
4206
|
if (depth >= maxDepth) return [];
|
|
3189
4207
|
const extracted = [];
|
|
3190
4208
|
for (const child of children) {
|
|
3191
|
-
if (
|
|
4209
|
+
if (t7.isJSXElement(child)) {
|
|
3192
4210
|
const component = this.extractComponentFromJSXElement(child);
|
|
3193
4211
|
if (component) {
|
|
3194
4212
|
extracted.push(component);
|
|
@@ -3201,10 +4219,10 @@ var ComponentAnalyzer = class {
|
|
|
3201
4219
|
|
|
3202
4220
|
// src/analyzers/FormAnalyzer.ts
|
|
3203
4221
|
var import_fs6 = require("fs");
|
|
3204
|
-
var
|
|
4222
|
+
var parser4 = __toESM(require("@babel/parser"));
|
|
3205
4223
|
var import_traverse7 = __toESM(require("@babel/traverse"));
|
|
3206
|
-
var
|
|
3207
|
-
var
|
|
4224
|
+
var t8 = __toESM(require("@babel/types"));
|
|
4225
|
+
var path5 = __toESM(require("path"));
|
|
3208
4226
|
var FormAnalyzer = class {
|
|
3209
4227
|
config;
|
|
3210
4228
|
stateVariables = /* @__PURE__ */ new Map();
|
|
@@ -3217,7 +4235,7 @@ var FormAnalyzer = class {
|
|
|
3217
4235
|
async analyzeFile(filePath) {
|
|
3218
4236
|
try {
|
|
3219
4237
|
const code = (0, import_fs6.readFileSync)(filePath, "utf-8");
|
|
3220
|
-
const ast =
|
|
4238
|
+
const ast = parser4.parse(code, {
|
|
3221
4239
|
sourceType: "module",
|
|
3222
4240
|
plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
|
|
3223
4241
|
});
|
|
@@ -3225,13 +4243,13 @@ var FormAnalyzer = class {
|
|
|
3225
4243
|
this.inputElements = [];
|
|
3226
4244
|
this.submitButtons = [];
|
|
3227
4245
|
(0, import_traverse7.default)(ast, {
|
|
3228
|
-
CallExpression: (
|
|
3229
|
-
this.extractStateVariables(
|
|
4246
|
+
CallExpression: (path13) => {
|
|
4247
|
+
this.extractStateVariables(path13.node);
|
|
3230
4248
|
}
|
|
3231
4249
|
});
|
|
3232
4250
|
(0, import_traverse7.default)(ast, {
|
|
3233
|
-
JSXElement: (
|
|
3234
|
-
this.extractFormElements(
|
|
4251
|
+
JSXElement: (path13) => {
|
|
4252
|
+
this.extractFormElements(path13.node);
|
|
3235
4253
|
}
|
|
3236
4254
|
});
|
|
3237
4255
|
const validationRules = this.extractValidationRules(ast);
|
|
@@ -3242,7 +4260,7 @@ var FormAnalyzer = class {
|
|
|
3242
4260
|
}
|
|
3243
4261
|
}
|
|
3244
4262
|
extractStateVariables(node) {
|
|
3245
|
-
if (
|
|
4263
|
+
if (t8.isIdentifier(node.callee) && node.callee.name === "useState" && node.arguments.length > 0) {
|
|
3246
4264
|
return;
|
|
3247
4265
|
}
|
|
3248
4266
|
}
|
|
@@ -3263,7 +4281,7 @@ var FormAnalyzer = class {
|
|
|
3263
4281
|
extractInputInfo(openingElement) {
|
|
3264
4282
|
const info2 = { varName: "" };
|
|
3265
4283
|
for (const attr of openingElement.attributes) {
|
|
3266
|
-
if (
|
|
4284
|
+
if (t8.isJSXAttribute(attr) && t8.isJSXIdentifier(attr.name)) {
|
|
3267
4285
|
const propName = attr.name.name;
|
|
3268
4286
|
const propValue = this.extractAttributeValue(attr.value);
|
|
3269
4287
|
switch (propName) {
|
|
@@ -3284,7 +4302,7 @@ var FormAnalyzer = class {
|
|
|
3284
4302
|
if (propValue) info2.varName = propValue;
|
|
3285
4303
|
break;
|
|
3286
4304
|
case "value":
|
|
3287
|
-
if (attr.value &&
|
|
4305
|
+
if (attr.value && t8.isJSXExpressionContainer(attr.value) && t8.isIdentifier(attr.value.expression)) {
|
|
3288
4306
|
info2.varName = attr.value.expression.name;
|
|
3289
4307
|
}
|
|
3290
4308
|
break;
|
|
@@ -3296,16 +4314,16 @@ var FormAnalyzer = class {
|
|
|
3296
4314
|
extractButtonInfo(openingElement) {
|
|
3297
4315
|
const info2 = {};
|
|
3298
4316
|
for (const attr of openingElement.attributes) {
|
|
3299
|
-
if (
|
|
4317
|
+
if (t8.isJSXAttribute(attr) && t8.isJSXIdentifier(attr.name)) {
|
|
3300
4318
|
const propName = attr.name.name;
|
|
3301
4319
|
if (propName === "title" || propName === "label") {
|
|
3302
4320
|
info2.label = this.extractAttributeValue(attr.value);
|
|
3303
4321
|
}
|
|
3304
4322
|
if (propName === "onPress") {
|
|
3305
|
-
if (attr.value &&
|
|
3306
|
-
if (
|
|
4323
|
+
if (attr.value && t8.isJSXExpressionContainer(attr.value)) {
|
|
4324
|
+
if (t8.isIdentifier(attr.value.expression)) {
|
|
3307
4325
|
info2.handler = attr.value.expression.name;
|
|
3308
|
-
} else if (
|
|
4326
|
+
} else if (t8.isArrowFunctionExpression(attr.value.expression) || t8.isFunctionExpression(attr.value.expression)) {
|
|
3309
4327
|
info2.handler = "anonymous";
|
|
3310
4328
|
}
|
|
3311
4329
|
}
|
|
@@ -3316,14 +4334,14 @@ var FormAnalyzer = class {
|
|
|
3316
4334
|
}
|
|
3317
4335
|
extractAttributeValue(value) {
|
|
3318
4336
|
if (value === null) return void 0;
|
|
3319
|
-
if (
|
|
3320
|
-
if (
|
|
4337
|
+
if (t8.isStringLiteral(value)) return value.value;
|
|
4338
|
+
if (t8.isJSXExpressionContainer(value) && t8.isStringLiteral(value.expression)) {
|
|
3321
4339
|
return value.expression.value;
|
|
3322
4340
|
}
|
|
3323
4341
|
return void 0;
|
|
3324
4342
|
}
|
|
3325
4343
|
getElementName(openingElement) {
|
|
3326
|
-
if (
|
|
4344
|
+
if (t8.isJSXIdentifier(openingElement.name)) {
|
|
3327
4345
|
return openingElement.name.name;
|
|
3328
4346
|
}
|
|
3329
4347
|
return null;
|
|
@@ -3331,8 +4349,8 @@ var FormAnalyzer = class {
|
|
|
3331
4349
|
extractValidationRules(ast) {
|
|
3332
4350
|
const rules = {};
|
|
3333
4351
|
(0, import_traverse7.default)(ast, {
|
|
3334
|
-
IfStatement: (
|
|
3335
|
-
const test =
|
|
4352
|
+
IfStatement: (path13) => {
|
|
4353
|
+
const test = path13.node.test;
|
|
3336
4354
|
const rule = this.extractRuleFromCondition(test);
|
|
3337
4355
|
if (rule) {
|
|
3338
4356
|
const { field, description } = rule;
|
|
@@ -3347,36 +4365,36 @@ var FormAnalyzer = class {
|
|
|
3347
4365
|
extractRuleFromCondition(test) {
|
|
3348
4366
|
let fieldName = "";
|
|
3349
4367
|
let description = "";
|
|
3350
|
-
if (
|
|
3351
|
-
if (
|
|
4368
|
+
if (t8.isUnaryExpression(test) && test.operator === "!") {
|
|
4369
|
+
if (t8.isCallExpression(test.argument)) {
|
|
3352
4370
|
const callExpr = test.argument;
|
|
3353
|
-
if (
|
|
4371
|
+
if (t8.isMemberExpression(callExpr.callee)) {
|
|
3354
4372
|
const memberExpr = callExpr.callee;
|
|
3355
|
-
if (
|
|
4373
|
+
if (t8.isIdentifier(memberExpr.object)) {
|
|
3356
4374
|
fieldName = memberExpr.object.name;
|
|
3357
4375
|
description = `${fieldName} is required`;
|
|
3358
4376
|
}
|
|
3359
4377
|
}
|
|
3360
|
-
} else if (
|
|
4378
|
+
} else if (t8.isIdentifier(test.argument)) {
|
|
3361
4379
|
fieldName = test.argument.name;
|
|
3362
4380
|
description = `${fieldName} is required`;
|
|
3363
4381
|
}
|
|
3364
4382
|
}
|
|
3365
|
-
if (
|
|
3366
|
-
if (
|
|
4383
|
+
if (t8.isBinaryExpression(test) && (test.operator === "<" || test.operator === "<=")) {
|
|
4384
|
+
if (t8.isMemberExpression(test.left)) {
|
|
3367
4385
|
const memberExpr = test.left;
|
|
3368
|
-
if (
|
|
4386
|
+
if (t8.isIdentifier(memberExpr.object)) {
|
|
3369
4387
|
fieldName = memberExpr.object.name;
|
|
3370
|
-
if (
|
|
4388
|
+
if (t8.isNumericLiteral(test.right)) {
|
|
3371
4389
|
description = `${fieldName} must be at least ${test.right.value} characters`;
|
|
3372
4390
|
}
|
|
3373
4391
|
}
|
|
3374
4392
|
}
|
|
3375
4393
|
}
|
|
3376
|
-
if (
|
|
3377
|
-
if (
|
|
3378
|
-
const methodName =
|
|
3379
|
-
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)) {
|
|
3380
4398
|
fieldName = test.argument.callee.object.name;
|
|
3381
4399
|
description = `${fieldName} format is invalid`;
|
|
3382
4400
|
}
|
|
@@ -3386,7 +4404,7 @@ var FormAnalyzer = class {
|
|
|
3386
4404
|
}
|
|
3387
4405
|
buildForms(filePath, validationRules) {
|
|
3388
4406
|
if (this.inputElements.length === 0) return [];
|
|
3389
|
-
const fileName =
|
|
4407
|
+
const fileName = path5.basename(filePath, path5.extname(filePath));
|
|
3390
4408
|
const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
|
|
3391
4409
|
const fields = this.inputElements.map((input) => {
|
|
3392
4410
|
const fieldType = this.inferFieldType(input);
|
|
@@ -3436,14 +4454,104 @@ var FormAnalyzer = class {
|
|
|
3436
4454
|
}
|
|
3437
4455
|
};
|
|
3438
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
|
+
|
|
3439
4551
|
// src/analyzers/ReactNativePlatformAnalyzer.ts
|
|
3440
4552
|
var ReactNativePlatformAnalyzer = class {
|
|
3441
4553
|
platform = "react-native";
|
|
3442
4554
|
async analyze(config, options) {
|
|
3443
|
-
const screenAnalyzer = new ScreenAnalyzer(config, {
|
|
3444
|
-
strictScreens: options.strictScreens ?? true,
|
|
3445
|
-
screenPatterns: options.screenPatterns
|
|
3446
|
-
});
|
|
3447
4555
|
const navigationAnalyzer = new NavigationAnalyzer(config, {
|
|
3448
4556
|
navigationInclude: options.navigationInclude,
|
|
3449
4557
|
navigationExclude: options.navigationExclude
|
|
@@ -3451,14 +4559,31 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3451
4559
|
const componentAnalyzer = new ComponentAnalyzer(config);
|
|
3452
4560
|
const formAnalyzer = new FormAnalyzer(config);
|
|
3453
4561
|
console.log("[ReactNativePlatformAnalyzer] Running analyzers...");
|
|
3454
|
-
const
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
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
|
+
);
|
|
3458
4583
|
console.log(
|
|
3459
4584
|
`[ReactNativePlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens`
|
|
3460
4585
|
);
|
|
3461
|
-
const screenFiles = await (
|
|
4586
|
+
const screenFiles = await globSorted(config.include || ["**/*.tsx", "**/*.ts"], {
|
|
3462
4587
|
cwd: config.rootDir,
|
|
3463
4588
|
ignore: config.exclude || ["**/node_modules/**"]
|
|
3464
4589
|
});
|
|
@@ -3466,7 +4591,7 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3466
4591
|
`[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
|
|
3467
4592
|
);
|
|
3468
4593
|
const enrichmentPromises = screenFiles.map(async (file2) => {
|
|
3469
|
-
const filePath =
|
|
4594
|
+
const filePath = import_node_path3.default.resolve(config.rootDir, file2);
|
|
3470
4595
|
try {
|
|
3471
4596
|
const [components, forms] = await Promise.all([
|
|
3472
4597
|
componentAnalyzer.analyzeFile(filePath),
|
|
@@ -3521,7 +4646,8 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3521
4646
|
screens: enrichedScreens,
|
|
3522
4647
|
navigation,
|
|
3523
4648
|
analyzedFiles: screenFiles.length,
|
|
3524
|
-
...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
|
|
4649
|
+
...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {},
|
|
4650
|
+
diagnostics: { navigation: navigationAnalyzer.diagnostics }
|
|
3525
4651
|
};
|
|
3526
4652
|
}
|
|
3527
4653
|
mergeForm(target, source) {
|
|
@@ -3539,9 +4665,12 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3539
4665
|
findEquivalentField(fields, incoming) {
|
|
3540
4666
|
return fields.find((field) => {
|
|
3541
4667
|
if (field.name && incoming.name && field.name === incoming.name) return true;
|
|
3542
|
-
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
3543
|
-
|
|
3544
|
-
if (field.
|
|
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;
|
|
3545
4674
|
if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
|
|
3546
4675
|
return true;
|
|
3547
4676
|
}
|
|
@@ -3574,7 +4703,9 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
3574
4703
|
if (incoming.fields.length === 0) return void 0;
|
|
3575
4704
|
let best;
|
|
3576
4705
|
for (const form of forms) {
|
|
3577
|
-
const overlap = incoming.fields.filter(
|
|
4706
|
+
const overlap = incoming.fields.filter(
|
|
4707
|
+
(field) => this.findEquivalentField(form.fields, field)
|
|
4708
|
+
).length;
|
|
3578
4709
|
if (overlap > 0 && (!best || overlap > best.overlap)) {
|
|
3579
4710
|
best = { form, overlap };
|
|
3580
4711
|
}
|
|
@@ -3601,9 +4732,8 @@ var GenericPlatformAnalyzer = class {
|
|
|
3601
4732
|
// src/analyzers/web/WebScreenAnalyzer.ts
|
|
3602
4733
|
var import_promises2 = __toESM(require("fs/promises"));
|
|
3603
4734
|
var import_path5 = __toESM(require("path"));
|
|
3604
|
-
var import_fast_glob4 = __toESM(require("fast-glob"));
|
|
3605
4735
|
var import_traverse9 = __toESM(require("@babel/traverse"));
|
|
3606
|
-
var
|
|
4736
|
+
var t11 = __toESM(require("@babel/types"));
|
|
3607
4737
|
|
|
3608
4738
|
// src/ast/jsx/web/classify.ts
|
|
3609
4739
|
var VIEW_TAGS = /* @__PURE__ */ new Set([
|
|
@@ -3719,16 +4849,16 @@ function classifyWebJsxComponent(name, element) {
|
|
|
3719
4849
|
}
|
|
3720
4850
|
|
|
3721
4851
|
// src/ast/jsx/names.ts
|
|
3722
|
-
var
|
|
4852
|
+
var t9 = __toESM(require("@babel/types"));
|
|
3723
4853
|
function getJsxElementName(openingElement) {
|
|
3724
4854
|
return getJsxName(openingElement.name);
|
|
3725
4855
|
}
|
|
3726
4856
|
function getJsxName(name) {
|
|
3727
|
-
if (
|
|
3728
|
-
if (
|
|
3729
|
-
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)) {
|
|
3730
4860
|
const objectName = getJsxName(name.object);
|
|
3731
|
-
const propertyName =
|
|
4861
|
+
const propertyName = t9.isJSXIdentifier(name.property) ? name.property.name : null;
|
|
3732
4862
|
return objectName && propertyName ? `${objectName}.${propertyName}` : null;
|
|
3733
4863
|
}
|
|
3734
4864
|
return null;
|
|
@@ -3736,32 +4866,32 @@ function getJsxName(name) {
|
|
|
3736
4866
|
|
|
3737
4867
|
// src/ast/navigation/web-calls.ts
|
|
3738
4868
|
var import_traverse8 = __toESM(require("@babel/traverse"));
|
|
3739
|
-
var
|
|
4869
|
+
var t10 = __toESM(require("@babel/types"));
|
|
3740
4870
|
var ROUTERISH_OBJECTS = /^(router|history|navigation)$/;
|
|
3741
4871
|
function staticRoutePath(node) {
|
|
3742
4872
|
if (!node) return void 0;
|
|
3743
|
-
if (
|
|
3744
|
-
if (
|
|
3745
|
-
let
|
|
4873
|
+
if (t10.isStringLiteral(node)) return node.value;
|
|
4874
|
+
if (t10.isTemplateLiteral(node)) {
|
|
4875
|
+
let path13 = "";
|
|
3746
4876
|
node.quasis.forEach((quasi, index) => {
|
|
3747
|
-
|
|
4877
|
+
path13 += quasi.value.cooked ?? quasi.value.raw;
|
|
3748
4878
|
const expr = node.expressions[index];
|
|
3749
|
-
if (expr)
|
|
4879
|
+
if (expr) path13 += `:${paramNameOf(expr)}`;
|
|
3750
4880
|
});
|
|
3751
|
-
return
|
|
4881
|
+
return path13;
|
|
3752
4882
|
}
|
|
3753
4883
|
return void 0;
|
|
3754
4884
|
}
|
|
3755
4885
|
function paramNameOf(expr) {
|
|
3756
|
-
if (
|
|
3757
|
-
if (
|
|
4886
|
+
if (t10.isIdentifier(expr)) return expr.name;
|
|
4887
|
+
if (t10.isMemberExpression(expr) && t10.isIdentifier(expr.property)) return expr.property.name;
|
|
3758
4888
|
return "param";
|
|
3759
4889
|
}
|
|
3760
4890
|
function extractWebNavigationCalls(ast) {
|
|
3761
4891
|
const calls = [];
|
|
3762
4892
|
const inspect = (node) => {
|
|
3763
|
-
if (
|
|
3764
|
-
if (
|
|
4893
|
+
if (t10.isCallExpression(node)) {
|
|
4894
|
+
if (t10.isIdentifier(node.callee) && node.callee.name === "navigate") {
|
|
3765
4895
|
const first = node.arguments[0];
|
|
3766
4896
|
const targetPath = staticRoutePath(first);
|
|
3767
4897
|
if (targetPath !== void 0) {
|
|
@@ -3769,12 +4899,12 @@ function extractWebNavigationCalls(ast) {
|
|
|
3769
4899
|
method: hasReplaceOption(node.arguments[1]) ? "replace" : "navigate",
|
|
3770
4900
|
targetPath
|
|
3771
4901
|
});
|
|
3772
|
-
} else if (
|
|
4902
|
+
} else if (t10.isNumericLiteral(first) || t10.isUnaryExpression(first) && first.operator === "-") {
|
|
3773
4903
|
calls.push({ method: "goBack" });
|
|
3774
4904
|
}
|
|
3775
4905
|
return;
|
|
3776
4906
|
}
|
|
3777
|
-
if (
|
|
4907
|
+
if (t10.isMemberExpression(node.callee) && t10.isIdentifier(node.callee.object) && ROUTERISH_OBJECTS.test(node.callee.object.name) && t10.isIdentifier(node.callee.property)) {
|
|
3778
4908
|
const method = node.callee.property.name;
|
|
3779
4909
|
const targetPath = staticRoutePath(node.arguments[0]);
|
|
3780
4910
|
if ((method === "push" || method === "navigate") && targetPath !== void 0) {
|
|
@@ -3787,25 +4917,25 @@ function extractWebNavigationCalls(ast) {
|
|
|
3787
4917
|
}
|
|
3788
4918
|
return;
|
|
3789
4919
|
}
|
|
3790
|
-
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("/")) {
|
|
3791
4921
|
calls.push({ method: "navigate", targetPath: node.right.value });
|
|
3792
4922
|
}
|
|
3793
4923
|
};
|
|
3794
4924
|
(0, import_traverse8.default)(ast, {
|
|
3795
|
-
noScope: !
|
|
4925
|
+
noScope: !t10.isFile(ast),
|
|
3796
4926
|
enter: (nodePath) => inspect(nodePath.node)
|
|
3797
4927
|
});
|
|
3798
4928
|
return calls;
|
|
3799
4929
|
}
|
|
3800
4930
|
function hasReplaceOption(arg) {
|
|
3801
|
-
if (!arg || !
|
|
4931
|
+
if (!arg || !t10.isObjectExpression(arg)) return false;
|
|
3802
4932
|
return arg.properties.some(
|
|
3803
|
-
(prop) =>
|
|
4933
|
+
(prop) => t10.isObjectProperty(prop) && t10.isIdentifier(prop.key) && prop.key.name === "replace" && t10.isBooleanLiteral(prop.value) && prop.value.value === true
|
|
3804
4934
|
);
|
|
3805
4935
|
}
|
|
3806
4936
|
function isLocationExpression(node) {
|
|
3807
|
-
if (
|
|
3808
|
-
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";
|
|
3809
4939
|
}
|
|
3810
4940
|
|
|
3811
4941
|
// src/analyzers/web/WebScreenAnalyzer.ts
|
|
@@ -3833,10 +4963,10 @@ var WebScreenAnalyzer = class {
|
|
|
3833
4963
|
include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
|
|
3834
4964
|
exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
3835
4965
|
} = this.config;
|
|
3836
|
-
const files = (await (
|
|
4966
|
+
const files = (await globSorted(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
|
|
3837
4967
|
(file2) => !file2.endsWith(".d.ts")
|
|
3838
4968
|
);
|
|
3839
|
-
const patternMatches = await (
|
|
4969
|
+
const patternMatches = await globSorted(this.screenPatterns, {
|
|
3840
4970
|
cwd: this.config.rootDir,
|
|
3841
4971
|
ignore: exclude
|
|
3842
4972
|
});
|
|
@@ -3930,7 +5060,7 @@ var WebScreenAnalyzer = class {
|
|
|
3930
5060
|
CallExpression: (nodePath) => {
|
|
3931
5061
|
if (!isRegisterScreenCallee(nodePath.node.callee)) return;
|
|
3932
5062
|
const arg = nodePath.node.arguments[0];
|
|
3933
|
-
if (
|
|
5063
|
+
if (t11.isObjectExpression(arg)) {
|
|
3934
5064
|
plain = literalToPlain(arg);
|
|
3935
5065
|
}
|
|
3936
5066
|
}
|
|
@@ -3977,20 +5107,20 @@ var WebScreenAnalyzer = class {
|
|
|
3977
5107
|
(0, import_traverse9.default)(ast, {
|
|
3978
5108
|
ExportDefaultDeclaration: (nodePath) => {
|
|
3979
5109
|
const declaration = nodePath.node.declaration;
|
|
3980
|
-
if (
|
|
5110
|
+
if (t11.isFunctionDeclaration(declaration) && declaration.id?.name) {
|
|
3981
5111
|
defaultName = declaration.id.name;
|
|
3982
|
-
} else if (
|
|
5112
|
+
} else if (t11.isIdentifier(declaration)) {
|
|
3983
5113
|
defaultName = declaration.name;
|
|
3984
5114
|
}
|
|
3985
5115
|
},
|
|
3986
5116
|
ExportNamedDeclaration: (nodePath) => {
|
|
3987
5117
|
if (firstExported) return;
|
|
3988
5118
|
const declaration = nodePath.node.declaration;
|
|
3989
|
-
if (
|
|
5119
|
+
if (t11.isFunctionDeclaration(declaration) && declaration.id && /^[A-Z]/.test(declaration.id.name)) {
|
|
3990
5120
|
firstExported = declaration.id.name;
|
|
3991
|
-
} else if (
|
|
5121
|
+
} else if (t11.isVariableDeclaration(declaration)) {
|
|
3992
5122
|
for (const declarator of declaration.declarations) {
|
|
3993
|
-
if (
|
|
5123
|
+
if (t11.isIdentifier(declarator.id) && /^[A-Z]/.test(declarator.id.name) && (t11.isArrowFunctionExpression(declarator.init) || t11.isFunctionExpression(declarator.init))) {
|
|
3994
5124
|
firstExported = declarator.id.name;
|
|
3995
5125
|
break;
|
|
3996
5126
|
}
|
|
@@ -4162,15 +5292,15 @@ var WebScreenAnalyzer = class {
|
|
|
4162
5292
|
});
|
|
4163
5293
|
}
|
|
4164
5294
|
for (const attr of opening.attributes) {
|
|
4165
|
-
if (!
|
|
5295
|
+
if (!t11.isJSXAttribute(attr) || !t11.isJSXIdentifier(attr.name)) continue;
|
|
4166
5296
|
const attrName = attr.name.name;
|
|
4167
5297
|
if (attrName === "required") {
|
|
4168
5298
|
if (attr.value === null) field.required = true;
|
|
4169
|
-
else if (
|
|
5299
|
+
else if (t11.isJSXExpressionContainer(attr.value) && t11.isBooleanLiteral(attr.value.expression)) {
|
|
4170
5300
|
field.required = attr.value.expression.value;
|
|
4171
5301
|
}
|
|
4172
5302
|
}
|
|
4173
|
-
if ((attrName === "value" || attrName === "checked") && attr.value &&
|
|
5303
|
+
if ((attrName === "value" || attrName === "checked") && attr.value && t11.isJSXExpressionContainer(attr.value) && t11.isIdentifier(attr.value.expression)) {
|
|
4174
5304
|
field.valueBinding = attr.value.expression.name;
|
|
4175
5305
|
if (!field.name) field.name = attr.value.expression.name;
|
|
4176
5306
|
}
|
|
@@ -4220,7 +5350,7 @@ var WebScreenAnalyzer = class {
|
|
|
4220
5350
|
extractSelectOptions(selectElement) {
|
|
4221
5351
|
const options = [];
|
|
4222
5352
|
for (const child of selectElement.children) {
|
|
4223
|
-
if (!
|
|
5353
|
+
if (!t11.isJSXElement(child)) continue;
|
|
4224
5354
|
if (getJsxElementName(child.openingElement) !== "option") continue;
|
|
4225
5355
|
const value = getStringAttr(child.openingElement, "value");
|
|
4226
5356
|
const label = jsxTextContent(child) || value || "";
|
|
@@ -4331,14 +5461,14 @@ var WebScreenAnalyzer = class {
|
|
|
4331
5461
|
}
|
|
4332
5462
|
let handlerName;
|
|
4333
5463
|
const onClickAttr = opening.attributes.find(
|
|
4334
|
-
(attr) =>
|
|
5464
|
+
(attr) => t11.isJSXAttribute(attr) && t11.isJSXIdentifier(attr.name) && attr.name.name === "onClick"
|
|
4335
5465
|
);
|
|
4336
|
-
if (onClickAttr?.value &&
|
|
5466
|
+
if (onClickAttr?.value && t11.isJSXExpressionContainer(onClickAttr.value)) {
|
|
4337
5467
|
const expr = onClickAttr.value.expression;
|
|
4338
|
-
if (
|
|
5468
|
+
if (t11.isIdentifier(expr)) {
|
|
4339
5469
|
handlerName = expr.name;
|
|
4340
5470
|
action.handler = expr.name;
|
|
4341
|
-
} else if (!
|
|
5471
|
+
} else if (!t11.isJSXEmptyExpression(expr)) {
|
|
4342
5472
|
const inlineNavCalls = extractWebNavigationCalls(expr);
|
|
4343
5473
|
const inlineNav = inlineNavCalls.find((call) => call.targetPath);
|
|
4344
5474
|
if (inlineNav?.targetPath) {
|
|
@@ -4477,12 +5607,12 @@ var WebScreenAnalyzer = class {
|
|
|
4477
5607
|
}
|
|
4478
5608
|
handlerNameFromAttr(opening, attrName) {
|
|
4479
5609
|
const attr = opening.attributes.find(
|
|
4480
|
-
(candidate) =>
|
|
5610
|
+
(candidate) => t11.isJSXAttribute(candidate) && t11.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
|
|
4481
5611
|
);
|
|
4482
|
-
if (!attr?.value || !
|
|
5612
|
+
if (!attr?.value || !t11.isJSXExpressionContainer(attr.value)) return void 0;
|
|
4483
5613
|
const expr = attr.value.expression;
|
|
4484
|
-
if (
|
|
4485
|
-
if (
|
|
5614
|
+
if (t11.isIdentifier(expr)) return expr.name;
|
|
5615
|
+
if (t11.isArrowFunctionExpression(expr) || t11.isFunctionExpression(expr)) {
|
|
4486
5616
|
return firstCalledFunctionName(expr) ?? "anonymous";
|
|
4487
5617
|
}
|
|
4488
5618
|
return void 0;
|
|
@@ -4516,11 +5646,11 @@ var WebScreenAnalyzer = class {
|
|
|
4516
5646
|
(0, import_traverse9.default)(ast, {
|
|
4517
5647
|
JSXExpressionContainer: (nodePath) => {
|
|
4518
5648
|
const expr = nodePath.node.expression;
|
|
4519
|
-
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") {
|
|
4520
5650
|
return;
|
|
4521
5651
|
}
|
|
4522
5652
|
const callback = expr.arguments[0];
|
|
4523
|
-
if (!
|
|
5653
|
+
if (!t11.isArrowFunctionExpression(callback) && !t11.isFunctionExpression(callback)) return;
|
|
4524
5654
|
const enclosing = nodePath.findParent(
|
|
4525
5655
|
(p) => p.isJSXElement()
|
|
4526
5656
|
);
|
|
@@ -4555,22 +5685,22 @@ var WebScreenAnalyzer = class {
|
|
|
4555
5685
|
}
|
|
4556
5686
|
};
|
|
4557
5687
|
function isRegisterScreenCallee(callee) {
|
|
4558
|
-
return
|
|
5688
|
+
return t11.isIdentifier(callee) && callee.name === "registerScreen" || t11.isMemberExpression(callee) && t11.isIdentifier(callee.property) && callee.property.name === "registerScreen";
|
|
4559
5689
|
}
|
|
4560
5690
|
function literalToPlain(node) {
|
|
4561
|
-
if (
|
|
5691
|
+
if (t11.isStringLiteral(node) || t11.isNumericLiteral(node) || t11.isBooleanLiteral(node)) {
|
|
4562
5692
|
return node.value;
|
|
4563
5693
|
}
|
|
4564
|
-
if (
|
|
4565
|
-
if (
|
|
4566
|
-
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);
|
|
4567
5697
|
}
|
|
4568
|
-
if (
|
|
5698
|
+
if (t11.isObjectExpression(node)) {
|
|
4569
5699
|
const out = {};
|
|
4570
5700
|
for (const prop of node.properties) {
|
|
4571
|
-
if (!
|
|
4572
|
-
const key =
|
|
4573
|
-
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;
|
|
4574
5704
|
const value = literalToPlain(prop.value);
|
|
4575
5705
|
if (value !== void 0) out[key] = value;
|
|
4576
5706
|
}
|
|
@@ -4582,12 +5712,12 @@ function jsxTextContent(element) {
|
|
|
4582
5712
|
const parts = [];
|
|
4583
5713
|
const walk = (children) => {
|
|
4584
5714
|
for (const child of children) {
|
|
4585
|
-
if (
|
|
5715
|
+
if (t11.isJSXText(child)) {
|
|
4586
5716
|
const trimmed = child.value.replace(/\s+/g, " ").trim();
|
|
4587
5717
|
if (trimmed) parts.push(trimmed);
|
|
4588
|
-
} else if (
|
|
5718
|
+
} else if (t11.isJSXExpressionContainer(child) && t11.isStringLiteral(child.expression)) {
|
|
4589
5719
|
parts.push(child.expression.value);
|
|
4590
|
-
} else if (
|
|
5720
|
+
} else if (t11.isJSXElement(child)) {
|
|
4591
5721
|
walk(child.children);
|
|
4592
5722
|
}
|
|
4593
5723
|
}
|
|
@@ -4597,26 +5727,26 @@ function jsxTextContent(element) {
|
|
|
4597
5727
|
return text.length > 0 ? text : void 0;
|
|
4598
5728
|
}
|
|
4599
5729
|
function firstCalledFunctionName(node) {
|
|
4600
|
-
if (
|
|
4601
|
-
if (
|
|
5730
|
+
if (t11.isIdentifier(node)) return node.name;
|
|
5731
|
+
if (t11.isArrowFunctionExpression(node) || t11.isFunctionExpression(node)) {
|
|
4602
5732
|
return firstCalledFunctionName(node.body);
|
|
4603
5733
|
}
|
|
4604
|
-
if (
|
|
5734
|
+
if (t11.isBlockStatement(node)) {
|
|
4605
5735
|
for (const statement of node.body) {
|
|
4606
5736
|
const handler = firstCalledFunctionName(statement);
|
|
4607
5737
|
if (handler) return handler;
|
|
4608
5738
|
}
|
|
4609
5739
|
return void 0;
|
|
4610
5740
|
}
|
|
4611
|
-
if (
|
|
4612
|
-
if (
|
|
5741
|
+
if (t11.isExpressionStatement(node)) return firstCalledFunctionName(node.expression);
|
|
5742
|
+
if (t11.isReturnStatement(node)) {
|
|
4613
5743
|
return node.argument ? firstCalledFunctionName(node.argument) : void 0;
|
|
4614
5744
|
}
|
|
4615
|
-
if (
|
|
5745
|
+
if (t11.isAwaitExpression(node) || t11.isUnaryExpression(node)) {
|
|
4616
5746
|
return firstCalledFunctionName(node.argument);
|
|
4617
5747
|
}
|
|
4618
|
-
if (
|
|
4619
|
-
if (
|
|
5748
|
+
if (t11.isCallExpression(node)) {
|
|
5749
|
+
if (t11.isIdentifier(node.callee) && !/^(navigate|confirm|alert)$/.test(node.callee.name)) {
|
|
4620
5750
|
return node.callee.name;
|
|
4621
5751
|
}
|
|
4622
5752
|
return void 0;
|
|
@@ -4631,8 +5761,8 @@ function containsWindowConfirm(body) {
|
|
|
4631
5761
|
noScope: true,
|
|
4632
5762
|
CallExpression: (nodePath) => {
|
|
4633
5763
|
const callee = nodePath.node.callee;
|
|
4634
|
-
if (
|
|
4635
|
-
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") {
|
|
4636
5766
|
found = true;
|
|
4637
5767
|
}
|
|
4638
5768
|
}
|
|
@@ -4647,9 +5777,9 @@ function routePathAttr(opening, attrName) {
|
|
|
4647
5777
|
const literal = getStringAttr(opening, attrName);
|
|
4648
5778
|
if (literal) return literal;
|
|
4649
5779
|
const attr = opening.attributes.find(
|
|
4650
|
-
(candidate) =>
|
|
5780
|
+
(candidate) => t11.isJSXAttribute(candidate) && t11.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
|
|
4651
5781
|
);
|
|
4652
|
-
if (!attr?.value || !
|
|
5782
|
+
if (!attr?.value || !t11.isJSXExpressionContainer(attr.value)) return void 0;
|
|
4653
5783
|
return staticRoutePath(attr.value.expression);
|
|
4654
5784
|
}
|
|
4655
5785
|
function slugify(label) {
|
|
@@ -4664,10 +5794,10 @@ function isWeakInferredFieldName(name) {
|
|
|
4664
5794
|
function collectionItemNames(callback) {
|
|
4665
5795
|
const names = /* @__PURE__ */ new Set(["item"]);
|
|
4666
5796
|
const firstParam = callback.params[0];
|
|
4667
|
-
if (
|
|
4668
|
-
if (
|
|
5797
|
+
if (t11.isIdentifier(firstParam)) names.add(firstParam.name);
|
|
5798
|
+
if (t11.isObjectPattern(firstParam)) {
|
|
4669
5799
|
for (const prop of firstParam.properties) {
|
|
4670
|
-
if (
|
|
5800
|
+
if (t11.isObjectProperty(prop) && t11.isIdentifier(prop.key) && t11.isIdentifier(prop.value)) {
|
|
4671
5801
|
names.add(prop.value.name);
|
|
4672
5802
|
}
|
|
4673
5803
|
}
|
|
@@ -4683,7 +5813,7 @@ function collectionDisplayFields(callback, itemNames) {
|
|
|
4683
5813
|
noScope: true,
|
|
4684
5814
|
MemberExpression: (nodePath) => {
|
|
4685
5815
|
const node = nodePath.node;
|
|
4686
|
-
if (
|
|
5816
|
+
if (t11.isIdentifier(node.object) && itemNames.has(node.object.name) && t11.isIdentifier(node.property)) {
|
|
4687
5817
|
fields.add(node.property.name);
|
|
4688
5818
|
}
|
|
4689
5819
|
}
|
|
@@ -4700,10 +5830,10 @@ function collectionKeyField(callback, itemNames) {
|
|
|
4700
5830
|
noScope: true,
|
|
4701
5831
|
JSXAttribute: (nodePath) => {
|
|
4702
5832
|
const attr = nodePath.node;
|
|
4703
|
-
if (!
|
|
4704
|
-
if (!attr.value || !
|
|
5833
|
+
if (!t11.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
|
|
5834
|
+
if (!attr.value || !t11.isJSXExpressionContainer(attr.value)) return;
|
|
4705
5835
|
const expr = attr.value.expression;
|
|
4706
|
-
if (
|
|
5836
|
+
if (t11.isMemberExpression(expr) && t11.isIdentifier(expr.object) && itemNames.has(expr.object.name) && t11.isIdentifier(expr.property)) {
|
|
4707
5837
|
keyField = keyField ?? expr.property.name;
|
|
4708
5838
|
}
|
|
4709
5839
|
}
|
|
@@ -4726,18 +5856,18 @@ function callbackReturnsTag(callback, tags) {
|
|
|
4726
5856
|
let found = false;
|
|
4727
5857
|
const inspect = (node) => {
|
|
4728
5858
|
if (!node || found) return;
|
|
4729
|
-
if (
|
|
5859
|
+
if (t11.isJSXElement(node)) {
|
|
4730
5860
|
const name = getJsxElementName(node.openingElement);
|
|
4731
5861
|
if (name && tags.has(name)) found = true;
|
|
4732
5862
|
return;
|
|
4733
5863
|
}
|
|
4734
|
-
if (
|
|
5864
|
+
if (t11.isBlockStatement(node)) {
|
|
4735
5865
|
for (const statement of node.body) {
|
|
4736
|
-
if (
|
|
5866
|
+
if (t11.isReturnStatement(statement)) inspect(statement.argument);
|
|
4737
5867
|
}
|
|
4738
5868
|
}
|
|
4739
|
-
if (
|
|
4740
|
-
if (
|
|
5869
|
+
if (t11.isParenthesizedExpression(node)) inspect(node.expression);
|
|
5870
|
+
if (t11.isConditionalExpression(node)) {
|
|
4741
5871
|
inspect(node.consequent);
|
|
4742
5872
|
inspect(node.alternate);
|
|
4743
5873
|
}
|
|
@@ -4764,9 +5894,8 @@ function inferIdentityFields(keyField, displayFields) {
|
|
|
4764
5894
|
// src/analyzers/web/WebNavigationAnalyzer.ts
|
|
4765
5895
|
var import_fs7 = require("fs");
|
|
4766
5896
|
var import_path6 = __toESM(require("path"));
|
|
4767
|
-
var import_fast_glob5 = __toESM(require("fast-glob"));
|
|
4768
5897
|
var import_traverse10 = __toESM(require("@babel/traverse"));
|
|
4769
|
-
var
|
|
5898
|
+
var t12 = __toESM(require("@babel/types"));
|
|
4770
5899
|
var WEB_NAVIGATOR_TYPE = "route";
|
|
4771
5900
|
var WebNavigationAnalyzer = class {
|
|
4772
5901
|
config;
|
|
@@ -4783,7 +5912,9 @@ var WebNavigationAnalyzer = class {
|
|
|
4783
5912
|
for (const filePath of files) {
|
|
4784
5913
|
try {
|
|
4785
5914
|
const content = await import_fs7.promises.readFile(filePath, "utf-8");
|
|
4786
|
-
if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
|
|
5915
|
+
if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
|
|
5916
|
+
content
|
|
5917
|
+
)) {
|
|
4787
5918
|
continue;
|
|
4788
5919
|
}
|
|
4789
5920
|
const ast = parseSource(content, this.config.parserPlugins);
|
|
@@ -4813,7 +5944,7 @@ var WebNavigationAnalyzer = class {
|
|
|
4813
5944
|
...this.config.exclude || [],
|
|
4814
5945
|
...this.navigationExclude
|
|
4815
5946
|
];
|
|
4816
|
-
const files = await (
|
|
5947
|
+
const files = await globSorted(patterns, { cwd: this.config.rootDir, ignore });
|
|
4817
5948
|
return files.map((file2) => import_path6.default.join(this.config.rootDir, file2));
|
|
4818
5949
|
}
|
|
4819
5950
|
// ── JSX <Route> style ────────────────────────────────────────────
|
|
@@ -4824,7 +5955,7 @@ var WebNavigationAnalyzer = class {
|
|
|
4824
5955
|
const name = getJsxElementName(opening);
|
|
4825
5956
|
if (name !== "Route") {
|
|
4826
5957
|
for (const child of element.children) {
|
|
4827
|
-
if (
|
|
5958
|
+
if (t12.isJSXElement(child)) visitRoute(child, parentPath);
|
|
4828
5959
|
}
|
|
4829
5960
|
return;
|
|
4830
5961
|
}
|
|
@@ -4833,13 +5964,13 @@ var WebNavigationAnalyzer = class {
|
|
|
4833
5964
|
const fullPath = this.joinPaths(parentPath, segment, isIndex);
|
|
4834
5965
|
const componentName = this.componentNameFromElementAttr(opening) ?? void 0;
|
|
4835
5966
|
const isLeaf = !element.children.some(
|
|
4836
|
-
(child) =>
|
|
5967
|
+
(child) => t12.isJSXElement(child) && getJsxElementName(child.openingElement) === "Route"
|
|
4837
5968
|
);
|
|
4838
5969
|
if ((segment || isIndex) && (componentName || isLeaf)) {
|
|
4839
5970
|
routes.push(this.buildRoute(fullPath, componentName, isIndex, !isLeaf));
|
|
4840
5971
|
}
|
|
4841
5972
|
for (const child of element.children) {
|
|
4842
|
-
if (
|
|
5973
|
+
if (t12.isJSXElement(child)) visitRoute(child, fullPath);
|
|
4843
5974
|
}
|
|
4844
5975
|
};
|
|
4845
5976
|
(0, import_traverse10.default)(ast, {
|
|
@@ -4861,13 +5992,13 @@ var WebNavigationAnalyzer = class {
|
|
|
4861
5992
|
/** `element={<VehicleList/>}` or `Component={VehicleList}`. */
|
|
4862
5993
|
componentNameFromElementAttr(opening) {
|
|
4863
5994
|
for (const attr of opening.attributes) {
|
|
4864
|
-
if (!
|
|
4865
|
-
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)) {
|
|
4866
5997
|
const expr = attr.value.expression;
|
|
4867
|
-
if (
|
|
5998
|
+
if (t12.isJSXElement(expr)) return getJsxElementName(expr.openingElement);
|
|
4868
5999
|
}
|
|
4869
|
-
if (attr.name.name === "Component" &&
|
|
4870
|
-
if (
|
|
6000
|
+
if (attr.name.name === "Component" && t12.isJSXExpressionContainer(attr.value)) {
|
|
6001
|
+
if (t12.isIdentifier(attr.value.expression)) return attr.value.expression.name;
|
|
4871
6002
|
}
|
|
4872
6003
|
}
|
|
4873
6004
|
return null;
|
|
@@ -4884,9 +6015,9 @@ var WebNavigationAnalyzer = class {
|
|
|
4884
6015
|
(0, import_traverse10.default)(ast, {
|
|
4885
6016
|
CallExpression: (nodePath) => {
|
|
4886
6017
|
const callee = nodePath.node.callee;
|
|
4887
|
-
if (!
|
|
6018
|
+
if (!t12.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
|
|
4888
6019
|
const first = nodePath.node.arguments[0];
|
|
4889
|
-
if (!
|
|
6020
|
+
if (!t12.isArrayExpression(first)) return;
|
|
4890
6021
|
this.visitRouteObjects(first, "", routes);
|
|
4891
6022
|
}
|
|
4892
6023
|
});
|
|
@@ -4894,21 +6025,21 @@ var WebNavigationAnalyzer = class {
|
|
|
4894
6025
|
}
|
|
4895
6026
|
visitRouteObjects(arr, parentPath, out) {
|
|
4896
6027
|
for (const element of arr.elements) {
|
|
4897
|
-
if (!
|
|
6028
|
+
if (!t12.isObjectExpression(element)) continue;
|
|
4898
6029
|
let segment;
|
|
4899
6030
|
let isIndex = false;
|
|
4900
6031
|
let componentName;
|
|
4901
6032
|
let children;
|
|
4902
6033
|
for (const prop of element.properties) {
|
|
4903
|
-
if (!
|
|
6034
|
+
if (!t12.isObjectProperty(prop) || !t12.isIdentifier(prop.key)) continue;
|
|
4904
6035
|
const key = prop.key.name;
|
|
4905
|
-
if (key === "path" &&
|
|
4906
|
-
if (key === "index" &&
|
|
4907
|
-
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)) {
|
|
4908
6039
|
componentName = getJsxElementName(prop.value.openingElement) ?? void 0;
|
|
4909
6040
|
}
|
|
4910
|
-
if (key === "Component" &&
|
|
4911
|
-
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;
|
|
4912
6043
|
}
|
|
4913
6044
|
const fullPath = this.joinPaths(parentPath, segment, isIndex);
|
|
4914
6045
|
if ((segment !== void 0 || isIndex) && (componentName || !children)) {
|
|
@@ -4986,11 +6117,13 @@ var WebNavigationAnalyzer = class {
|
|
|
4986
6117
|
return {
|
|
4987
6118
|
screens,
|
|
4988
6119
|
initialScreen: initialRoute?.screenName ?? "",
|
|
4989
|
-
navigators: routes.length > 0 ? [
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
6120
|
+
navigators: routes.length > 0 ? [
|
|
6121
|
+
{
|
|
6122
|
+
name: navigatorName,
|
|
6123
|
+
type: WEB_NAVIGATOR_TYPE,
|
|
6124
|
+
screens: screenNames
|
|
6125
|
+
}
|
|
6126
|
+
] : []
|
|
4994
6127
|
};
|
|
4995
6128
|
}
|
|
4996
6129
|
};
|
|
@@ -5109,7 +6242,7 @@ var ReactWebPlatformAnalyzer = class {
|
|
|
5109
6242
|
|
|
5110
6243
|
// src/manifest/loadManifest.ts
|
|
5111
6244
|
var import_promises3 = require("fs/promises");
|
|
5112
|
-
var
|
|
6245
|
+
var import_node_path4 = __toESM(require("path"));
|
|
5113
6246
|
|
|
5114
6247
|
// ../../node_modules/zod/v3/external.js
|
|
5115
6248
|
var external_exports = {};
|
|
@@ -5316,8 +6449,8 @@ var ZodParsedType = util.arrayToEnum([
|
|
|
5316
6449
|
"set"
|
|
5317
6450
|
]);
|
|
5318
6451
|
var getParsedType = (data) => {
|
|
5319
|
-
const
|
|
5320
|
-
switch (
|
|
6452
|
+
const t16 = typeof data;
|
|
6453
|
+
switch (t16) {
|
|
5321
6454
|
case "undefined":
|
|
5322
6455
|
return ZodParsedType.undefined;
|
|
5323
6456
|
case "string":
|
|
@@ -5589,8 +6722,8 @@ function getErrorMap() {
|
|
|
5589
6722
|
|
|
5590
6723
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
5591
6724
|
var makeIssue = (params) => {
|
|
5592
|
-
const { data, path:
|
|
5593
|
-
const fullPath = [...
|
|
6725
|
+
const { data, path: path13, errorMaps, issueData } = params;
|
|
6726
|
+
const fullPath = [...path13, ...issueData.path || []];
|
|
5594
6727
|
const fullIssue = {
|
|
5595
6728
|
...issueData,
|
|
5596
6729
|
path: fullPath
|
|
@@ -5706,11 +6839,11 @@ var errorUtil;
|
|
|
5706
6839
|
|
|
5707
6840
|
// ../../node_modules/zod/v3/types.js
|
|
5708
6841
|
var ParseInputLazyPath = class {
|
|
5709
|
-
constructor(parent, value,
|
|
6842
|
+
constructor(parent, value, path13, key) {
|
|
5710
6843
|
this._cachedPath = [];
|
|
5711
6844
|
this.parent = parent;
|
|
5712
6845
|
this.data = value;
|
|
5713
|
-
this._path =
|
|
6846
|
+
this._path = path13;
|
|
5714
6847
|
this._key = key;
|
|
5715
6848
|
}
|
|
5716
6849
|
get path() {
|
|
@@ -9152,7 +10285,18 @@ var coerce = {
|
|
|
9152
10285
|
};
|
|
9153
10286
|
var NEVER = INVALID;
|
|
9154
10287
|
|
|
9155
|
-
// ../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
|
|
9156
10300
|
var locatorSourceSchema = external_exports.enum([
|
|
9157
10301
|
"appilotsId",
|
|
9158
10302
|
"testID",
|
|
@@ -9331,6 +10475,23 @@ var actionDescriptorSchema = external_exports.object({
|
|
|
9331
10475
|
effect: external_exports.enum(["read", "write", "destructive"]).or(external_exports.string()).optional(),
|
|
9332
10476
|
riskLevel: external_exports.enum(["low", "medium", "high"]).or(external_exports.string()).optional(),
|
|
9333
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(),
|
|
9334
10495
|
appilotsInferred: appilotsInferredActionSchema.optional()
|
|
9335
10496
|
}).passthrough();
|
|
9336
10497
|
var screenPermissionDescriptorSchema = external_exports.object({
|
|
@@ -9513,7 +10674,7 @@ var uploadMCPDocumentSchema = external_exports.object({
|
|
|
9513
10674
|
version: external_exports.string().default("1.0"),
|
|
9514
10675
|
content: external_exports.record(external_exports.unknown())
|
|
9515
10676
|
});
|
|
9516
|
-
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
|
|
10677
|
+
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator", "publish"]);
|
|
9517
10678
|
var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
|
|
9518
10679
|
var createApiKeySchema = external_exports.object({
|
|
9519
10680
|
name: external_exports.string().min(1).max(100),
|
|
@@ -9524,8 +10685,8 @@ var createApiKeySchema = external_exports.object({
|
|
|
9524
10685
|
scope: apiKeyScopeSchema.default("sdk"),
|
|
9525
10686
|
environment: apiKeyEnvironmentSchema.optional(),
|
|
9526
10687
|
expiresAt: external_exports.string().datetime().optional()
|
|
9527
|
-
}).refine((v) => v.scope
|
|
9528
|
-
message:
|
|
10688
|
+
}).refine((v) => v.scope === "operator" || !!v.projectId, {
|
|
10689
|
+
message: "projectId is required for SDK and publishing keys",
|
|
9529
10690
|
path: ["projectId"]
|
|
9530
10691
|
});
|
|
9531
10692
|
var boundedString = (max) => external_exports.string().max(max);
|
|
@@ -9550,6 +10711,15 @@ var snapshotInputSchema = external_exports.object({
|
|
|
9550
10711
|
type: boundedString(40).optional(),
|
|
9551
10712
|
required: external_exports.boolean().optional(),
|
|
9552
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(),
|
|
9553
10723
|
inModal: external_exports.boolean().optional()
|
|
9554
10724
|
}).passthrough();
|
|
9555
10725
|
var snapshotButtonSchema = external_exports.object({
|
|
@@ -9705,6 +10875,32 @@ var agentSnapshotSchema = external_exports.object({
|
|
|
9705
10875
|
* it. Optional: older SDKs never clamp and never send it.
|
|
9706
10876
|
*/
|
|
9707
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(),
|
|
9708
10904
|
/**
|
|
9709
10905
|
* How many of this screen's controls the client could name, split by
|
|
9710
10906
|
* `identityProvenance`. Diagnostic only — the relay never grounds an
|
|
@@ -9738,7 +10934,19 @@ var agentContextSchema = external_exports.object({
|
|
|
9738
10934
|
rootRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
|
|
9739
10935
|
currentRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
|
|
9740
10936
|
routeNames: external_exports.array(boundedString(200)).max(500).optional(),
|
|
9741
|
-
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()
|
|
9742
10950
|
}).passthrough().optional(),
|
|
9743
10951
|
screenMetadata: external_exports.object({
|
|
9744
10952
|
name: boundedString(200).optional(),
|
|
@@ -9844,7 +11052,7 @@ var formFillPayloadSchema = external_exports.object({
|
|
|
9844
11052
|
submitAfterFill: external_exports.boolean().optional()
|
|
9845
11053
|
}).passthrough();
|
|
9846
11054
|
var uiInteractionPayloadSchema = external_exports.object({
|
|
9847
|
-
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(),
|
|
9848
11056
|
// `targetId` is the canonical server/LLM field. SDK runtimes still
|
|
9849
11057
|
// accept `componentId` as a compatibility alias.
|
|
9850
11058
|
targetId: external_exports.string().min(1),
|
|
@@ -10012,8 +11220,15 @@ var continueAgentSchema = external_exports.object({
|
|
|
10012
11220
|
// issue #169 — the observation the server grounds targets against, now
|
|
10013
11221
|
// validated + bounded at the border instead of z.record(z.unknown()).
|
|
10014
11222
|
context: agentContextSchema.optional(),
|
|
10015
|
-
/**
|
|
10016
|
-
|
|
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()
|
|
10017
11232
|
});
|
|
10018
11233
|
var agentAccessLevelSchema = external_exports.enum(["read", "write", "none"]);
|
|
10019
11234
|
var screenPermissionSchema = external_exports.object({
|
|
@@ -10119,15 +11334,18 @@ var themeTokensSchema = external_exports.object({
|
|
|
10119
11334
|
*/
|
|
10120
11335
|
mode: external_exports.enum(["auto", "light", "dark"]).optional()
|
|
10121
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
|
+
});
|
|
10122
11340
|
var projectPersonalizationSchema = external_exports.object({
|
|
10123
11341
|
/** Tone / persona instructions appended to the system prompt. */
|
|
10124
11342
|
personaPrompt: external_exports.string().max(2048).nullable(),
|
|
10125
11343
|
/** Display name in the chat header (e.g. "Aria"). */
|
|
10126
11344
|
assistantName: external_exports.string().max(60).nullable(),
|
|
10127
11345
|
/** URL or remote asset id for the avatar shown next to assistant turns. */
|
|
10128
|
-
assistantAvatar:
|
|
11346
|
+
assistantAvatar: imageSourceField.nullable(),
|
|
10129
11347
|
/** Emoji or image URL for the empty-state icon. Auto-detected by prefix. */
|
|
10130
|
-
emptyStateIcon:
|
|
11348
|
+
emptyStateIcon: imageSourceField.nullable(),
|
|
10131
11349
|
/** First-message text shown in the empty state. */
|
|
10132
11350
|
welcomeMessage: external_exports.string().max(500).nullable(),
|
|
10133
11351
|
/** Title rendered at the top of the chat. */
|
|
@@ -10141,20 +11359,20 @@ var projectPersonalizationSchema = external_exports.object({
|
|
|
10141
11359
|
/** Chat-open FAB background color. Null falls back to theme.colors.primary. */
|
|
10142
11360
|
triggerButtonColor: hexColorSchema.nullable(),
|
|
10143
11361
|
/** Image URL rendered inside the FAB instead of the default chat-bubble icon. */
|
|
10144
|
-
triggerButtonImageUrl:
|
|
11362
|
+
triggerButtonImageUrl: imageSourceField.nullable()
|
|
10145
11363
|
});
|
|
10146
11364
|
var updateProjectPersonalizationSchema = external_exports.object({
|
|
10147
11365
|
personaPrompt: external_exports.string().max(2048).nullable().optional(),
|
|
10148
11366
|
assistantName: external_exports.string().max(60).nullable().optional(),
|
|
10149
|
-
assistantAvatar:
|
|
10150
|
-
emptyStateIcon:
|
|
11367
|
+
assistantAvatar: imageSourceField.nullable().optional(),
|
|
11368
|
+
emptyStateIcon: imageSourceField.nullable().optional(),
|
|
10151
11369
|
welcomeMessage: external_exports.string().max(500).nullable().optional(),
|
|
10152
11370
|
chatTitle: external_exports.string().max(60).nullable().optional(),
|
|
10153
11371
|
poweredByVisible: external_exports.boolean().optional(),
|
|
10154
11372
|
theme: themeTokensSchema.nullable().optional(),
|
|
10155
11373
|
defaultLocale: localeSchema.nullable().optional(),
|
|
10156
11374
|
triggerButtonColor: hexColorSchema.nullable().optional(),
|
|
10157
|
-
triggerButtonImageUrl:
|
|
11375
|
+
triggerButtonImageUrl: imageSourceField.nullable().optional()
|
|
10158
11376
|
}).strict();
|
|
10159
11377
|
var sandboxObservationSchema = external_exports.object({
|
|
10160
11378
|
route: external_exports.string().max(200).optional(),
|
|
@@ -10174,7 +11392,19 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
10174
11392
|
type: external_exports.string().max(40).optional(),
|
|
10175
11393
|
placeholder: external_exports.string().max(200).optional(),
|
|
10176
11394
|
/** Inline validation state (SDK snapshot field). */
|
|
10177
|
-
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()
|
|
10178
11408
|
})
|
|
10179
11409
|
).max(100).optional(),
|
|
10180
11410
|
buttons: external_exports.array(
|
|
@@ -10632,7 +11862,7 @@ var manifestSchema = external_exports.object({
|
|
|
10632
11862
|
navigation: navigationGraphSchema.partial().optional()
|
|
10633
11863
|
}).passthrough();
|
|
10634
11864
|
async function loadManifest(rootDir, manifestPath) {
|
|
10635
|
-
const resolvedPath =
|
|
11865
|
+
const resolvedPath = import_node_path4.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
|
|
10636
11866
|
let raw;
|
|
10637
11867
|
try {
|
|
10638
11868
|
raw = await (0, import_promises3.readFile)(resolvedPath, "utf-8");
|
|
@@ -10781,7 +12011,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10781
12011
|
const agentReadyScreens = mergedScreens.map(
|
|
10782
12012
|
(screen) => enrichScreenForAgent({
|
|
10783
12013
|
...screen,
|
|
10784
|
-
filePath: screen.filePath ?
|
|
12014
|
+
filePath: screen.filePath ? import_node_path5.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
|
|
10785
12015
|
})
|
|
10786
12016
|
);
|
|
10787
12017
|
const projectInfo = await this.getProjectInfo();
|
|
@@ -10805,11 +12035,11 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10805
12035
|
}
|
|
10806
12036
|
};
|
|
10807
12037
|
const serialized = JSON.stringify(document, null, 2);
|
|
10808
|
-
const checksum = this.calculateChecksum(
|
|
10809
|
-
const filePath =
|
|
12038
|
+
const checksum = this.calculateChecksum(serializeForChecksum(document));
|
|
12039
|
+
const filePath = import_node_path5.default.resolve(outputDir, `mcp-document.${this.options.format}`);
|
|
10810
12040
|
await (0, import_promises4.writeFile)(filePath, serialized, "utf-8");
|
|
10811
12041
|
console.log(`[MCPGenerator] Document written to: ${filePath}`);
|
|
10812
|
-
const checksumFilePath =
|
|
12042
|
+
const checksumFilePath = import_node_path5.default.resolve(outputDir, ".appilots-checksum");
|
|
10813
12043
|
await (0, import_promises4.writeFile)(checksumFilePath, checksum, "utf-8");
|
|
10814
12044
|
console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
|
|
10815
12045
|
console.log("[MCPGenerator] Generation complete!");
|
|
@@ -10817,7 +12047,8 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10817
12047
|
document,
|
|
10818
12048
|
filePath,
|
|
10819
12049
|
format: this.options.format,
|
|
10820
|
-
checksum
|
|
12050
|
+
checksum,
|
|
12051
|
+
...analyzed.diagnostics ? { diagnostics: analyzed.diagnostics } : {}
|
|
10821
12052
|
};
|
|
10822
12053
|
}
|
|
10823
12054
|
/**
|
|
@@ -10829,7 +12060,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10829
12060
|
* after calling `generate()`.
|
|
10830
12061
|
*/
|
|
10831
12062
|
static async readPreviousChecksum(outputDir) {
|
|
10832
|
-
const checksumFilePath =
|
|
12063
|
+
const checksumFilePath = import_node_path5.default.resolve(outputDir, ".appilots-checksum");
|
|
10833
12064
|
try {
|
|
10834
12065
|
const content = await (0, import_promises4.readFile)(checksumFilePath, "utf-8");
|
|
10835
12066
|
return content.trim() || null;
|
|
@@ -10848,7 +12079,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10848
12079
|
*/
|
|
10849
12080
|
async getProjectInfo() {
|
|
10850
12081
|
try {
|
|
10851
|
-
const packageJsonPath =
|
|
12082
|
+
const packageJsonPath = import_node_path5.default.resolve(this.analyzerConfig.rootDir, "package.json");
|
|
10852
12083
|
const packageJsonContent = await (0, import_promises4.readFile)(packageJsonPath, "utf-8");
|
|
10853
12084
|
const packageJson = JSON.parse(packageJsonContent);
|
|
10854
12085
|
return {
|
|
@@ -10923,8 +12154,8 @@ function reportMetadataLint(document, strict) {
|
|
|
10923
12154
|
}
|
|
10924
12155
|
|
|
10925
12156
|
// src/cli/utils/remote-drift.ts
|
|
10926
|
-
var
|
|
10927
|
-
var
|
|
12157
|
+
var import_node_fs3 = require("fs");
|
|
12158
|
+
var import_node_path6 = require("path");
|
|
10928
12159
|
var STUB_RATIO = 0.5;
|
|
10929
12160
|
function compareWithRemote(local, remote) {
|
|
10930
12161
|
if (remote.screensCount === local.screens) return null;
|
|
@@ -10937,7 +12168,7 @@ function compareWithRemote(local, remote) {
|
|
|
10937
12168
|
}
|
|
10938
12169
|
function readLocalDocumentSummary(outputDir) {
|
|
10939
12170
|
try {
|
|
10940
|
-
const raw = (0,
|
|
12171
|
+
const raw = (0, import_node_fs3.readFileSync)((0, import_node_path6.join)(outputDir, "mcp-document.json"), "utf-8");
|
|
10941
12172
|
const parsed = JSON.parse(raw);
|
|
10942
12173
|
if (typeof parsed?.metadata?.totalScreens !== "number") return null;
|
|
10943
12174
|
return { screens: parsed.metadata.totalScreens };
|
|
@@ -11022,14 +12253,18 @@ function assertDocumentHasScreens(document, options = {}) {
|
|
|
11022
12253
|
}
|
|
11023
12254
|
error("No screens found \u2014 this application map would give the agent nothing to navigate.");
|
|
11024
12255
|
info("");
|
|
11025
|
-
info("The analyzer
|
|
11026
|
-
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.");
|
|
11027
12259
|
info("");
|
|
11028
12260
|
info("The two usual causes:");
|
|
11029
|
-
info(" \u2022 The
|
|
11030
|
-
info("
|
|
11031
|
-
info("
|
|
11032
|
-
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.");
|
|
11033
12268
|
info("");
|
|
11034
12269
|
info("The escape hatch, which works for any router: declare your screens in");
|
|
11035
12270
|
info("an `appilots.manifest.json` at the project root. Its screens are merged");
|
|
@@ -11076,7 +12311,7 @@ function syncCommand() {
|
|
|
11076
12311
|
const outputDir = config.outputDir || ".appilots";
|
|
11077
12312
|
let previousChecksum = "";
|
|
11078
12313
|
try {
|
|
11079
|
-
const checksumPath = (0,
|
|
12314
|
+
const checksumPath = (0, import_node_path7.join)(outputDir, ".appilots-checksum");
|
|
11080
12315
|
previousChecksum = (await (0, import_promises5.readFile)(checksumPath, "utf-8")).trim();
|
|
11081
12316
|
} catch {
|
|
11082
12317
|
}
|
|
@@ -11203,7 +12438,7 @@ function syncCommand() {
|
|
|
11203
12438
|
|
|
11204
12439
|
// src/cli/commands/watch.ts
|
|
11205
12440
|
var import_commander3 = require("commander");
|
|
11206
|
-
var
|
|
12441
|
+
var import_node_fs4 = require("fs");
|
|
11207
12442
|
function watchCommand() {
|
|
11208
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) => {
|
|
11209
12444
|
try {
|
|
@@ -11250,7 +12485,7 @@ function watchCommand() {
|
|
|
11250
12485
|
dim("Press Ctrl+C to stop\n");
|
|
11251
12486
|
let debounceTimer = null;
|
|
11252
12487
|
const DEBOUNCE_MS = 500;
|
|
11253
|
-
const watcher = (0,
|
|
12488
|
+
const watcher = (0, import_node_fs4.watch)(process.cwd(), { recursive: true }, async (eventType, filename) => {
|
|
11254
12489
|
if (!filename) return;
|
|
11255
12490
|
if (filename.startsWith(".") || filename.includes("node_modules") || filename.includes(config.outputDir || ".appilots")) {
|
|
11256
12491
|
return;
|
|
@@ -11302,6 +12537,95 @@ function watchCommand() {
|
|
|
11302
12537
|
|
|
11303
12538
|
// src/cli/commands/generate.ts
|
|
11304
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
|
|
11305
12629
|
function generateCommand() {
|
|
11306
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(
|
|
11307
12631
|
"--strict-metadata",
|
|
@@ -11318,7 +12642,7 @@ function generateCommand() {
|
|
|
11318
12642
|
spinner.start();
|
|
11319
12643
|
let loaded = null;
|
|
11320
12644
|
try {
|
|
11321
|
-
loaded = loadConfig(warn);
|
|
12645
|
+
loaded = loadConfig(warn, { requireApiKey: false });
|
|
11322
12646
|
} catch {
|
|
11323
12647
|
loaded = null;
|
|
11324
12648
|
}
|
|
@@ -11326,11 +12650,21 @@ function generateCommand() {
|
|
|
11326
12650
|
rootDir: process.cwd(),
|
|
11327
12651
|
outputDir: options.output || ".appilots",
|
|
11328
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,
|
|
11329
12659
|
platform: loaded?.platform,
|
|
11330
12660
|
manifestPath: loaded?.manifestPath
|
|
11331
12661
|
});
|
|
11332
12662
|
const output = await generator.generate();
|
|
11333
12663
|
spinner.stop();
|
|
12664
|
+
reportReach({
|
|
12665
|
+
document: output.document,
|
|
12666
|
+
...output.diagnostics?.navigation ? { navigation: output.diagnostics.navigation } : {}
|
|
12667
|
+
});
|
|
11334
12668
|
assertDocumentHasScreens(output.document, {
|
|
11335
12669
|
rootDir: process.cwd(),
|
|
11336
12670
|
allowEmpty: Boolean(options.allowEmpty)
|
|
@@ -11350,19 +12684,18 @@ function generateCommand() {
|
|
|
11350
12684
|
|
|
11351
12685
|
// src/cli/commands/annotate.ts
|
|
11352
12686
|
var import_promises6 = require("fs/promises");
|
|
11353
|
-
var
|
|
12687
|
+
var import_node_path9 = __toESM(require("path"));
|
|
11354
12688
|
var import_commander5 = require("commander");
|
|
11355
|
-
var import_fast_glob6 = __toESM(require("fast-glob"));
|
|
11356
12689
|
|
|
11357
12690
|
// src/annotate/sites.ts
|
|
11358
12691
|
var import_traverse12 = __toESM(require("@babel/traverse"));
|
|
11359
|
-
var
|
|
12692
|
+
var t14 = __toESM(require("@babel/types"));
|
|
11360
12693
|
|
|
11361
12694
|
// src/annotate/forwarding.ts
|
|
11362
|
-
var
|
|
11363
|
-
var
|
|
12695
|
+
var import_node_path8 = __toESM(require("path"));
|
|
12696
|
+
var import_node_fs5 = require("fs");
|
|
11364
12697
|
var import_traverse11 = __toESM(require("@babel/traverse"));
|
|
11365
|
-
var
|
|
12698
|
+
var t13 = __toESM(require("@babel/types"));
|
|
11366
12699
|
var traverse11 = import_traverse11.default.default ?? import_traverse11.default;
|
|
11367
12700
|
var RN_TESTID_CARRIERS = /* @__PURE__ */ new Set([
|
|
11368
12701
|
"View",
|
|
@@ -11394,10 +12727,10 @@ function collectImports(ast) {
|
|
|
11394
12727
|
ImportDeclaration(nodePath) {
|
|
11395
12728
|
const source = nodePath.node.source.value;
|
|
11396
12729
|
for (const spec of nodePath.node.specifiers) {
|
|
11397
|
-
if (
|
|
11398
|
-
const imported =
|
|
12730
|
+
if (t13.isImportSpecifier(spec)) {
|
|
12731
|
+
const imported = t13.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
|
|
11399
12732
|
out.set(spec.local.name, { source, local: spec.local.name, imported });
|
|
11400
|
-
} else if (
|
|
12733
|
+
} else if (t13.isImportDefaultSpecifier(spec)) {
|
|
11401
12734
|
out.set(spec.local.name, { source, local: spec.local.name, imported: "default" });
|
|
11402
12735
|
}
|
|
11403
12736
|
}
|
|
@@ -11405,17 +12738,17 @@ function collectImports(ast) {
|
|
|
11405
12738
|
});
|
|
11406
12739
|
return out;
|
|
11407
12740
|
}
|
|
11408
|
-
var
|
|
12741
|
+
var EXTENSIONS2 = [".tsx", ".ts", ".jsx", ".js"];
|
|
11409
12742
|
function resolveRelativeImport(fromFile, source) {
|
|
11410
12743
|
if (!source.startsWith(".")) return void 0;
|
|
11411
|
-
const base =
|
|
11412
|
-
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) {
|
|
11413
12746
|
const candidate = `${base}${ext}`;
|
|
11414
|
-
if ((0,
|
|
12747
|
+
if ((0, import_node_fs5.existsSync)(candidate)) return candidate;
|
|
11415
12748
|
}
|
|
11416
|
-
for (const ext of
|
|
11417
|
-
const candidate =
|
|
11418
|
-
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;
|
|
11419
12752
|
}
|
|
11420
12753
|
return void 0;
|
|
11421
12754
|
}
|
|
@@ -11430,7 +12763,7 @@ function inspectModule(file2, exportName, depth = 0) {
|
|
|
11430
12763
|
if (depth > 2) return none;
|
|
11431
12764
|
let source;
|
|
11432
12765
|
try {
|
|
11433
|
-
source = (0,
|
|
12766
|
+
source = (0, import_node_fs5.readFileSync)(file2, "utf-8");
|
|
11434
12767
|
} catch {
|
|
11435
12768
|
return none;
|
|
11436
12769
|
}
|
|
@@ -11451,7 +12784,7 @@ function inspectModule(file2, exportName, depth = 0) {
|
|
|
11451
12784
|
const from = nodePath.node.source?.value;
|
|
11452
12785
|
if (!from) return;
|
|
11453
12786
|
const named = nodePath.node.specifiers.some(
|
|
11454
|
-
(spec) =>
|
|
12787
|
+
(spec) => t13.isExportSpecifier(spec) && (t13.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === exportName
|
|
11455
12788
|
);
|
|
11456
12789
|
if (named) barrelTarget = from;
|
|
11457
12790
|
},
|
|
@@ -11460,11 +12793,11 @@ function inspectModule(file2, exportName, depth = 0) {
|
|
|
11460
12793
|
},
|
|
11461
12794
|
// `interface CardProps { testID?: string }` / `type Props = { testID… }`
|
|
11462
12795
|
TSPropertySignature(nodePath) {
|
|
11463
|
-
if (
|
|
12796
|
+
if (t13.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
|
|
11464
12797
|
},
|
|
11465
12798
|
// `function Card({ testID }) {}` — the destructured parameter.
|
|
11466
12799
|
ObjectProperty(nodePath) {
|
|
11467
|
-
if (
|
|
12800
|
+
if (t13.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
|
|
11468
12801
|
},
|
|
11469
12802
|
// `<View {...props} />` — anything spread onto JSX carries it along.
|
|
11470
12803
|
JSXSpreadAttribute() {
|
|
@@ -11475,17 +12808,17 @@ function inspectModule(file2, exportName, depth = 0) {
|
|
|
11475
12808
|
// that is one hardcoded name shared by every instance, which is the
|
|
11476
12809
|
// duplicate-id problem rather than a solution to it.
|
|
11477
12810
|
JSXAttribute(nodePath) {
|
|
11478
|
-
const name =
|
|
12811
|
+
const name = t13.isJSXIdentifier(nodePath.node.name) ? nodePath.node.name.name : "";
|
|
11479
12812
|
if (name !== "testID" && name !== "accessibilityLabel") return;
|
|
11480
12813
|
const value = nodePath.node.value;
|
|
11481
|
-
if (!
|
|
11482
|
-
if (
|
|
12814
|
+
if (!t13.isJSXExpressionContainer(value)) return;
|
|
12815
|
+
if (t13.isStringLiteral(value.expression)) return;
|
|
11483
12816
|
derives = true;
|
|
11484
12817
|
},
|
|
11485
12818
|
// `useAppilotsTarget(`${groupId}-${option.value}`, …)`
|
|
11486
12819
|
CallExpression(nodePath) {
|
|
11487
12820
|
const callee = nodePath.node.callee;
|
|
11488
|
-
if (
|
|
12821
|
+
if (t13.isIdentifier(callee) && IDENTITY_HOOKS.has(callee.name)) derives = true;
|
|
11489
12822
|
}
|
|
11490
12823
|
});
|
|
11491
12824
|
if (forwards || derives) return { forwards, derives };
|
|
@@ -11570,18 +12903,18 @@ function hasLabelText(element) {
|
|
|
11570
12903
|
function namedHandler(element, prop) {
|
|
11571
12904
|
if (!prop) return void 0;
|
|
11572
12905
|
const attr = findJsxAttribute(element, prop);
|
|
11573
|
-
if (!attr || !
|
|
11574
|
-
return
|
|
12906
|
+
if (!attr || !t14.isJSXExpressionContainer(attr.value)) return void 0;
|
|
12907
|
+
return t14.isIdentifier(attr.value.expression) ? attr.value.expression.name : void 0;
|
|
11575
12908
|
}
|
|
11576
12909
|
var ANNOTATABLE_ROLES = /* @__PURE__ */ new Set(["button", "input", "select", "toggle", "date", "list"]);
|
|
11577
12910
|
function isPerItemRender(nodePath) {
|
|
11578
12911
|
let current = nodePath.parentPath;
|
|
11579
12912
|
while (current) {
|
|
11580
12913
|
const node = current.node;
|
|
11581
|
-
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")) {
|
|
11582
12915
|
return true;
|
|
11583
12916
|
}
|
|
11584
|
-
if (
|
|
12917
|
+
if (t14.isJSXAttribute(node) && t14.isJSXIdentifier(node.name) && /^render[A-Z]|^ListHeaderComponent$|^ListFooterComponent$/.test(node.name.name) && node.name.name !== "renderScrollComponent") {
|
|
11585
12918
|
return /^renderItem$|^renderSectionHeader$|^renderSectionFooter$/.test(node.name.name);
|
|
11586
12919
|
}
|
|
11587
12920
|
current = current.parentPath;
|
|
@@ -11611,7 +12944,7 @@ function findAnnotationSites(source, file2) {
|
|
|
11611
12944
|
traverse12(ast, {
|
|
11612
12945
|
JSXOpeningElement(nodePath) {
|
|
11613
12946
|
const element = nodePath.node;
|
|
11614
|
-
if (!
|
|
12947
|
+
if (!t14.isJSXIdentifier(element.name)) return;
|
|
11615
12948
|
const component = element.name.name;
|
|
11616
12949
|
const role = classifyJsxComponent(component, element);
|
|
11617
12950
|
if (!ANNOTATABLE_ROLES.has(role)) return;
|
|
@@ -11693,10 +13026,10 @@ function applyToFile(source, sites) {
|
|
|
11693
13026
|
}
|
|
11694
13027
|
|
|
11695
13028
|
// src/annotate/forward.ts
|
|
11696
|
-
var
|
|
13029
|
+
var import_node_fs6 = require("fs");
|
|
11697
13030
|
var import_magic_string2 = __toESM(require("magic-string"));
|
|
11698
13031
|
var import_traverse13 = __toESM(require("@babel/traverse"));
|
|
11699
|
-
var
|
|
13032
|
+
var t15 = __toESM(require("@babel/types"));
|
|
11700
13033
|
var traverse13 = import_traverse13.default.default ?? import_traverse13.default;
|
|
11701
13034
|
function findComponent(ast, name) {
|
|
11702
13035
|
let found;
|
|
@@ -11707,9 +13040,9 @@ function findComponent(ast, name) {
|
|
|
11707
13040
|
}
|
|
11708
13041
|
},
|
|
11709
13042
|
VariableDeclarator(nodePath) {
|
|
11710
|
-
if (!
|
|
13043
|
+
if (!t15.isIdentifier(nodePath.node.id) || nodePath.node.id.name !== name) return;
|
|
11711
13044
|
const init = nodePath.node.init;
|
|
11712
|
-
if (
|
|
13045
|
+
if (t15.isArrowFunctionExpression(init) || t15.isFunctionExpression(init)) {
|
|
11713
13046
|
found ??= { params: init.params, body: init.body };
|
|
11714
13047
|
}
|
|
11715
13048
|
}
|
|
@@ -11721,26 +13054,26 @@ function findReturnedRoots(body) {
|
|
|
11721
13054
|
let unsupported = false;
|
|
11722
13055
|
const record = (node) => {
|
|
11723
13056
|
if (!node) return;
|
|
11724
|
-
if (
|
|
11725
|
-
if (
|
|
13057
|
+
if (t15.isParenthesizedExpression(node)) return record(node.expression);
|
|
13058
|
+
if (t15.isJSXElement(node)) {
|
|
11726
13059
|
roots.push(node.openingElement);
|
|
11727
13060
|
return;
|
|
11728
13061
|
}
|
|
11729
|
-
if (
|
|
13062
|
+
if (t15.isConditionalExpression(node)) {
|
|
11730
13063
|
record(node.consequent);
|
|
11731
13064
|
record(node.alternate);
|
|
11732
13065
|
return;
|
|
11733
13066
|
}
|
|
11734
|
-
if (
|
|
13067
|
+
if (t15.isJSXFragment(node)) {
|
|
11735
13068
|
unsupported = true;
|
|
11736
13069
|
return;
|
|
11737
13070
|
}
|
|
11738
|
-
if (
|
|
13071
|
+
if (t15.isNullLiteral(node)) return;
|
|
11739
13072
|
unsupported = true;
|
|
11740
13073
|
};
|
|
11741
|
-
if (
|
|
13074
|
+
if (t15.isBlockStatement(body)) {
|
|
11742
13075
|
traverse13(
|
|
11743
|
-
|
|
13076
|
+
t15.file(t15.program([t15.expressionStatement(t15.functionExpression(null, [], body))])),
|
|
11744
13077
|
{
|
|
11745
13078
|
ReturnStatement(nodePath) {
|
|
11746
13079
|
record(nodePath.node.argument);
|
|
@@ -11754,7 +13087,7 @@ function findReturnedRoots(body) {
|
|
|
11754
13087
|
return roots;
|
|
11755
13088
|
}
|
|
11756
13089
|
function isCarrierRoot(element, imports) {
|
|
11757
|
-
if (!
|
|
13090
|
+
if (!t15.isJSXIdentifier(element.name)) return false;
|
|
11758
13091
|
const name = element.name.name;
|
|
11759
13092
|
const origin = imports.get(name);
|
|
11760
13093
|
if (origin?.source !== "react-native") return false;
|
|
@@ -11762,7 +13095,7 @@ function isCarrierRoot(element, imports) {
|
|
|
11762
13095
|
}
|
|
11763
13096
|
function hasProp(element, name) {
|
|
11764
13097
|
return element.attributes.some(
|
|
11765
|
-
(attr) =>
|
|
13098
|
+
(attr) => t15.isJSXAttribute(attr) && t15.isJSXIdentifier(attr.name) && attr.name.name === name
|
|
11766
13099
|
);
|
|
11767
13100
|
}
|
|
11768
13101
|
function indentOf(source, offset) {
|
|
@@ -11793,7 +13126,7 @@ function followBarrel(file2, ast, component) {
|
|
|
11793
13126
|
const from = nodePath.node.source?.value;
|
|
11794
13127
|
if (!from) return;
|
|
11795
13128
|
const named = nodePath.node.specifiers.some(
|
|
11796
|
-
(spec) =>
|
|
13129
|
+
(spec) => t15.isExportSpecifier(spec) && (t15.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === component
|
|
11797
13130
|
);
|
|
11798
13131
|
if (named) target ??= from;
|
|
11799
13132
|
}
|
|
@@ -11804,7 +13137,7 @@ function planForward(file2, component, depth = 0) {
|
|
|
11804
13137
|
const base = { file: file2, component, edits: [], roots: 0 };
|
|
11805
13138
|
let source;
|
|
11806
13139
|
try {
|
|
11807
|
-
source = (0,
|
|
13140
|
+
source = (0, import_node_fs6.readFileSync)(file2, "utf-8");
|
|
11808
13141
|
} catch {
|
|
11809
13142
|
return { ...base, refusal: `cannot read ${file2}` };
|
|
11810
13143
|
}
|
|
@@ -11821,7 +13154,7 @@ function planForward(file2, component, depth = 0) {
|
|
|
11821
13154
|
return { ...base, refusal: `no function named ${component} in this file` };
|
|
11822
13155
|
}
|
|
11823
13156
|
const param = found.params[0];
|
|
11824
|
-
if (!
|
|
13157
|
+
if (!t15.isObjectPattern(param)) {
|
|
11825
13158
|
return {
|
|
11826
13159
|
...base,
|
|
11827
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`
|
|
@@ -11837,7 +13170,7 @@ function planForward(file2, component, depth = 0) {
|
|
|
11837
13170
|
const imports = collectImports(ast);
|
|
11838
13171
|
const nonCarrier = roots.find((root) => !isCarrierRoot(root, imports));
|
|
11839
13172
|
if (nonCarrier) {
|
|
11840
|
-
const name =
|
|
13173
|
+
const name = t15.isJSXIdentifier(nonCarrier.name) ? nonCarrier.name.name : "its root";
|
|
11841
13174
|
return {
|
|
11842
13175
|
...base,
|
|
11843
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`
|
|
@@ -11845,21 +13178,21 @@ function planForward(file2, component, depth = 0) {
|
|
|
11845
13178
|
}
|
|
11846
13179
|
const edits = [];
|
|
11847
13180
|
const alreadyDestructured = param.properties.some(
|
|
11848
|
-
(prop) =>
|
|
13181
|
+
(prop) => t15.isObjectProperty(prop) && t15.isIdentifier(prop.key) && prop.key.name === "testID"
|
|
11849
13182
|
);
|
|
11850
13183
|
if (!alreadyDestructured) {
|
|
11851
13184
|
const insert = insertAfterLastMember(source, param.properties, "testID", param.end ?? 0, ",");
|
|
11852
13185
|
edits.push({ ...insert, what: "parameter" });
|
|
11853
13186
|
}
|
|
11854
13187
|
const annotation = param.typeAnnotation;
|
|
11855
|
-
if (
|
|
13188
|
+
if (t15.isTSTypeAnnotation(annotation)) {
|
|
11856
13189
|
const declared = annotation.typeAnnotation;
|
|
11857
13190
|
let body;
|
|
11858
13191
|
let closing = 0;
|
|
11859
|
-
if (
|
|
13192
|
+
if (t15.isTSTypeLiteral(declared)) {
|
|
11860
13193
|
body = declared.members;
|
|
11861
13194
|
closing = declared.end ?? 0;
|
|
11862
|
-
} else if (
|
|
13195
|
+
} else if (t15.isTSTypeReference(declared) && t15.isIdentifier(declared.typeName)) {
|
|
11863
13196
|
const typeName = declared.typeName.name;
|
|
11864
13197
|
traverse13(ast, {
|
|
11865
13198
|
TSInterfaceDeclaration(nodePath) {
|
|
@@ -11868,7 +13201,7 @@ function planForward(file2, component, depth = 0) {
|
|
|
11868
13201
|
closing = nodePath.node.body.end ?? 0;
|
|
11869
13202
|
},
|
|
11870
13203
|
TSTypeAliasDeclaration(nodePath) {
|
|
11871
|
-
if (nodePath.node.id.name !== typeName || !
|
|
13204
|
+
if (nodePath.node.id.name !== typeName || !t15.isTSTypeLiteral(nodePath.node.typeAnnotation))
|
|
11872
13205
|
return;
|
|
11873
13206
|
body = nodePath.node.typeAnnotation.members;
|
|
11874
13207
|
closing = nodePath.node.typeAnnotation.end ?? 0;
|
|
@@ -11883,7 +13216,7 @@ function planForward(file2, component, depth = 0) {
|
|
|
11883
13216
|
}
|
|
11884
13217
|
if (body) {
|
|
11885
13218
|
const declaresTestId = body.some(
|
|
11886
|
-
(member) =>
|
|
13219
|
+
(member) => t15.isTSPropertySignature(member) && t15.isIdentifier(member.key) && member.key.name === "testID"
|
|
11887
13220
|
);
|
|
11888
13221
|
if (!declaresTestId) {
|
|
11889
13222
|
const insert = insertAfterLastMember(source, body, "testID?: string;", closing, ";");
|
|
@@ -11940,7 +13273,7 @@ function annotateCommand() {
|
|
|
11940
13273
|
const spinner = createSpinner("Scanning for unaddressable controls...");
|
|
11941
13274
|
spinner.start();
|
|
11942
13275
|
resetForwardingCache();
|
|
11943
|
-
const files = await (
|
|
13276
|
+
const files = await globSorted(include, { cwd: rootDir, ignore: exclude, absolute: true });
|
|
11944
13277
|
const scan = async () => {
|
|
11945
13278
|
const out = [];
|
|
11946
13279
|
for (const file2 of files) {
|
|
@@ -11989,9 +13322,9 @@ function annotateCommand() {
|
|
|
11989
13322
|
written: options.write === true,
|
|
11990
13323
|
annotated: written.length,
|
|
11991
13324
|
blocked: blocked.length,
|
|
11992
|
-
files: touched.map((edit) =>
|
|
13325
|
+
files: touched.map((edit) => import_node_path9.default.relative(rootDir, edit.file)),
|
|
11993
13326
|
sites: [...written, ...blocked].map((site) => ({
|
|
11994
|
-
file:
|
|
13327
|
+
file: import_node_path9.default.relative(rootDir, site.file),
|
|
11995
13328
|
line: site.line,
|
|
11996
13329
|
component: site.component,
|
|
11997
13330
|
role: site.role,
|
|
@@ -12034,9 +13367,12 @@ async function planForwards(edits, rootDir) {
|
|
|
12034
13367
|
void rootDir;
|
|
12035
13368
|
return plans;
|
|
12036
13369
|
}
|
|
13370
|
+
function escapeRegExp(value) {
|
|
13371
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13372
|
+
}
|
|
12037
13373
|
function findImportSource(source, name) {
|
|
12038
13374
|
const pattern = new RegExp(
|
|
12039
|
-
String.raw`import\s*\{[^}]*\b${name}\b[^}]*\}\s*from\s*['"]([^'"]+)['"]`
|
|
13375
|
+
String.raw`import\s*\{[^}]*\b${escapeRegExp(name)}\b[^}]*\}\s*from\s*['"]([^'"]+)['"]`
|
|
12040
13376
|
);
|
|
12041
13377
|
return pattern.exec(source)?.[1];
|
|
12042
13378
|
}
|
|
@@ -12045,7 +13381,7 @@ function reportSites(rootDir, touched, blocked, didWrite, forwards) {
|
|
|
12045
13381
|
if (written.length > 0) {
|
|
12046
13382
|
heading(didWrite ? "Added" : "Would add");
|
|
12047
13383
|
for (const edit of touched) {
|
|
12048
|
-
info(
|
|
13384
|
+
info(import_node_path9.default.relative(rootDir, edit.file));
|
|
12049
13385
|
for (const site of edit.written) {
|
|
12050
13386
|
dim(` ${String(site.line).padStart(4)} <${site.component}> testID="${site.testId}"`);
|
|
12051
13387
|
}
|
|
@@ -12057,7 +13393,7 @@ function reportSites(rootDir, touched, blocked, didWrite, forwards) {
|
|
|
12057
13393
|
if (teachable.size > 0) {
|
|
12058
13394
|
heading(didWrite ? "Taught to carry testID" : "Would teach to carry testID");
|
|
12059
13395
|
for (const plan of teachable.values()) {
|
|
12060
|
-
info(`<${plan.component}> ${
|
|
13396
|
+
info(`<${plan.component}> ${import_node_path9.default.relative(rootDir, plan.file)}`);
|
|
12061
13397
|
for (const edit of plan.edits) {
|
|
12062
13398
|
dim(` ${edit.what}: ${edit.text.trim()}`);
|
|
12063
13399
|
}
|
|
@@ -12365,10 +13701,10 @@ function matchActions(expected, actual, reply = "") {
|
|
|
12365
13701
|
var import_fs9 = require("fs");
|
|
12366
13702
|
var import_path8 = require("path");
|
|
12367
13703
|
var EMPTY_BASELINE = { generatedAt: null, gitSha: null, perScenario: {} };
|
|
12368
|
-
function loadBaseline(
|
|
12369
|
-
if (!(0, import_fs9.existsSync)(
|
|
13704
|
+
function loadBaseline(path13) {
|
|
13705
|
+
if (!(0, import_fs9.existsSync)(path13)) return { ...EMPTY_BASELINE, perScenario: {} };
|
|
12370
13706
|
try {
|
|
12371
|
-
const raw = JSON.parse((0, import_fs9.readFileSync)(
|
|
13707
|
+
const raw = JSON.parse((0, import_fs9.readFileSync)(path13, "utf-8"));
|
|
12372
13708
|
return {
|
|
12373
13709
|
generatedAt: raw.generatedAt ?? null,
|
|
12374
13710
|
gitSha: raw.gitSha ?? null,
|
|
@@ -12378,9 +13714,9 @@ function loadBaseline(path11) {
|
|
|
12378
13714
|
return { ...EMPTY_BASELINE, perScenario: {} };
|
|
12379
13715
|
}
|
|
12380
13716
|
}
|
|
12381
|
-
function saveBaseline(
|
|
12382
|
-
(0, import_fs9.mkdirSync)((0, import_path8.dirname)(
|
|
12383
|
-
(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)}
|
|
12384
13720
|
`, "utf-8");
|
|
12385
13721
|
}
|
|
12386
13722
|
function buildBaseline(outcomes, meta) {
|
|
@@ -12408,11 +13744,11 @@ function diffAgainstBaseline(outcomes, baseline, maxTokenRegression) {
|
|
|
12408
13744
|
continue;
|
|
12409
13745
|
}
|
|
12410
13746
|
if (outcome.status === "passed" && prior.status === "passed" && prior.totalTokens > 0 && outcome.totalTokens > prior.totalTokens * (1 + maxTokenRegression)) {
|
|
12411
|
-
const
|
|
13747
|
+
const pct2 = Math.round((outcome.totalTokens - prior.totalTokens) / prior.totalTokens * 100);
|
|
12412
13748
|
regressions.push({
|
|
12413
13749
|
name: outcome.name,
|
|
12414
13750
|
kind: "token-regression",
|
|
12415
|
-
detail: `tokens ${prior.totalTokens} \u2192 ${outcome.totalTokens} (+${
|
|
13751
|
+
detail: `tokens ${prior.totalTokens} \u2192 ${outcome.totalTokens} (+${pct2}%, threshold +${Math.round(maxTokenRegression * 100)}%)`
|
|
12416
13752
|
});
|
|
12417
13753
|
}
|
|
12418
13754
|
}
|