@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/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var fs = require('fs/promises');
|
|
4
|
-
var
|
|
4
|
+
var path2 = require('path');
|
|
5
5
|
var traverse4 = require('@babel/traverse');
|
|
6
6
|
var BabelTypes = require('@babel/types');
|
|
7
|
-
var
|
|
7
|
+
var fastGlob = require('fast-glob');
|
|
8
8
|
var parser = require('@babel/parser');
|
|
9
9
|
var fs$1 = require('fs');
|
|
10
10
|
var crypto = require('crypto');
|
|
@@ -30,10 +30,10 @@ function _interopNamespace(e) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
33
|
-
var
|
|
33
|
+
var path2__namespace = /*#__PURE__*/_interopNamespace(path2);
|
|
34
34
|
var traverse4__default = /*#__PURE__*/_interopDefault(traverse4);
|
|
35
35
|
var BabelTypes__namespace = /*#__PURE__*/_interopNamespace(BabelTypes);
|
|
36
|
-
var
|
|
36
|
+
var fastGlob__default = /*#__PURE__*/_interopDefault(fastGlob);
|
|
37
37
|
var parser__namespace = /*#__PURE__*/_interopNamespace(parser);
|
|
38
38
|
|
|
39
39
|
var __defProp = Object.defineProperty;
|
|
@@ -41,6 +41,13 @@ var __export = (target, all) => {
|
|
|
41
41
|
for (var name in all)
|
|
42
42
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
43
43
|
};
|
|
44
|
+
function byCodeUnit(a, b) {
|
|
45
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
46
|
+
}
|
|
47
|
+
async function globSorted(patterns, options) {
|
|
48
|
+
const files = await fastGlob__default.default(patterns, options);
|
|
49
|
+
return files.sort(byCodeUnit);
|
|
50
|
+
}
|
|
44
51
|
var DEFAULT_PARSER_PLUGINS = [
|
|
45
52
|
"jsx",
|
|
46
53
|
"typescript",
|
|
@@ -425,14 +432,25 @@ var ScreenAnalyzer = class {
|
|
|
425
432
|
strictScreens;
|
|
426
433
|
/** Glob patterns that identify screen files in strict mode */
|
|
427
434
|
screenPatterns;
|
|
435
|
+
/**
|
|
436
|
+
* Arquivos que uma ROTA monta (`component={…}` resolvido pelo
|
|
437
|
+
* `NavigationAnalyzer`). Passam pelo filtro estrito sem depender de
|
|
438
|
+
* convenção de nome, porque um componente que uma rota monta É uma tela por
|
|
439
|
+
* definição — não é heurística, é o que o app declarou.
|
|
440
|
+
*
|
|
441
|
+
* É isto que resolve o caso `rocketchat`, cujas telas se chamam `*View.tsx`
|
|
442
|
+
* em `app/views/`, e o `coopcycle`, que não tem diretório `screens/` nenhum.
|
|
443
|
+
*/
|
|
444
|
+
routeTargetFiles;
|
|
428
445
|
/** §D: Count of screens filtered out in strict mode (available after analyze()) */
|
|
429
446
|
screensFilteredOut = 0;
|
|
430
447
|
constructor(config, options) {
|
|
431
448
|
this.config = config;
|
|
449
|
+
this.routeTargetFiles = options?.routeTargetFiles ?? /* @__PURE__ */ new Set();
|
|
432
450
|
this.strictScreens = options?.strictScreens ?? false;
|
|
433
451
|
this.screenPatterns = options?.screenPatterns ?? [
|
|
434
|
-
"**/*Screen.{ts,tsx}",
|
|
435
|
-
"**/screens/**/*.{ts,tsx}"
|
|
452
|
+
"**/*Screen.{ts,tsx,js,jsx}",
|
|
453
|
+
"**/screens/**/*.{ts,tsx,js,jsx}"
|
|
436
454
|
];
|
|
437
455
|
}
|
|
438
456
|
/** Analyze all screens in the project */
|
|
@@ -444,7 +462,7 @@ var ScreenAnalyzer = class {
|
|
|
444
462
|
console.log(`[ScreenAnalyzer] Strict mode ON \u2014 screen patterns:`, this.screenPatterns);
|
|
445
463
|
}
|
|
446
464
|
}
|
|
447
|
-
const files = await
|
|
465
|
+
const files = await globSorted(include, {
|
|
448
466
|
cwd: this.config.rootDir,
|
|
449
467
|
ignore: exclude
|
|
450
468
|
});
|
|
@@ -453,23 +471,24 @@ var ScreenAnalyzer = class {
|
|
|
453
471
|
}
|
|
454
472
|
let screenPatternFiles = null;
|
|
455
473
|
if (this.strictScreens) {
|
|
456
|
-
const matched = await
|
|
474
|
+
const matched = await globSorted(this.screenPatterns, {
|
|
457
475
|
cwd: this.config.rootDir,
|
|
458
476
|
ignore: exclude
|
|
459
477
|
});
|
|
460
|
-
screenPatternFiles = new Set(matched.map((f) =>
|
|
478
|
+
screenPatternFiles = new Set(matched.map((f) => path2__namespace.default.resolve(this.config.rootDir, f)));
|
|
461
479
|
}
|
|
462
480
|
const screens = [];
|
|
463
481
|
this.screensFilteredOut = 0;
|
|
464
482
|
for (const file of files) {
|
|
465
|
-
const filePath =
|
|
483
|
+
const filePath = path2__namespace.default.resolve(this.config.rootDir, file);
|
|
466
484
|
try {
|
|
467
485
|
const descriptor = await this.analyzeFile(filePath);
|
|
468
486
|
if (!descriptor) continue;
|
|
469
487
|
if (this.strictScreens) {
|
|
470
488
|
const hasRegisterScreen = descriptor.__hasRegisterScreen === true;
|
|
471
489
|
const matchesPattern = screenPatternFiles?.has(filePath) ?? false;
|
|
472
|
-
|
|
490
|
+
const isRouteTarget = this.routeTargetFiles.has(filePath);
|
|
491
|
+
if (!hasRegisterScreen && !matchesPattern && !isRouteTarget) {
|
|
473
492
|
this.screensFilteredOut++;
|
|
474
493
|
if (this.verbose) {
|
|
475
494
|
console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file}`);
|
|
@@ -677,6 +696,8 @@ var ScreenAnalyzer = class {
|
|
|
677
696
|
action.riskLevel = value.value;
|
|
678
697
|
} else if (key === "nativeConfirmationExpected" && BabelTypes__namespace.isBooleanLiteral(value)) {
|
|
679
698
|
action.nativeConfirmationExpected = value.value;
|
|
699
|
+
} else if (key === "asyncBudgetMs" && BabelTypes__namespace.isNumericLiteral(value) && Number.isInteger(value.value) && value.value > 0) {
|
|
700
|
+
action.asyncBudgetMs = value.value;
|
|
680
701
|
} else if (key === "appilotsInferred" && BabelTypes__namespace.isObjectExpression(value)) {
|
|
681
702
|
action.appilotsInferred = this.parseAppilotsInferredObject(value);
|
|
682
703
|
}
|
|
@@ -1656,43 +1677,871 @@ var ScreenAnalyzer = class {
|
|
|
1656
1677
|
* E.g., /src/screens/ItemListScreen.tsx -> ItemListScreen
|
|
1657
1678
|
*/
|
|
1658
1679
|
extractScreenName(filePath) {
|
|
1659
|
-
const basename2 =
|
|
1680
|
+
const basename2 = path2__namespace.default.basename(filePath);
|
|
1660
1681
|
return basename2.replace(/\.(tsx?|jsx?)$/, "");
|
|
1661
1682
|
}
|
|
1662
1683
|
};
|
|
1684
|
+
var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
|
|
1685
|
+
var MAX_HOPS = 8;
|
|
1686
|
+
var ModuleGraph = class {
|
|
1687
|
+
asts = /* @__PURE__ */ new Map();
|
|
1688
|
+
resolved = /* @__PURE__ */ new Map();
|
|
1689
|
+
/** `@src/*` → `<root>/src/*`, lido do tsconfig do app. */
|
|
1690
|
+
aliases;
|
|
1691
|
+
/** `uniswap` → `<repo>/packages/uniswap`, lido do workspace do monorepo. */
|
|
1692
|
+
workspacePackages;
|
|
1693
|
+
constructor(rootDir) {
|
|
1694
|
+
this.aliases = rootDir ? readTsconfigAliases(rootDir) : [];
|
|
1695
|
+
this.workspacePackages = rootDir ? readWorkspacePackages(rootDir) : [];
|
|
1696
|
+
}
|
|
1697
|
+
/** AST de um arquivo, memoizada. `null` quando não parseia. */
|
|
1698
|
+
parse(file) {
|
|
1699
|
+
const cached = this.asts.get(file);
|
|
1700
|
+
if (cached !== void 0) return cached;
|
|
1701
|
+
let ast = null;
|
|
1702
|
+
try {
|
|
1703
|
+
ast = parser__namespace.parse(fs$1.readFileSync(file, "utf-8"), {
|
|
1704
|
+
sourceType: "module",
|
|
1705
|
+
plugins: ["jsx", "typescript"]
|
|
1706
|
+
});
|
|
1707
|
+
} catch {
|
|
1708
|
+
ast = null;
|
|
1709
|
+
}
|
|
1710
|
+
this.asts.set(file, ast);
|
|
1711
|
+
return ast;
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* `./account/Home` a partir de `src/navigation/index.tsx` → caminho absoluto.
|
|
1715
|
+
* Só resolve caminho relativo: import de pacote (`@react-navigation/native`)
|
|
1716
|
+
* é de terceiro e não tem tela nossa dentro.
|
|
1717
|
+
*/
|
|
1718
|
+
resolve(fromFile, spec) {
|
|
1719
|
+
const key = `${fromFile} ${spec}`;
|
|
1720
|
+
const cached = this.resolved.get(key);
|
|
1721
|
+
if (cached !== void 0) return cached;
|
|
1722
|
+
let base = null;
|
|
1723
|
+
if (spec.startsWith(".")) {
|
|
1724
|
+
base = path2__namespace.default.resolve(path2__namespace.default.dirname(fromFile), spec);
|
|
1725
|
+
} else {
|
|
1726
|
+
for (const { prefix, target } of this.aliases) {
|
|
1727
|
+
if (spec === prefix || spec.startsWith(prefix + "/")) {
|
|
1728
|
+
base = path2__namespace.default.join(target, spec.slice(prefix.length));
|
|
1729
|
+
break;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
if (!base) {
|
|
1733
|
+
for (const pkg of this.workspacePackages) {
|
|
1734
|
+
if (spec === pkg.name || spec.startsWith(pkg.name + "/")) {
|
|
1735
|
+
base = path2__namespace.default.join(pkg.dir, spec.slice(pkg.name.length));
|
|
1736
|
+
break;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
if (!base) {
|
|
1742
|
+
this.resolved.set(key, null);
|
|
1743
|
+
return null;
|
|
1744
|
+
}
|
|
1745
|
+
const candidates = [
|
|
1746
|
+
base,
|
|
1747
|
+
...EXTENSIONS.map((e) => base + e),
|
|
1748
|
+
...EXTENSIONS.map((e) => path2__namespace.default.join(base, "index" + e))
|
|
1749
|
+
];
|
|
1750
|
+
let found = null;
|
|
1751
|
+
for (const c of candidates) {
|
|
1752
|
+
try {
|
|
1753
|
+
if (fs$1.existsSync(c) && fs$1.statSync(c).isFile()) {
|
|
1754
|
+
found = c;
|
|
1755
|
+
break;
|
|
1756
|
+
}
|
|
1757
|
+
} catch {
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
this.resolved.set(key, found);
|
|
1761
|
+
return found;
|
|
1762
|
+
}
|
|
1763
|
+
/** `import`s do arquivo, por nome local. */
|
|
1764
|
+
imports(file) {
|
|
1765
|
+
const out = /* @__PURE__ */ new Map();
|
|
1766
|
+
const ast = this.parse(file);
|
|
1767
|
+
if (!ast) return out;
|
|
1768
|
+
for (const stmt of ast.program.body) {
|
|
1769
|
+
if (!BabelTypes__namespace.isImportDeclaration(stmt)) continue;
|
|
1770
|
+
const source = stmt.source.value;
|
|
1771
|
+
for (const spec of stmt.specifiers) {
|
|
1772
|
+
if (BabelTypes__namespace.isImportDefaultSpecifier(spec)) {
|
|
1773
|
+
out.set(spec.local.name, { source, imported: "default" });
|
|
1774
|
+
} else if (BabelTypes__namespace.isImportNamespaceSpecifier(spec)) {
|
|
1775
|
+
out.set(spec.local.name, { source, imported: "*" });
|
|
1776
|
+
} else if (BabelTypes__namespace.isImportSpecifier(spec)) {
|
|
1777
|
+
const imported = BabelTypes__namespace.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
|
|
1778
|
+
out.set(spec.local.name, { source, imported });
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
return out;
|
|
1783
|
+
}
|
|
1784
|
+
/** `const X = <init>` no topo do arquivo, incluindo `export const`. */
|
|
1785
|
+
topLevelInit(file, name) {
|
|
1786
|
+
const ast = this.parse(file);
|
|
1787
|
+
if (!ast) return null;
|
|
1788
|
+
for (const stmt of ast.program.body) {
|
|
1789
|
+
const decl = BabelTypes__namespace.isExportNamedDeclaration(stmt) ? stmt.declaration : stmt;
|
|
1790
|
+
if (!BabelTypes__namespace.isVariableDeclaration(decl)) continue;
|
|
1791
|
+
for (const d of decl.declarations) {
|
|
1792
|
+
if (BabelTypes__namespace.isIdentifier(d.id) && d.id.name === name && d.init) {
|
|
1793
|
+
return BabelTypes__namespace.isTSAsExpression(d.init) ? d.init.expression : d.init;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return null;
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Onde `name` é DEFINIDO — segue import e reexport de barrel.
|
|
1801
|
+
* Devolve o arquivo e o nome sob o qual ele é definido lá.
|
|
1802
|
+
*/
|
|
1803
|
+
resolveBinding(file, name, hops = 0) {
|
|
1804
|
+
if (hops > MAX_HOPS) return null;
|
|
1805
|
+
if (this.topLevelInit(file, name) !== null) return { file, name };
|
|
1806
|
+
const binding = this.imports(file).get(name);
|
|
1807
|
+
if (binding) {
|
|
1808
|
+
const target = this.resolve(file, binding.source);
|
|
1809
|
+
if (!target) return null;
|
|
1810
|
+
const next = binding.imported === "default" || binding.imported === "*" ? name : binding.imported;
|
|
1811
|
+
const deeper = this.resolveBinding(target, next, hops + 1);
|
|
1812
|
+
return deeper ?? { file: target, name: next };
|
|
1813
|
+
}
|
|
1814
|
+
const ast = this.parse(file);
|
|
1815
|
+
if (ast) {
|
|
1816
|
+
for (const stmt of ast.program.body) {
|
|
1817
|
+
if (!BabelTypes__namespace.isExportNamedDeclaration(stmt) || !stmt.source) continue;
|
|
1818
|
+
for (const spec of stmt.specifiers) {
|
|
1819
|
+
if (!BabelTypes__namespace.isExportSpecifier(spec)) continue;
|
|
1820
|
+
const exported = BabelTypes__namespace.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value;
|
|
1821
|
+
if (exported !== name) continue;
|
|
1822
|
+
const target = this.resolve(file, stmt.source.value);
|
|
1823
|
+
if (!target) return null;
|
|
1824
|
+
const local = spec.local.name;
|
|
1825
|
+
return this.resolveBinding(target, local, hops + 1) ?? { file: target, name: local };
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
if (ast) {
|
|
1830
|
+
for (const stmt of ast.program.body) {
|
|
1831
|
+
if (!BabelTypes__namespace.isExportAllDeclaration(stmt)) continue;
|
|
1832
|
+
const target = this.resolve(file, stmt.source.value);
|
|
1833
|
+
if (!target || target === file) continue;
|
|
1834
|
+
const deeper = this.resolveBinding(target, name, hops + 1);
|
|
1835
|
+
if (deeper) return deeper;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
if (ast) {
|
|
1839
|
+
for (const stmt of ast.program.body) {
|
|
1840
|
+
if (!BabelTypes__namespace.isExportDefaultDeclaration(stmt)) continue;
|
|
1841
|
+
if (BabelTypes__namespace.isIdentifier(stmt.declaration)) {
|
|
1842
|
+
const local = stmt.declaration.name;
|
|
1843
|
+
if (local === name) return null;
|
|
1844
|
+
return this.resolveBinding(file, local, hops + 1) ?? { file, name: local };
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
return null;
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* O valor string de uma expressão de nome de rota, ou `null`.
|
|
1852
|
+
*
|
|
1853
|
+
* Cobre `"Chat"`, `ROUTES.CHAT`, `ROUTES.ONBOARDING.SPLASH` e `SOME_CONST` —
|
|
1854
|
+
* seguindo import quando o objeto vem de outro arquivo. NÃO cobre template
|
|
1855
|
+
* com interpolação nem valor calculado, de propósito.
|
|
1856
|
+
*/
|
|
1857
|
+
stringConstant(file, node, hops = 0) {
|
|
1858
|
+
if (!node || hops > MAX_HOPS) return null;
|
|
1859
|
+
if (BabelTypes__namespace.isStringLiteral(node)) return node.value;
|
|
1860
|
+
if (BabelTypes__namespace.isTemplateLiteral(node)) {
|
|
1861
|
+
return node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? null : null;
|
|
1862
|
+
}
|
|
1863
|
+
if (BabelTypes__namespace.isTSAsExpression(node)) return this.stringConstant(file, node.expression, hops + 1);
|
|
1864
|
+
const chain = memberChain(node);
|
|
1865
|
+
if (!chain) return null;
|
|
1866
|
+
const origin = this.resolveBinding(file, chain.root);
|
|
1867
|
+
if (!origin) return null;
|
|
1868
|
+
let current = this.topLevelInit(origin.file, origin.name);
|
|
1869
|
+
if (!current) return null;
|
|
1870
|
+
for (const key of chain.path) {
|
|
1871
|
+
if (!BabelTypes__namespace.isObjectExpression(current)) return null;
|
|
1872
|
+
const prop = objectProperty(current, key);
|
|
1873
|
+
if (!prop) return null;
|
|
1874
|
+
current = BabelTypes__namespace.isTSAsExpression(prop) ? prop.expression : prop;
|
|
1875
|
+
}
|
|
1876
|
+
return BabelTypes__namespace.isStringLiteral(current) ? current.value : null;
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* O ARQUIVO onde vive o componente de uma rota, ou `null`.
|
|
1880
|
+
*
|
|
1881
|
+
* Aceita as três formas que o corpus mostrou: identificador
|
|
1882
|
+
* (`component={Home}`), membro de barrel (`component={screens.AccountHome}`)
|
|
1883
|
+
* e componente embrulhado em HOC (`component={gestureHandlerRootHOC(Chat)}`,
|
|
1884
|
+
* que é como o `pocketpal` monta todas as telas do Drawer).
|
|
1885
|
+
*/
|
|
1886
|
+
componentFile(file, node, hops = 0) {
|
|
1887
|
+
if (!node || hops > MAX_HOPS) return null;
|
|
1888
|
+
if (BabelTypes__namespace.isCallExpression(node)) {
|
|
1889
|
+
for (const arg of node.arguments) {
|
|
1890
|
+
if (BabelTypes__namespace.isIdentifier(arg) || BabelTypes__namespace.isMemberExpression(arg)) {
|
|
1891
|
+
const inner = this.componentFile(file, arg, hops + 1);
|
|
1892
|
+
if (inner) return inner;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
return null;
|
|
1896
|
+
}
|
|
1897
|
+
if (BabelTypes__namespace.isTSAsExpression(node)) return this.componentFile(file, node.expression, hops + 1);
|
|
1898
|
+
const chain = memberChain(node);
|
|
1899
|
+
if (!chain) return null;
|
|
1900
|
+
const origin = this.resolveBinding(file, chain.root);
|
|
1901
|
+
if (!origin) return null;
|
|
1902
|
+
if (chain.path.length === 0) return origin.file;
|
|
1903
|
+
const init = this.topLevelInit(origin.file, origin.name);
|
|
1904
|
+
if (init && BabelTypes__namespace.isObjectExpression(init)) {
|
|
1905
|
+
const prop = objectProperty(init, chain.path[0]);
|
|
1906
|
+
if (prop) return this.componentFile(origin.file, prop, hops + 1);
|
|
1907
|
+
}
|
|
1908
|
+
const viaExport = this.resolveBinding(origin.file, chain.path[0], hops + 1);
|
|
1909
|
+
return viaExport?.file ?? null;
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Os arquivos DO PROJETO que este arquivo renderiza como JSX.
|
|
1913
|
+
*
|
|
1914
|
+
* `<ChatView …/>` em `ChatScreen.tsx` → `components/ChatView/ChatView.tsx`.
|
|
1915
|
+
* Import de pacote devolve `null` no `resolve` e fica de fora: componente de
|
|
1916
|
+
* terceiro não tem tela nossa dentro.
|
|
1917
|
+
*
|
|
1918
|
+
* Ignora `<X.Screen>` e `<X.Navigator>` de propósito — navegação é outro
|
|
1919
|
+
* extractor, e um navegador não é conteúdo de tela.
|
|
1920
|
+
*/
|
|
1921
|
+
renderedComponentFiles(file) {
|
|
1922
|
+
const ast = this.parse(file);
|
|
1923
|
+
if (!ast) return [];
|
|
1924
|
+
const names = /* @__PURE__ */ new Set();
|
|
1925
|
+
const visit = (node) => {
|
|
1926
|
+
if (!node || typeof node !== "object") return;
|
|
1927
|
+
if (BabelTypes__namespace.isJSXOpeningElement(node) && BabelTypes__namespace.isJSXIdentifier(node.name)) {
|
|
1928
|
+
const n = node.name.name;
|
|
1929
|
+
if (/^[A-Z]/.test(n)) names.add(n);
|
|
1930
|
+
}
|
|
1931
|
+
for (const key of Object.keys(node)) {
|
|
1932
|
+
const value = node[key];
|
|
1933
|
+
if (Array.isArray(value)) {
|
|
1934
|
+
for (const item of value) visit(item);
|
|
1935
|
+
} else if (value && typeof value === "object" && "type" in value) {
|
|
1936
|
+
visit(value);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
};
|
|
1940
|
+
visit(ast.program);
|
|
1941
|
+
const out = /* @__PURE__ */ new Set();
|
|
1942
|
+
for (const name of names) {
|
|
1943
|
+
const origin = this.resolveBinding(file, name);
|
|
1944
|
+
if (origin && origin.file !== file) out.add(origin.file);
|
|
1945
|
+
}
|
|
1946
|
+
return [...out];
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1949
|
+
function memberChain(node) {
|
|
1950
|
+
const chain = [];
|
|
1951
|
+
let current = node;
|
|
1952
|
+
while (BabelTypes__namespace.isMemberExpression(current)) {
|
|
1953
|
+
if (current.computed) {
|
|
1954
|
+
if (!BabelTypes__namespace.isStringLiteral(current.property)) return null;
|
|
1955
|
+
chain.unshift(current.property.value);
|
|
1956
|
+
} else if (BabelTypes__namespace.isIdentifier(current.property)) {
|
|
1957
|
+
chain.unshift(current.property.name);
|
|
1958
|
+
} else {
|
|
1959
|
+
return null;
|
|
1960
|
+
}
|
|
1961
|
+
current = current.object;
|
|
1962
|
+
}
|
|
1963
|
+
return BabelTypes__namespace.isIdentifier(current) ? { root: current.name, path: chain } : null;
|
|
1964
|
+
}
|
|
1965
|
+
function objectProperty(obj, key) {
|
|
1966
|
+
for (const prop of obj.properties) {
|
|
1967
|
+
if (!BabelTypes__namespace.isObjectProperty(prop)) continue;
|
|
1968
|
+
const name = BabelTypes__namespace.isIdentifier(prop.key) ? prop.key.name : BabelTypes__namespace.isStringLiteral(prop.key) ? prop.key.value : null;
|
|
1969
|
+
if (name === key && BabelTypes__namespace.isExpression(prop.value)) return prop.value;
|
|
1970
|
+
}
|
|
1971
|
+
return null;
|
|
1972
|
+
}
|
|
1973
|
+
function readTsconfigAliases(rootDir) {
|
|
1974
|
+
const file = path2__namespace.default.join(rootDir, "tsconfig.json");
|
|
1975
|
+
if (!fs$1.existsSync(file)) return [];
|
|
1976
|
+
let parsed;
|
|
1977
|
+
try {
|
|
1978
|
+
const raw = stripJsonComments(fs$1.readFileSync(file, "utf-8")).replace(/,(\s*[}\]])/g, "$1");
|
|
1979
|
+
parsed = JSON.parse(raw);
|
|
1980
|
+
} catch {
|
|
1981
|
+
return [];
|
|
1982
|
+
}
|
|
1983
|
+
const paths = parsed.compilerOptions?.paths;
|
|
1984
|
+
if (!paths) return [];
|
|
1985
|
+
const baseUrl = path2__namespace.default.resolve(rootDir, parsed.compilerOptions?.baseUrl ?? ".");
|
|
1986
|
+
const out = [];
|
|
1987
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
1988
|
+
const first = targets[0];
|
|
1989
|
+
if (typeof first !== "string") continue;
|
|
1990
|
+
out.push({
|
|
1991
|
+
prefix: pattern.replace(/\/?\*$/, ""),
|
|
1992
|
+
target: path2__namespace.default.resolve(baseUrl, first.replace(/\/?\*$/, ""))
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
return out.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
1996
|
+
}
|
|
1997
|
+
function stripJsonComments(text) {
|
|
1998
|
+
let out = "";
|
|
1999
|
+
let inString = false;
|
|
2000
|
+
let escaped = false;
|
|
2001
|
+
for (let i = 0; i < text.length; i++) {
|
|
2002
|
+
const c = text[i];
|
|
2003
|
+
if (inString) {
|
|
2004
|
+
out += c;
|
|
2005
|
+
if (escaped) escaped = false;
|
|
2006
|
+
else if (c === "\\") escaped = true;
|
|
2007
|
+
else if (c === '"') inString = false;
|
|
2008
|
+
continue;
|
|
2009
|
+
}
|
|
2010
|
+
if (c === '"') {
|
|
2011
|
+
inString = true;
|
|
2012
|
+
out += c;
|
|
2013
|
+
continue;
|
|
2014
|
+
}
|
|
2015
|
+
if (c === "/" && text[i + 1] === "/") {
|
|
2016
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
2017
|
+
out += "\n";
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
2020
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
2021
|
+
i += 2;
|
|
2022
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
2023
|
+
i++;
|
|
2024
|
+
continue;
|
|
2025
|
+
}
|
|
2026
|
+
out += c;
|
|
2027
|
+
}
|
|
2028
|
+
return out;
|
|
2029
|
+
}
|
|
2030
|
+
function readWorkspacePackages(rootDir) {
|
|
2031
|
+
const start = path2__namespace.default.resolve(rootDir);
|
|
2032
|
+
let dir = start;
|
|
2033
|
+
for (let i = 0; i < 6; i++) {
|
|
2034
|
+
const globs = workspaceGlobs(dir);
|
|
2035
|
+
if (globs.length > 0) {
|
|
2036
|
+
const packages = expandWorkspaceGlobs(dir, globs);
|
|
2037
|
+
const containsApp = packages.some(
|
|
2038
|
+
(p) => start === p.dir || start.startsWith(p.dir + path2__namespace.default.sep)
|
|
2039
|
+
);
|
|
2040
|
+
return containsApp ? packages : [];
|
|
2041
|
+
}
|
|
2042
|
+
const parent = path2__namespace.default.dirname(dir);
|
|
2043
|
+
if (parent === dir) break;
|
|
2044
|
+
dir = parent;
|
|
2045
|
+
}
|
|
2046
|
+
return [];
|
|
2047
|
+
}
|
|
2048
|
+
function workspaceGlobs(dir) {
|
|
2049
|
+
const pnpm = path2__namespace.default.join(dir, "pnpm-workspace.yaml");
|
|
2050
|
+
if (fs$1.existsSync(pnpm)) {
|
|
2051
|
+
try {
|
|
2052
|
+
const lines = fs$1.readFileSync(pnpm, "utf-8").split(/\r?\n/);
|
|
2053
|
+
const out = [];
|
|
2054
|
+
let inPackages = false;
|
|
2055
|
+
for (const line of lines) {
|
|
2056
|
+
if (/^packages:/.test(line)) {
|
|
2057
|
+
inPackages = true;
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
2060
|
+
if (inPackages) {
|
|
2061
|
+
const m = /^\s*-\s*["']?([^"'#]+?)["']?\s*$/.exec(line);
|
|
2062
|
+
if (m) out.push(m[1].trim());
|
|
2063
|
+
else if (/^\S/.test(line)) break;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
if (out.length) return out;
|
|
2067
|
+
} catch {
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
const pkgPath = path2__namespace.default.join(dir, "package.json");
|
|
2071
|
+
if (!fs$1.existsSync(pkgPath)) return [];
|
|
2072
|
+
try {
|
|
2073
|
+
const pkg = JSON.parse(fs$1.readFileSync(pkgPath, "utf-8"));
|
|
2074
|
+
const ws = pkg.workspaces;
|
|
2075
|
+
if (Array.isArray(ws)) return ws;
|
|
2076
|
+
if (ws && Array.isArray(ws.packages)) return ws.packages;
|
|
2077
|
+
} catch {
|
|
2078
|
+
}
|
|
2079
|
+
return [];
|
|
2080
|
+
}
|
|
2081
|
+
function expandWorkspaceGlobs(root, globs) {
|
|
2082
|
+
const out = [];
|
|
2083
|
+
const add = (dir) => {
|
|
2084
|
+
const pkgPath = path2__namespace.default.join(dir, "package.json");
|
|
2085
|
+
if (!fs$1.existsSync(pkgPath)) return;
|
|
2086
|
+
try {
|
|
2087
|
+
const name = JSON.parse(fs$1.readFileSync(pkgPath, "utf-8")).name;
|
|
2088
|
+
if (name) out.push({ name, dir });
|
|
2089
|
+
} catch {
|
|
2090
|
+
}
|
|
2091
|
+
};
|
|
2092
|
+
for (const glob of globs) {
|
|
2093
|
+
if (glob.endsWith("/*")) {
|
|
2094
|
+
const parent = path2__namespace.default.join(root, glob.slice(0, -2));
|
|
2095
|
+
let entries = [];
|
|
2096
|
+
try {
|
|
2097
|
+
entries = fs$1.readdirSync(parent);
|
|
2098
|
+
} catch {
|
|
2099
|
+
continue;
|
|
2100
|
+
}
|
|
2101
|
+
for (const entry of entries) {
|
|
2102
|
+
const dir = path2__namespace.default.join(parent, entry);
|
|
2103
|
+
try {
|
|
2104
|
+
if (fs$1.statSync(dir).isDirectory()) add(dir);
|
|
2105
|
+
} catch {
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
} else if (!glob.includes("*")) {
|
|
2109
|
+
add(path2__namespace.default.join(root, glob));
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
return out.sort((a, b) => b.name.length - a.name.length);
|
|
2113
|
+
}
|
|
2114
|
+
var ROUTE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
2115
|
+
var PLATFORM_SUFFIXES = [".ios", ".android", ".native", ".web"];
|
|
2116
|
+
var ROUTE_DIR_CANDIDATES = ["app", "src/app"];
|
|
2117
|
+
function baseName(file) {
|
|
2118
|
+
let name = file;
|
|
2119
|
+
for (const ext of ROUTE_EXTENSIONS) {
|
|
2120
|
+
if (name.endsWith(ext)) {
|
|
2121
|
+
name = name.slice(0, -ext.length);
|
|
2122
|
+
break;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
for (const suffix of PLATFORM_SUFFIXES) {
|
|
2126
|
+
if (name.endsWith(suffix)) return name.slice(0, -suffix.length);
|
|
2127
|
+
}
|
|
2128
|
+
return name;
|
|
2129
|
+
}
|
|
2130
|
+
function isRouteFile(file) {
|
|
2131
|
+
return ROUTE_EXTENSIONS.some((ext) => file.endsWith(ext));
|
|
2132
|
+
}
|
|
2133
|
+
function isSpecial(base) {
|
|
2134
|
+
if (base === "_layout") return true;
|
|
2135
|
+
if (base.startsWith("+")) return base !== "+not-found";
|
|
2136
|
+
return base.startsWith("_");
|
|
2137
|
+
}
|
|
2138
|
+
function layoutType(file) {
|
|
2139
|
+
try {
|
|
2140
|
+
const src = fs$1.readFileSync(file, "utf-8");
|
|
2141
|
+
if (/<(?:Native)?Tabs\b/.test(src)) return "tab";
|
|
2142
|
+
if (/<Drawer\b/.test(src)) return "drawer";
|
|
2143
|
+
} catch {
|
|
2144
|
+
}
|
|
2145
|
+
return "stack";
|
|
2146
|
+
}
|
|
2147
|
+
function findLayout(dir) {
|
|
2148
|
+
for (const ext of ROUTE_EXTENSIONS) {
|
|
2149
|
+
const candidate = path2__namespace.default.join(dir, "_layout" + ext);
|
|
2150
|
+
if (fs$1.existsSync(candidate)) return candidate;
|
|
2151
|
+
}
|
|
2152
|
+
return null;
|
|
2153
|
+
}
|
|
2154
|
+
function analyzeExpoRouter(appRoot) {
|
|
2155
|
+
let routeDir = null;
|
|
2156
|
+
for (const candidate of ROUTE_DIR_CANDIDATES) {
|
|
2157
|
+
const full = path2__namespace.default.join(appRoot, candidate);
|
|
2158
|
+
if (fs$1.existsSync(full) && findLayout(full)) {
|
|
2159
|
+
routeDir = full;
|
|
2160
|
+
break;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
if (!routeDir) return null;
|
|
2164
|
+
const routes = [];
|
|
2165
|
+
const navigators = /* @__PURE__ */ new Map();
|
|
2166
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2167
|
+
const walk = (dir, segments, navigator) => {
|
|
2168
|
+
const layout = findLayout(dir);
|
|
2169
|
+
const current = layout ? segments.join("/") || "/" : navigator;
|
|
2170
|
+
if (layout && !navigators.has(current)) navigators.set(current, layoutType(layout));
|
|
2171
|
+
let entries;
|
|
2172
|
+
try {
|
|
2173
|
+
entries = fs$1.readdirSync(dir);
|
|
2174
|
+
} catch {
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
for (const entry of entries.sort()) {
|
|
2178
|
+
const full = path2__namespace.default.join(dir, entry);
|
|
2179
|
+
let isDir = false;
|
|
2180
|
+
try {
|
|
2181
|
+
isDir = fs$1.statSync(full).isDirectory();
|
|
2182
|
+
} catch {
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
if (isDir) {
|
|
2186
|
+
if (entry === "node_modules" || entry.startsWith(".")) continue;
|
|
2187
|
+
walk(full, [...segments, entry], current);
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
if (!isRouteFile(entry)) continue;
|
|
2191
|
+
const base = baseName(entry);
|
|
2192
|
+
if (isSpecial(base)) continue;
|
|
2193
|
+
const routeSegments = base === "index" ? segments : [...segments, base];
|
|
2194
|
+
const name = "/" + routeSegments.join("/");
|
|
2195
|
+
if (seen.has(name)) continue;
|
|
2196
|
+
seen.add(name);
|
|
2197
|
+
routes.push({
|
|
2198
|
+
name,
|
|
2199
|
+
componentFile: full,
|
|
2200
|
+
navigatorName: current,
|
|
2201
|
+
navigatorType: navigators.get(current) ?? "stack",
|
|
2202
|
+
params: routeSegments.filter((s) => s.startsWith("[") && s.endsWith("]")).map((s) => s.slice(1, -1).replace(/^\.\.\./, ""))
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
};
|
|
2206
|
+
walk(routeDir, [], "/");
|
|
2207
|
+
return {
|
|
2208
|
+
routeDir: path2__namespace.default.relative(appRoot, routeDir),
|
|
2209
|
+
routes,
|
|
2210
|
+
navigators: [...navigators.entries()].map(([name, type]) => ({ name, type }))
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
// src/analyzers/NavigationAnalyzer.ts
|
|
2215
|
+
var NAVIGATOR_FACTORIES = {
|
|
2216
|
+
createStackNavigator: "stack",
|
|
2217
|
+
createNativeStackNavigator: "stack",
|
|
2218
|
+
createBottomTabNavigator: "tab",
|
|
2219
|
+
createTabNavigator: "tab",
|
|
2220
|
+
createMaterialTopTabNavigator: "tab",
|
|
2221
|
+
createMaterialBottomTabNavigator: "tab",
|
|
2222
|
+
createDrawerNavigator: "drawer"
|
|
2223
|
+
};
|
|
2224
|
+
var CUSTOM_FACTORY = /^create[A-Za-z0-9]*(Navigator|Stack|Tabs?|Drawer)$/;
|
|
2225
|
+
function unwrapStaticScreen(value) {
|
|
2226
|
+
if (BabelTypes__namespace.isCallExpression(value)) {
|
|
2227
|
+
const arg = value.arguments[0];
|
|
2228
|
+
return arg ? unwrapStaticScreen(arg) : null;
|
|
2229
|
+
}
|
|
2230
|
+
if (BabelTypes__namespace.isObjectExpression(value)) {
|
|
2231
|
+
for (const prop of value.properties) {
|
|
2232
|
+
if (!BabelTypes__namespace.isObjectProperty(prop)) continue;
|
|
2233
|
+
const key = BabelTypes__namespace.isIdentifier(prop.key) ? prop.key.name : null;
|
|
2234
|
+
if (key === "screen" && BabelTypes__namespace.isExpression(prop.value)) return prop.value;
|
|
2235
|
+
}
|
|
2236
|
+
return null;
|
|
2237
|
+
}
|
|
2238
|
+
return BabelTypes__namespace.isExpression(value) ? value : null;
|
|
2239
|
+
}
|
|
2240
|
+
function inferNavigatorType(factoryName) {
|
|
2241
|
+
if (/drawer/i.test(factoryName)) return "drawer";
|
|
2242
|
+
if (/tab/i.test(factoryName)) return "tab";
|
|
2243
|
+
return "stack";
|
|
2244
|
+
}
|
|
2245
|
+
var NAVIGATION_EVIDENCE = new RegExp(
|
|
2246
|
+
// `\.Navigator`/`\.Screen` entram porque o JSX pode estar num arquivo que
|
|
2247
|
+
// não menciona fábrica nenhuma: `comapeo` declara `RootStack` em
|
|
2248
|
+
// `Stack/RootStack.ts` e escreve todas as 120 rotas em `Stack/index.tsx`,
|
|
2249
|
+
// que não cita `createNativeStackNavigator` uma vez sequer. Sem isto a
|
|
2250
|
+
// junção entre arquivos nunca chega a ser tentada.
|
|
2251
|
+
// Sem `\\b` depois da palavra — `createNativeStackNavigatorWithAuth` não tem
|
|
2252
|
+
// fronteira ali, e exigi-la fecharia o portão para o arquivo que declara os
|
|
2253
|
+
// seis navegadores do `bluesky`.
|
|
2254
|
+
`create[A-Za-z0-9]*(?:Navigator|Stack|Tabs?|Drawer)|ParamList|\\.Navigator\\b|\\.Screen\\b`
|
|
2255
|
+
);
|
|
1663
2256
|
var NavigationAnalyzer = class {
|
|
1664
2257
|
config;
|
|
1665
2258
|
/** Extra glob patterns for navigation file discovery (added to defaults) */
|
|
1666
2259
|
navigationInclude;
|
|
1667
2260
|
/** Extra glob patterns to exclude from navigation analysis */
|
|
1668
2261
|
navigationExclude;
|
|
2262
|
+
/** Exposto para a composição reusar o cache de AST em vez de reparsear a árvore. */
|
|
2263
|
+
graph;
|
|
2264
|
+
/**
|
|
2265
|
+
* Rota → arquivo do componente que ela monta. É a única ligação entre o grafo
|
|
2266
|
+
* de navegação e a árvore de telas; sem ela as duas metades do documento
|
|
2267
|
+
* falam de coisas diferentes. Populada por `analyze()`.
|
|
2268
|
+
*/
|
|
2269
|
+
routeTargets = /* @__PURE__ */ new Map();
|
|
2270
|
+
/**
|
|
2271
|
+
* Arquivos que DECLARAM uma fábrica de navegador.
|
|
2272
|
+
*
|
|
2273
|
+
* Um navegador aninhado é montado como `component=` de uma rota — no
|
|
2274
|
+
* `apps/example-app`, `<Stack.Screen name="Main" component={MainTabNavigator} />`.
|
|
2275
|
+
* Promover esse arquivo a tela criaria seis "telas" sem uma única ação, que é
|
|
2276
|
+
* exatamente o falso positivo que o corpus existe para não repetir. Um
|
|
2277
|
+
* navegador é um contêiner de rotas; a tela está um nível abaixo.
|
|
2278
|
+
*
|
|
2279
|
+
* Entram aqui os dois lados: o arquivo que CHAMA a fábrica e o arquivo que
|
|
2280
|
+
* RENDERIZA `<X.Navigator>`. Não são o mesmo — `bluewallet` declara
|
|
2281
|
+
* `DetailViewStack` num arquivo e escreve o JSX em
|
|
2282
|
+
* `navigation/DetailViewScreensStack.tsx`, e checar só o primeiro deixava o
|
|
2283
|
+
* segundo entrar como tela.
|
|
2284
|
+
*/
|
|
2285
|
+
navigatorFiles = /* @__PURE__ */ new Set();
|
|
2286
|
+
/** Preenchido por `analyze()`. Vazio antes disso. */
|
|
2287
|
+
diagnostics = {
|
|
2288
|
+
filesScanned: 0,
|
|
2289
|
+
filesWithEvidence: 0,
|
|
2290
|
+
navigatorsDeclared: 0,
|
|
2291
|
+
navigatorsWithJsx: 0,
|
|
2292
|
+
routes: 0,
|
|
2293
|
+
routesLinkedToFile: 0
|
|
2294
|
+
};
|
|
1669
2295
|
constructor(config, options) {
|
|
1670
2296
|
this.config = config;
|
|
2297
|
+
this.graph = new ModuleGraph(config.rootDir);
|
|
1671
2298
|
this.navigationInclude = options?.navigationInclude ?? [];
|
|
1672
2299
|
this.navigationExclude = options?.navigationExclude ?? [];
|
|
1673
2300
|
}
|
|
1674
2301
|
/** Build the full navigation graph */
|
|
1675
2302
|
async analyze() {
|
|
1676
2303
|
const navigationFiles = await this.findNavigationFiles();
|
|
1677
|
-
const parsedNavigators = [];
|
|
1678
2304
|
const typeExports = [];
|
|
2305
|
+
this.routeTargets.clear();
|
|
2306
|
+
const declarations = /* @__PURE__ */ new Map();
|
|
2307
|
+
const evidenceFiles = [];
|
|
2308
|
+
this.navigatorFiles.clear();
|
|
1679
2309
|
for (const filePath of navigationFiles) {
|
|
1680
2310
|
try {
|
|
1681
2311
|
const content = await fs$1.promises.readFile(filePath, "utf-8");
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
2312
|
+
if (!NAVIGATION_EVIDENCE.test(content)) continue;
|
|
2313
|
+
evidenceFiles.push(filePath);
|
|
2314
|
+
for (const decl of this.collectDeclarations(filePath)) {
|
|
2315
|
+
declarations.set(`${decl.file}#${decl.name}`, decl);
|
|
2316
|
+
if (decl.confident) this.navigatorFiles.add(decl.file);
|
|
2317
|
+
}
|
|
2318
|
+
typeExports.push(...this.parseParamTypes(content, filePath));
|
|
1686
2319
|
} catch (error) {
|
|
1687
2320
|
console.warn(`Failed to parse ${filePath}:`, error);
|
|
1688
2321
|
}
|
|
1689
2322
|
}
|
|
2323
|
+
const parsedNavigators = [];
|
|
2324
|
+
for (const filePath of evidenceFiles) {
|
|
2325
|
+
try {
|
|
2326
|
+
parsedNavigators.push(...this.parseNavigatorUsages(filePath, declarations));
|
|
2327
|
+
} catch (error) {
|
|
2328
|
+
console.warn(`Failed to parse navigators in ${filePath}:`, error);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
let detachedScreens = 0;
|
|
2332
|
+
for (const filePath of evidenceFiles) {
|
|
2333
|
+
try {
|
|
2334
|
+
for (const detached of this.parseDetachedScreens(filePath, declarations)) {
|
|
2335
|
+
const existing = parsedNavigators.find(
|
|
2336
|
+
(nav) => nav.name === detached.name && nav.type === detached.type
|
|
2337
|
+
);
|
|
2338
|
+
if (!existing) {
|
|
2339
|
+
parsedNavigators.push(detached);
|
|
2340
|
+
detachedScreens += detached.screens.length;
|
|
2341
|
+
continue;
|
|
2342
|
+
}
|
|
2343
|
+
const seen = new Set(existing.screens.map((screen) => screen.name));
|
|
2344
|
+
for (const screen of detached.screens) {
|
|
2345
|
+
if (seen.has(screen.name)) continue;
|
|
2346
|
+
seen.add(screen.name);
|
|
2347
|
+
existing.screens.push(screen);
|
|
2348
|
+
detachedScreens++;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
} catch (error) {
|
|
2352
|
+
console.warn(`Failed to parse detached screens in ${filePath}:`, error);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
const expo = analyzeExpoRouter(this.config.rootDir);
|
|
2356
|
+
if (expo) {
|
|
2357
|
+
console.log(
|
|
2358
|
+
`[NavigationAnalyzer] expo-router em "${expo.routeDir}": ${expo.routes.length} rotas, ${expo.navigators.length} layouts`
|
|
2359
|
+
);
|
|
2360
|
+
this.diagnostics.fileBasedRouter = {
|
|
2361
|
+
kind: "expo-router",
|
|
2362
|
+
routeDir: expo.routeDir,
|
|
2363
|
+
routes: expo.routes.length
|
|
2364
|
+
};
|
|
2365
|
+
const byNavigator = /* @__PURE__ */ new Map();
|
|
2366
|
+
for (const nav of expo.navigators) {
|
|
2367
|
+
byNavigator.set(nav.name, { name: nav.name, type: nav.type, screens: [] });
|
|
2368
|
+
}
|
|
2369
|
+
for (const route of expo.routes) {
|
|
2370
|
+
const navigator = byNavigator.get(route.navigatorName) ?? byNavigator.set(route.navigatorName, {
|
|
2371
|
+
name: route.navigatorName,
|
|
2372
|
+
type: route.navigatorType,
|
|
2373
|
+
screens: []
|
|
2374
|
+
}).get(route.navigatorName);
|
|
2375
|
+
navigator.screens.push({
|
|
2376
|
+
name: route.name,
|
|
2377
|
+
navigatorName: route.navigatorName,
|
|
2378
|
+
navigatorType: route.navigatorType,
|
|
2379
|
+
componentFile: route.componentFile,
|
|
2380
|
+
...route.params.length ? {
|
|
2381
|
+
params: route.params.map((name) => ({ name, type: "string", required: true }))
|
|
2382
|
+
} : {}
|
|
2383
|
+
});
|
|
2384
|
+
this.routeTargets.set(route.name, route.componentFile);
|
|
2385
|
+
}
|
|
2386
|
+
parsedNavigators.push(...byNavigator.values());
|
|
2387
|
+
}
|
|
2388
|
+
let staticOnly = 0;
|
|
2389
|
+
for (const decl of declarations.values()) {
|
|
2390
|
+
if (!decl.confident || !decl.staticScreens?.length) continue;
|
|
2391
|
+
const existing = parsedNavigators.find((n) => n.name === decl.name && n.type === decl.type);
|
|
2392
|
+
if (existing) {
|
|
2393
|
+
const seen = new Set(existing.screens.map((s) => s.name));
|
|
2394
|
+
for (const screen of decl.staticScreens) {
|
|
2395
|
+
if (!seen.has(screen.name)) existing.screens.push(screen);
|
|
2396
|
+
}
|
|
2397
|
+
continue;
|
|
2398
|
+
}
|
|
2399
|
+
parsedNavigators.push({ name: decl.name, type: decl.type, screens: decl.staticScreens });
|
|
2400
|
+
staticOnly++;
|
|
2401
|
+
}
|
|
2402
|
+
const confidentDeclarations = [...declarations.values()].filter((d) => d.confident).length;
|
|
2403
|
+
const uniqueRoutes = new Set(
|
|
2404
|
+
parsedNavigators.flatMap((nav) => nav.screens.map((screen) => screen.name))
|
|
2405
|
+
);
|
|
2406
|
+
this.diagnostics = {
|
|
2407
|
+
...this.diagnostics,
|
|
2408
|
+
filesScanned: navigationFiles.length,
|
|
2409
|
+
filesWithEvidence: evidenceFiles.length,
|
|
2410
|
+
navigatorsDeclared: confidentDeclarations,
|
|
2411
|
+
navigatorsWithJsx: parsedNavigators.length,
|
|
2412
|
+
routes: uniqueRoutes.size,
|
|
2413
|
+
// `routeTargets` é indexado por nome de rota, e uma rota pode ter sido
|
|
2414
|
+
// registrada por um navegador que não sobreviveu. O mínimo evita a
|
|
2415
|
+
// cobertura acima de 100% que a medição do corpus expôs no `coopcycle`.
|
|
2416
|
+
routesLinkedToFile: Math.min(this.routeTargets.size, uniqueRoutes.size)
|
|
2417
|
+
};
|
|
2418
|
+
console.log(
|
|
2419
|
+
`[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`
|
|
2420
|
+
);
|
|
1690
2421
|
this.attachParamsToNavigators(parsedNavigators, typeExports);
|
|
1691
2422
|
return this.buildNavigationGraph(parsedNavigators);
|
|
1692
2423
|
}
|
|
1693
|
-
/**
|
|
2424
|
+
/** Fase 1: as fábricas `create*Navigator()` atribuídas a um nome neste arquivo. */
|
|
2425
|
+
collectDeclarations(filePath) {
|
|
2426
|
+
const ast = this.graph.parse(filePath);
|
|
2427
|
+
if (!ast) return [];
|
|
2428
|
+
const out = /* @__PURE__ */ new Map();
|
|
2429
|
+
traverse4__default.default(ast, {
|
|
2430
|
+
CallExpression: (nodePath) => {
|
|
2431
|
+
const callee = nodePath.node.callee;
|
|
2432
|
+
if (!BabelTypes__namespace.isIdentifier(callee)) return;
|
|
2433
|
+
const known = NAVIGATOR_FACTORIES[callee.name] ?? (CUSTOM_FACTORY.test(callee.name) ? inferNavigatorType(callee.name) : null);
|
|
2434
|
+
const type = known ?? inferNavigatorType(callee.name);
|
|
2435
|
+
const confident = known !== null;
|
|
2436
|
+
let up = nodePath.parentPath;
|
|
2437
|
+
for (let i = 0; i < 4 && up; i++) {
|
|
2438
|
+
if (BabelTypes__namespace.isVariableDeclarator(up.node)) break;
|
|
2439
|
+
if (!BabelTypes__namespace.isMemberExpression(up.node) && !BabelTypes__namespace.isCallExpression(up.node)) {
|
|
2440
|
+
up = null;
|
|
2441
|
+
break;
|
|
2442
|
+
}
|
|
2443
|
+
up = up.parentPath;
|
|
2444
|
+
}
|
|
2445
|
+
const declarator = up?.node;
|
|
2446
|
+
if (!declarator || !BabelTypes__namespace.isVariableDeclarator(declarator) || !BabelTypes__namespace.isIdentifier(declarator.id)) {
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
const name = declarator.id.name;
|
|
2450
|
+
if (out.get(name)?.confident) return;
|
|
2451
|
+
const staticScreens = confident ? this.parseStaticScreens(filePath, nodePath.node.arguments[0], name, type) : [];
|
|
2452
|
+
out.set(name, {
|
|
2453
|
+
file: filePath,
|
|
2454
|
+
name,
|
|
2455
|
+
type,
|
|
2456
|
+
confident,
|
|
2457
|
+
...staticScreens.length ? { staticScreens } : {}
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
});
|
|
2461
|
+
return [...out.values()];
|
|
2462
|
+
}
|
|
2463
|
+
/**
|
|
2464
|
+
* As rotas de `createXNavigator({ screens: { … } })`.
|
|
2465
|
+
*
|
|
2466
|
+
* Quatro formas no corpus, todas em `rocketchat`:
|
|
2467
|
+
* `NewServerView` — shorthand, nome = componente
|
|
2468
|
+
* `LoginView: createNativeStackScreen({ screen })` — embrulho da própria lib
|
|
2469
|
+
* `SelectListView: SelectListViewScreen` — identificador direto
|
|
2470
|
+
* `ChatsStackNavigator: ChatsStack` — navegador aninhado
|
|
2471
|
+
*
|
|
2472
|
+
* `groups: { G: { screens: { … } } }` também entra: faz parte da API estática
|
|
2473
|
+
* e agrupar rotas não muda o que elas são.
|
|
2474
|
+
*/
|
|
2475
|
+
parseStaticScreens(filePath, config, navigatorName, navigatorType, depth = 0) {
|
|
2476
|
+
if (!config || !BabelTypes__namespace.isObjectExpression(config) || depth > 4) return [];
|
|
2477
|
+
const out = [];
|
|
2478
|
+
for (const prop of config.properties) {
|
|
2479
|
+
if (!BabelTypes__namespace.isObjectProperty(prop)) continue;
|
|
2480
|
+
const key = BabelTypes__namespace.isIdentifier(prop.key) ? prop.key.name : BabelTypes__namespace.isStringLiteral(prop.key) ? prop.key.value : null;
|
|
2481
|
+
if (!key) continue;
|
|
2482
|
+
if (key === "groups" && BabelTypes__namespace.isObjectExpression(prop.value)) {
|
|
2483
|
+
for (const group of prop.value.properties) {
|
|
2484
|
+
if (!BabelTypes__namespace.isObjectProperty(group) || !BabelTypes__namespace.isExpression(group.value)) continue;
|
|
2485
|
+
out.push(
|
|
2486
|
+
...this.parseStaticScreens(
|
|
2487
|
+
filePath,
|
|
2488
|
+
group.value,
|
|
2489
|
+
navigatorName,
|
|
2490
|
+
navigatorType,
|
|
2491
|
+
depth + 1
|
|
2492
|
+
)
|
|
2493
|
+
);
|
|
2494
|
+
}
|
|
2495
|
+
continue;
|
|
2496
|
+
}
|
|
2497
|
+
if (key !== "screens" || !BabelTypes__namespace.isObjectExpression(prop.value)) continue;
|
|
2498
|
+
for (const entry of prop.value.properties) {
|
|
2499
|
+
if (!BabelTypes__namespace.isObjectProperty(entry)) continue;
|
|
2500
|
+
const routeName = BabelTypes__namespace.isIdentifier(entry.key) ? entry.key.name : BabelTypes__namespace.isStringLiteral(entry.key) ? entry.key.value : null;
|
|
2501
|
+
if (!routeName) continue;
|
|
2502
|
+
const componentExpr = unwrapStaticScreen(entry.value);
|
|
2503
|
+
const componentFile = componentExpr ? this.graph.componentFile(filePath, componentExpr) : null;
|
|
2504
|
+
if (componentFile) this.routeTargets.set(routeName, componentFile);
|
|
2505
|
+
out.push({
|
|
2506
|
+
name: routeName,
|
|
2507
|
+
navigatorName,
|
|
2508
|
+
navigatorType,
|
|
2509
|
+
...componentFile ? { componentFile } : {}
|
|
2510
|
+
});
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
return out;
|
|
2514
|
+
}
|
|
2515
|
+
/**
|
|
2516
|
+
* Todo arquivo-fonte do projeto — não os que ficam num diretório com o nome
|
|
2517
|
+
* certo.
|
|
2518
|
+
*
|
|
2519
|
+
* POR QUE ISTO MUDOU. A lista anterior era `**\/navigation/**`,
|
|
2520
|
+
* `**\/navigator*` e `**\/routes*`, o que fazia da descoberta de navegação uma
|
|
2521
|
+
* convenção de caminho. Medido contra 20 apps React Native de terceiros
|
|
2522
|
+
* (`qa/eval-corpus`), o filtro errava por motivos que nada têm a ver com o
|
|
2523
|
+
* app não ter navegação:
|
|
2524
|
+
*
|
|
2525
|
+
* - `pocketpal` declara 4 navegadores em `App.tsx` e dentro de `src/screens/`;
|
|
2526
|
+
* - `comapeo` usa `src/frontend/Navigation/` — `N` maiúsculo, e o glob é
|
|
2527
|
+
* sensível a caixa;
|
|
2528
|
+
* - `abacus` usa `src/routes/index.tsx` — `routes` é o DIRETÓRIO, e o glob
|
|
2529
|
+
* pedia um arquivo chamado `routes*`;
|
|
2530
|
+
* - `discourse` declara em `js/Discourse.js` — o glob só aceitava `.ts`/`.tsx`;
|
|
2531
|
+
* - `rainbow` tem 11 arquivos com navegador e o glob alcançava 1.
|
|
2532
|
+
*
|
|
2533
|
+
* Varrer tudo não custa uma varredura nova: `ReactNativePlatformAnalyzer` já
|
|
2534
|
+
* globa e parseia a árvore inteira para `ComponentAnalyzer` e `FormAnalyzer`.
|
|
2535
|
+
* O que segura o custo aqui é o portão por evidência em `analyze()`, que só
|
|
2536
|
+
* parseia arquivo cujo texto menciona uma fábrica de navegador.
|
|
2537
|
+
*
|
|
2538
|
+
* Os globs antigos continuam na lista: se um projeto restringir `include`, o
|
|
2539
|
+
* que era encontrado antes continua sendo.
|
|
2540
|
+
*/
|
|
1694
2541
|
async findNavigationFiles() {
|
|
1695
2542
|
const patterns = [
|
|
2543
|
+
...this.config.include ?? ["**/*.{ts,tsx,js,jsx}"],
|
|
2544
|
+
// Piso de compatibilidade — nunca encontrar MENOS que a versão anterior.
|
|
1696
2545
|
"**/navigation/**/*.{ts,tsx}",
|
|
1697
2546
|
"**/navigator*.{ts,tsx}",
|
|
1698
2547
|
"**/routes*.{ts,tsx}",
|
|
@@ -1703,91 +2552,188 @@ var NavigationAnalyzer = class {
|
|
|
1703
2552
|
"**/node_modules/**",
|
|
1704
2553
|
"**/dist/**",
|
|
1705
2554
|
"**/build/**",
|
|
2555
|
+
// Um navegador declarado dentro de um teste é fixture, não a navegação
|
|
2556
|
+
// do app. Isto passou a importar quando a varredura deixou de ser por
|
|
2557
|
+
// caminho: em `expensify`, os únicos arquivos que o glob antigo
|
|
2558
|
+
// alcançava eram três de `tests/`.
|
|
2559
|
+
"**/__tests__/**",
|
|
2560
|
+
"**/test/**",
|
|
2561
|
+
"**/tests/**",
|
|
2562
|
+
"**/*.test.{ts,tsx,js,jsx}",
|
|
2563
|
+
"**/*.tests.{ts,tsx,js,jsx}",
|
|
2564
|
+
"**/*.spec.{ts,tsx,js,jsx}",
|
|
2565
|
+
// Sufixo `Test` sem ponto: `expensify` chama os fixtures dele de
|
|
2566
|
+
// `LegalNameStepTest.tsx`, e cada um monta um `<Stack.Screen>` próprio.
|
|
2567
|
+
// Auditando as 144 rotas lidas dele contra `SCREENS.ts`, 2 vinham daqui —
|
|
2568
|
+
// destinos que existem no teste e não no app, para os quais o agente
|
|
2569
|
+
// tentaria navegar.
|
|
2570
|
+
"**/*Test.{ts,tsx,js,jsx}",
|
|
2571
|
+
"**/*Tests.{ts,tsx,js,jsx}",
|
|
1706
2572
|
...this.config.exclude || [],
|
|
1707
2573
|
// §5: Add user-configured navigation excludes
|
|
1708
2574
|
...this.navigationExclude
|
|
1709
2575
|
];
|
|
1710
|
-
const files = await
|
|
2576
|
+
const files = await globSorted(patterns, {
|
|
1711
2577
|
cwd: this.config.rootDir,
|
|
1712
2578
|
ignore: excludePatterns
|
|
1713
2579
|
});
|
|
1714
|
-
return files.map((file) =>
|
|
2580
|
+
return files.map((file) => path2__namespace.default.join(this.config.rootDir, file));
|
|
1715
2581
|
}
|
|
1716
|
-
/**
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
traverse4__default.default(ast, {
|
|
1731
|
-
// Detect createStackNavigator / createTabNavigator / createDrawerNavigator calls
|
|
1732
|
-
CallExpression: (nodePath) => {
|
|
1733
|
-
const { node } = nodePath;
|
|
1734
|
-
const callee = node.callee;
|
|
1735
|
-
let navigatorType = null;
|
|
1736
|
-
if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createNativeStackNavigator") {
|
|
1737
|
-
navigatorType = "stack";
|
|
1738
|
-
} else if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createStackNavigator") {
|
|
1739
|
-
navigatorType = "stack";
|
|
1740
|
-
} else if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createBottomTabNavigator") {
|
|
1741
|
-
navigatorType = "tab";
|
|
1742
|
-
} else if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createTabNavigator") {
|
|
1743
|
-
navigatorType = "tab";
|
|
1744
|
-
} else if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createDrawerNavigator") {
|
|
1745
|
-
navigatorType = "drawer";
|
|
1746
|
-
}
|
|
1747
|
-
if (navigatorType) {
|
|
1748
|
-
const parent = nodePath.parent;
|
|
1749
|
-
if (BabelTypes__namespace.isVariableDeclarator(parent) && BabelTypes__namespace.isIdentifier(parent.id)) {
|
|
1750
|
-
const varName = parent.id.name;
|
|
1751
|
-
navigatorCalls.set(varName, {
|
|
1752
|
-
name: varName,
|
|
1753
|
-
type: navigatorType,
|
|
1754
|
-
screens: []
|
|
1755
|
-
});
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
},
|
|
1759
|
-
// Detect Stack.Navigator / Tab.Navigator JSX elements
|
|
1760
|
-
JSXElement: (nodePath) => {
|
|
1761
|
-
const { node } = nodePath;
|
|
1762
|
-
const openingElement = node.openingElement;
|
|
1763
|
-
if (BabelTypes__namespace.isJSXMemberExpression(openingElement.name) && BabelTypes__namespace.isJSXIdentifier(openingElement.name.object)) {
|
|
1764
|
-
const objectName = openingElement.name.object.name;
|
|
1765
|
-
const propertyName = BabelTypes__namespace.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
|
|
1766
|
-
if (propertyName === "Navigator") {
|
|
1767
|
-
const navigator = navigatorCalls.get(objectName);
|
|
1768
|
-
if (navigator) {
|
|
1769
|
-
const initialRouteAttr = openingElement.attributes.find(
|
|
1770
|
-
(attr) => BabelTypes__namespace.isJSXAttribute(attr) && BabelTypes__namespace.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
|
|
1771
|
-
);
|
|
1772
|
-
if (BabelTypes__namespace.isJSXAttribute(initialRouteAttr) && BabelTypes__namespace.isStringLiteral(initialRouteAttr.value)) {
|
|
1773
|
-
navigator.initialRouteName = initialRouteAttr.value.value;
|
|
1774
|
-
}
|
|
1775
|
-
const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
|
|
1776
|
-
screensByNavigator.set(objectName, screens);
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
2582
|
+
/**
|
|
2583
|
+
* Fase 2: o JSX `<X.Navigator>` deste arquivo, ligado à declaração de `X` —
|
|
2584
|
+
* que pode estar aqui ou em qualquer arquivo que este importe.
|
|
2585
|
+
*/
|
|
2586
|
+
parseNavigatorUsages(filePath, declarations) {
|
|
2587
|
+
const ast = this.graph.parse(filePath);
|
|
2588
|
+
if (!ast) return [];
|
|
2589
|
+
const found = /* @__PURE__ */ new Map();
|
|
2590
|
+
traverse4__default.default(ast, {
|
|
2591
|
+
JSXElement: (nodePath) => {
|
|
2592
|
+
const { node } = nodePath;
|
|
2593
|
+
const openingElement = node.openingElement;
|
|
2594
|
+
if (!BabelTypes__namespace.isJSXMemberExpression(openingElement.name) || !BabelTypes__namespace.isJSXIdentifier(openingElement.name.object)) {
|
|
2595
|
+
return;
|
|
1780
2596
|
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
2597
|
+
const objectName = openingElement.name.object.name;
|
|
2598
|
+
const propertyName = BabelTypes__namespace.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
|
|
2599
|
+
if (propertyName !== "Navigator") return;
|
|
2600
|
+
const decl = this.resolveNavigator(filePath, objectName, declarations);
|
|
2601
|
+
if (!decl) return;
|
|
2602
|
+
const navigator = found.get(objectName) ?? {
|
|
2603
|
+
name: objectName,
|
|
2604
|
+
type: decl.type,
|
|
2605
|
+
screens: []
|
|
2606
|
+
};
|
|
2607
|
+
const initialRouteAttr = openingElement.attributes.find(
|
|
2608
|
+
(attr) => BabelTypes__namespace.isJSXAttribute(attr) && BabelTypes__namespace.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
|
|
2609
|
+
);
|
|
2610
|
+
if (BabelTypes__namespace.isJSXAttribute(initialRouteAttr)) {
|
|
2611
|
+
const initial = this.attributeString(filePath, initialRouteAttr);
|
|
2612
|
+
if (initial) navigator.initialRouteName = initial;
|
|
2613
|
+
}
|
|
2614
|
+
this.navigatorFiles.add(filePath);
|
|
2615
|
+
this.navigatorFiles.add(decl.file);
|
|
2616
|
+
navigator.screens = this.extractScreensFromNavigator(filePath, node, objectName, decl.type);
|
|
2617
|
+
found.set(objectName, navigator);
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
return [...found.values()];
|
|
2621
|
+
}
|
|
2622
|
+
/**
|
|
2623
|
+
* Rotas declaradas FORA de qualquer `<X.Navigator>`.
|
|
2624
|
+
*
|
|
2625
|
+
* O CASO. A fase 2 desce a partir do `<X.Navigator>` e lê as `<X.Screen>`
|
|
2626
|
+
* que estão DENTRO dele. Dois alvos do corpus não escrevem assim, e entre os
|
|
2627
|
+
* dois são 171 rotas invisíveis:
|
|
2628
|
+
*
|
|
2629
|
+
* `bluesky` — `function commonScreens(Stack: typeof Flat) { return (<>
|
|
2630
|
+
* <Stack.Screen name="NotFound" … /> … </>) }`, chamada de
|
|
2631
|
+
* dentro de seis navegadores diferentes. 70 rotas.
|
|
2632
|
+
* `comapeo` — `export const createAppScreens = ({intl}) => (<>
|
|
2633
|
+
* <RootStack.Group><RootStack.Screen … /></RootStack.Group></>)`,
|
|
2634
|
+
* num arquivo sem `<RootStack.Navigator>` nenhum. 101 rotas.
|
|
2635
|
+
*
|
|
2636
|
+
* A REGRA. Uma `<X.Screen name="…">` sem `<Y.Navigator>` ancestral é rota do
|
|
2637
|
+
* navegador ao qual `X` se resolve. Não há palpite em jogo: o nome da rota é
|
|
2638
|
+
* literal do fonte (ou constante que o resolvedor segue), e `X` precisa
|
|
2639
|
+
* chegar a uma declaração de navegador que já existe. O que não resolve não
|
|
2640
|
+
* vira nada.
|
|
2641
|
+
*
|
|
2642
|
+
* O ancestral é o que evita contar duas vezes — dentro do `<X.Navigator>` a
|
|
2643
|
+
* fase 2 já leu, e somar aqui duplicaria cada rota do corpus inteiro.
|
|
2644
|
+
*
|
|
2645
|
+
* DUAS FORMAS DE RESOLVER `X`, e a segunda é o que o `bluesky` exige. A
|
|
2646
|
+
* primeira é a de sempre (declaração local, ou `import` seguido até a
|
|
2647
|
+
* origem) e resolve o `comapeo`. A segunda lê a ANOTAÇÃO DE TIPO do
|
|
2648
|
+
* parâmetro: em `commonScreens(Stack: typeof Flat)`, quem diz que `Stack` é
|
|
2649
|
+
* o navegador `Flat` é o próprio app, no fonte.
|
|
2650
|
+
*
|
|
2651
|
+
* O QUE ISTO NÃO FAZ. As 70 do `bluesky` ficam atribuídas a `Flat` — que as
|
|
2652
|
+
* contém de fato (`{commonScreens(Flat, numUnread)}`) — e não aos outros
|
|
2653
|
+
* cinco stacks que também chamam a mesma função. Seguir os seis pontos de
|
|
2654
|
+
* chamada é análise interprocedural, e o ganho seria só de atribuição: o nome
|
|
2655
|
+
* da rota e o arquivo da tela, que é o que o agente usa, já saem certos.
|
|
2656
|
+
*/
|
|
2657
|
+
parseDetachedScreens(filePath, declarations) {
|
|
2658
|
+
const ast = this.graph.parse(filePath);
|
|
2659
|
+
if (!ast) return [];
|
|
2660
|
+
const byNavigator = /* @__PURE__ */ new Map();
|
|
2661
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
2662
|
+
traverse4__default.default(ast, {
|
|
2663
|
+
JSXElement: (nodePath) => {
|
|
2664
|
+
const name = nodePath.node.openingElement.name;
|
|
2665
|
+
if (!BabelTypes__namespace.isJSXMemberExpression(name) || !BabelTypes__namespace.isJSXIdentifier(name.object)) return;
|
|
2666
|
+
if (!BabelTypes__namespace.isJSXIdentifier(name.property) || name.property.name !== "Screen") return;
|
|
2667
|
+
const insideNavigator = nodePath.findParent((parent) => {
|
|
2668
|
+
if (!parent.isJSXElement()) return false;
|
|
2669
|
+
const parentName = parent.node.openingElement.name;
|
|
2670
|
+
return BabelTypes__namespace.isJSXMemberExpression(parentName) && BabelTypes__namespace.isJSXIdentifier(parentName.property) && parentName.property.name === "Navigator";
|
|
2671
|
+
});
|
|
2672
|
+
if (insideNavigator) return;
|
|
2673
|
+
const local = name.object.name;
|
|
2674
|
+
if (!resolved.has(local)) {
|
|
2675
|
+
resolved.set(
|
|
2676
|
+
local,
|
|
2677
|
+
this.resolveDetachedNavigator(filePath, local, nodePath, declarations)
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
const decl = resolved.get(local);
|
|
2681
|
+
if (!decl) return;
|
|
2682
|
+
const screen = this.parseScreenElement(filePath, nodePath.node, decl.name, decl.type);
|
|
2683
|
+
if (!screen) return;
|
|
2684
|
+
const nav = byNavigator.get(decl.name) ?? {
|
|
2685
|
+
name: decl.name,
|
|
2686
|
+
type: decl.type,
|
|
2687
|
+
screens: []
|
|
2688
|
+
};
|
|
2689
|
+
if (!nav.screens.some((existing) => existing.name === screen.name))
|
|
2690
|
+
nav.screens.push(screen);
|
|
2691
|
+
byNavigator.set(decl.name, nav);
|
|
2692
|
+
}
|
|
2693
|
+
});
|
|
2694
|
+
return [...byNavigator.values()];
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2697
|
+
* De um `X` usado como `<X.Screen>` até a declaração do navegador.
|
|
2698
|
+
*
|
|
2699
|
+
* Além do caminho normal — declaração local ou `import` seguido até a origem
|
|
2700
|
+
* — aceita `X` como PARÂMETRO anotado com `typeof Y`. É o idioma do
|
|
2701
|
+
* `bluesky`, e a anotação é declaração do app: nada aqui é inferido do nome.
|
|
2702
|
+
*/
|
|
2703
|
+
resolveDetachedNavigator(filePath, localName, nodePath, declarations) {
|
|
2704
|
+
const direct = this.resolveNavigator(filePath, localName, declarations);
|
|
2705
|
+
if (direct) return direct;
|
|
2706
|
+
const binding = nodePath.scope.getBinding(localName);
|
|
2707
|
+
if (!binding || binding.kind !== "param") return null;
|
|
2708
|
+
const param = binding.path.node;
|
|
2709
|
+
if (!BabelTypes__namespace.isIdentifier(param) || !BabelTypes__namespace.isTSTypeAnnotation(param.typeAnnotation)) return null;
|
|
2710
|
+
const annotation = param.typeAnnotation.typeAnnotation;
|
|
2711
|
+
if (!BabelTypes__namespace.isTSTypeQuery(annotation) || !BabelTypes__namespace.isIdentifier(annotation.exprName)) return null;
|
|
2712
|
+
return this.resolveNavigator(filePath, annotation.exprName.name, declarations);
|
|
2713
|
+
}
|
|
2714
|
+
/**
|
|
2715
|
+
* De `<X.Navigator>` até a declaração de `X`.
|
|
2716
|
+
*
|
|
2717
|
+
* Primeiro no próprio arquivo; se não estiver, segue o `import` até onde `X`
|
|
2718
|
+
* é definido. Resolver pelo import é o que torna a junção entre arquivos
|
|
2719
|
+
* segura: dois `Stack` de arquivos diferentes nunca colidem, porque a chave
|
|
2720
|
+
* é o arquivo de DEFINIÇÃO.
|
|
2721
|
+
*/
|
|
2722
|
+
resolveNavigator(filePath, localName, declarations) {
|
|
2723
|
+
const local = declarations.get(`${filePath}#${localName}`);
|
|
2724
|
+
if (local) return local;
|
|
2725
|
+
const origin = this.graph.resolveBinding(filePath, localName);
|
|
2726
|
+
if (!origin) return null;
|
|
2727
|
+
return declarations.get(`${origin.file}#${origin.name}`) ?? null;
|
|
2728
|
+
}
|
|
2729
|
+
/** O valor string de um atributo JSX, resolvendo constante importada. */
|
|
2730
|
+
attributeString(filePath, attr) {
|
|
2731
|
+
const value = attr.value;
|
|
2732
|
+
if (BabelTypes__namespace.isStringLiteral(value)) return value.value;
|
|
2733
|
+
if (BabelTypes__namespace.isJSXExpressionContainer(value) && BabelTypes__namespace.isExpression(value.expression)) {
|
|
2734
|
+
return this.graph.stringConstant(filePath, value.expression);
|
|
1789
2735
|
}
|
|
1790
|
-
return
|
|
2736
|
+
return null;
|
|
1791
2737
|
}
|
|
1792
2738
|
/**
|
|
1793
2739
|
* Is this JSX element `<navigatorVarName.MEMBER …>`?
|
|
@@ -1882,22 +2828,48 @@ var NavigationAnalyzer = class {
|
|
|
1882
2828
|
return found;
|
|
1883
2829
|
}
|
|
1884
2830
|
/** Extract screens from a navigator JSX element */
|
|
1885
|
-
extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
|
|
2831
|
+
extractScreensFromNavigator(filePath, navigatorElement, navigatorVarName, navigatorType) {
|
|
1886
2832
|
const screens = [];
|
|
1887
2833
|
if (!navigatorElement.children) return screens;
|
|
1888
2834
|
const seen = /* @__PURE__ */ new Set();
|
|
1889
2835
|
for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
|
|
1890
|
-
const
|
|
1891
|
-
if (!
|
|
1892
|
-
seen.add(
|
|
1893
|
-
screens.push(
|
|
1894
|
-
name: screenName,
|
|
1895
|
-
navigatorName: navigatorVarName,
|
|
1896
|
-
navigatorType
|
|
1897
|
-
});
|
|
2836
|
+
const screen = this.parseScreenElement(filePath, element, navigatorVarName, navigatorType);
|
|
2837
|
+
if (!screen || seen.has(screen.name)) continue;
|
|
2838
|
+
seen.add(screen.name);
|
|
2839
|
+
screens.push(screen);
|
|
1898
2840
|
}
|
|
1899
2841
|
return screens;
|
|
1900
2842
|
}
|
|
2843
|
+
/**
|
|
2844
|
+
* Uma `<X.Screen>` isolada até a rota que ela declara.
|
|
2845
|
+
*
|
|
2846
|
+
* Separado de `extractScreensFromNavigator` porque a MESMA leitura serve para
|
|
2847
|
+
* a `<X.Screen>` que mora dentro do `<X.Navigator>` e para a que mora fora
|
|
2848
|
+
* dele — só a forma de chegar até o elemento muda.
|
|
2849
|
+
*/
|
|
2850
|
+
parseScreenElement(filePath, element, navigatorName, navigatorType) {
|
|
2851
|
+
const attrs = element.openingElement.attributes;
|
|
2852
|
+
const nameAttr = attrs.find(
|
|
2853
|
+
(a) => BabelTypes__namespace.isJSXAttribute(a) && BabelTypes__namespace.isJSXIdentifier(a.name) && a.name.name === "name"
|
|
2854
|
+
);
|
|
2855
|
+
const screenName = BabelTypes__namespace.isJSXAttribute(nameAttr) ? this.attributeString(filePath, nameAttr) : null;
|
|
2856
|
+
if (!screenName) return null;
|
|
2857
|
+
const componentAttr = attrs.find(
|
|
2858
|
+
(a) => BabelTypes__namespace.isJSXAttribute(a) && BabelTypes__namespace.isJSXIdentifier(a.name) && a.name.name === "component"
|
|
2859
|
+
);
|
|
2860
|
+
let componentFile = null;
|
|
2861
|
+
if (BabelTypes__namespace.isJSXAttribute(componentAttr) && BabelTypes__namespace.isJSXExpressionContainer(componentAttr.value)) {
|
|
2862
|
+
const expr = componentAttr.value.expression;
|
|
2863
|
+
if (BabelTypes__namespace.isExpression(expr)) componentFile = this.graph.componentFile(filePath, expr);
|
|
2864
|
+
}
|
|
2865
|
+
if (componentFile) this.routeTargets.set(screenName, componentFile);
|
|
2866
|
+
return {
|
|
2867
|
+
name: screenName,
|
|
2868
|
+
navigatorName,
|
|
2869
|
+
navigatorType,
|
|
2870
|
+
...componentFile ? { componentFile } : {}
|
|
2871
|
+
};
|
|
2872
|
+
}
|
|
1901
2873
|
/** Extract string attribute value from JSX attributes */
|
|
1902
2874
|
extractAttributeValue(attributes, attrName) {
|
|
1903
2875
|
const attr = attributes.find(
|
|
@@ -1993,7 +2965,7 @@ var NavigationAnalyzer = class {
|
|
|
1993
2965
|
if (type.type === "TSUndefinedKeyword") return "undefined";
|
|
1994
2966
|
if (type.type === "TSNullKeyword") return "null";
|
|
1995
2967
|
if (type.type === "TSUnionType") {
|
|
1996
|
-
return type.types.map((
|
|
2968
|
+
return type.types.map((t13) => this.typeToString(t13)).join(" | ");
|
|
1997
2969
|
}
|
|
1998
2970
|
if (type.type === "TSTypeLiteral") {
|
|
1999
2971
|
return "object";
|
|
@@ -2014,7 +2986,7 @@ var NavigationAnalyzer = class {
|
|
|
2014
2986
|
/** Attach parsed type params to navigator screens */
|
|
2015
2987
|
attachParamsToNavigators(navigators, types) {
|
|
2016
2988
|
for (const navigator of navigators) {
|
|
2017
|
-
const matchingType = types.find((
|
|
2989
|
+
const matchingType = types.find((t13) => t13.type === navigator.type);
|
|
2018
2990
|
if (matchingType) {
|
|
2019
2991
|
for (const screen of navigator.screens) {
|
|
2020
2992
|
const screenParams = matchingType.paramEntries.get(screen.name);
|
|
@@ -2104,8 +3076,8 @@ var ComponentAnalyzer = class {
|
|
|
2104
3076
|
});
|
|
2105
3077
|
const components = [];
|
|
2106
3078
|
traverse4__default.default(ast, {
|
|
2107
|
-
JSXElement: (
|
|
2108
|
-
const component = this.extractComponentFromJSXElement(
|
|
3079
|
+
JSXElement: (path11) => {
|
|
3080
|
+
const component = this.extractComponentFromJSXElement(path11.node);
|
|
2109
3081
|
if (component) {
|
|
2110
3082
|
components.push(component);
|
|
2111
3083
|
}
|
|
@@ -2232,13 +3204,13 @@ var FormAnalyzer = class {
|
|
|
2232
3204
|
this.inputElements = [];
|
|
2233
3205
|
this.submitButtons = [];
|
|
2234
3206
|
traverse4__default.default(ast, {
|
|
2235
|
-
CallExpression: (
|
|
2236
|
-
this.extractStateVariables(
|
|
3207
|
+
CallExpression: (path11) => {
|
|
3208
|
+
this.extractStateVariables(path11.node);
|
|
2237
3209
|
}
|
|
2238
3210
|
});
|
|
2239
3211
|
traverse4__default.default(ast, {
|
|
2240
|
-
JSXElement: (
|
|
2241
|
-
this.extractFormElements(
|
|
3212
|
+
JSXElement: (path11) => {
|
|
3213
|
+
this.extractFormElements(path11.node);
|
|
2242
3214
|
}
|
|
2243
3215
|
});
|
|
2244
3216
|
const validationRules = this.extractValidationRules(ast);
|
|
@@ -2338,8 +3310,8 @@ var FormAnalyzer = class {
|
|
|
2338
3310
|
extractValidationRules(ast) {
|
|
2339
3311
|
const rules = {};
|
|
2340
3312
|
traverse4__default.default(ast, {
|
|
2341
|
-
IfStatement: (
|
|
2342
|
-
const test =
|
|
3313
|
+
IfStatement: (path11) => {
|
|
3314
|
+
const test = path11.node.test;
|
|
2343
3315
|
const rule = this.extractRuleFromCondition(test);
|
|
2344
3316
|
if (rule) {
|
|
2345
3317
|
const { field, description } = rule;
|
|
@@ -2393,7 +3365,7 @@ var FormAnalyzer = class {
|
|
|
2393
3365
|
}
|
|
2394
3366
|
buildForms(filePath, validationRules) {
|
|
2395
3367
|
if (this.inputElements.length === 0) return [];
|
|
2396
|
-
const fileName =
|
|
3368
|
+
const fileName = path2__namespace.basename(filePath, path2__namespace.extname(filePath));
|
|
2397
3369
|
const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
|
|
2398
3370
|
const fields = this.inputElements.map((input) => {
|
|
2399
3371
|
const fieldType = this.inferFieldType(input);
|
|
@@ -2442,13 +3414,105 @@ var FormAnalyzer = class {
|
|
|
2442
3414
|
return rule !== void 0 && rule.includes("required");
|
|
2443
3415
|
}
|
|
2444
3416
|
};
|
|
3417
|
+
|
|
3418
|
+
// src/pipeline/composition.ts
|
|
3419
|
+
var MAX_DEPTH = 3;
|
|
3420
|
+
var MAX_VISITED_PER_SCREEN = 60;
|
|
3421
|
+
var MAX_FAN_IN = 3;
|
|
3422
|
+
var ADDRESSABLE = /* @__PURE__ */ new Set(["testID", "appilotsId", "accessibilityLabel", "label"]);
|
|
3423
|
+
function descendants(graph, from, screenFiles) {
|
|
3424
|
+
const seen = /* @__PURE__ */ new Set([from]);
|
|
3425
|
+
const out = [];
|
|
3426
|
+
let frontier = [from];
|
|
3427
|
+
for (let depth = 0; depth < MAX_DEPTH && frontier.length > 0; depth++) {
|
|
3428
|
+
const next = [];
|
|
3429
|
+
for (const file of frontier) {
|
|
3430
|
+
for (const child of graph.renderedComponentFiles(file)) {
|
|
3431
|
+
if (seen.has(child)) continue;
|
|
3432
|
+
seen.add(child);
|
|
3433
|
+
if (screenFiles.has(child)) continue;
|
|
3434
|
+
out.push(child);
|
|
3435
|
+
next.push(child);
|
|
3436
|
+
if (out.length >= MAX_VISITED_PER_SCREEN) return out;
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
frontier = next;
|
|
3440
|
+
}
|
|
3441
|
+
return out;
|
|
3442
|
+
}
|
|
3443
|
+
function addressable(locator) {
|
|
3444
|
+
if (!locator || typeof locator !== "object") return false;
|
|
3445
|
+
const l = locator;
|
|
3446
|
+
if (typeof l["source"] === "string" && ADDRESSABLE.has(l["source"])) return true;
|
|
3447
|
+
return typeof l["testID"] === "string" && l["testID"].length > 0;
|
|
3448
|
+
}
|
|
3449
|
+
async function composeAffordances(options) {
|
|
3450
|
+
const { screens, graph, analyzeFile, screenFiles } = options;
|
|
3451
|
+
const byScreen = /* @__PURE__ */ new Map();
|
|
3452
|
+
const fanIn = /* @__PURE__ */ new Map();
|
|
3453
|
+
for (const screen of screens) {
|
|
3454
|
+
if (!screen.filePath) continue;
|
|
3455
|
+
const children = descendants(graph, screen.filePath, screenFiles);
|
|
3456
|
+
byScreen.set(screen, children);
|
|
3457
|
+
for (const child of children) fanIn.set(child, (fanIn.get(child) ?? 0) + 1);
|
|
3458
|
+
}
|
|
3459
|
+
const analyzed = /* @__PURE__ */ new Map();
|
|
3460
|
+
let screensEnriched = 0;
|
|
3461
|
+
let filesMerged = 0;
|
|
3462
|
+
for (const [screen, children] of byScreen) {
|
|
3463
|
+
const exclusive = children.filter((c) => (fanIn.get(c) ?? 0) <= MAX_FAN_IN);
|
|
3464
|
+
if (exclusive.length === 0) continue;
|
|
3465
|
+
const actionIds = new Set(screen.actions.map((a) => a.id));
|
|
3466
|
+
const targetIds = new Set((screen.targets ?? []).map((t13) => t13.id));
|
|
3467
|
+
const formIds = new Set(screen.forms.map((f) => f.id));
|
|
3468
|
+
let gained = false;
|
|
3469
|
+
for (const file of exclusive) {
|
|
3470
|
+
if (!analyzed.has(file)) {
|
|
3471
|
+
try {
|
|
3472
|
+
analyzed.set(file, await analyzeFile(file));
|
|
3473
|
+
} catch {
|
|
3474
|
+
analyzed.set(file, null);
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
const child = analyzed.get(file);
|
|
3478
|
+
if (!child) continue;
|
|
3479
|
+
let merged = false;
|
|
3480
|
+
for (const action of child.actions) {
|
|
3481
|
+
if (!addressable(action.locator)) continue;
|
|
3482
|
+
if (actionIds.has(action.id)) continue;
|
|
3483
|
+
actionIds.add(action.id);
|
|
3484
|
+
screen.actions.push(action);
|
|
3485
|
+
merged = true;
|
|
3486
|
+
}
|
|
3487
|
+
for (const target of child.targets ?? []) {
|
|
3488
|
+
if (!addressable(target.locator)) continue;
|
|
3489
|
+
if (targetIds.has(target.id)) continue;
|
|
3490
|
+
targetIds.add(target.id);
|
|
3491
|
+
(screen.targets ??= []).push(target);
|
|
3492
|
+
merged = true;
|
|
3493
|
+
}
|
|
3494
|
+
for (const form of child.forms) {
|
|
3495
|
+
if (form.fields.length === 0) continue;
|
|
3496
|
+
const id = formIds.has(form.id) ? `${form.id}:${child.name}` : form.id;
|
|
3497
|
+
if (formIds.has(id)) continue;
|
|
3498
|
+
formIds.add(id);
|
|
3499
|
+
screen.forms.push({ ...form, id });
|
|
3500
|
+
merged = true;
|
|
3501
|
+
}
|
|
3502
|
+
if (merged) {
|
|
3503
|
+
filesMerged++;
|
|
3504
|
+
gained = true;
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
if (gained) screensEnriched++;
|
|
3508
|
+
}
|
|
3509
|
+
return { screens, screensEnriched, filesMerged };
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
// src/analyzers/ReactNativePlatformAnalyzer.ts
|
|
2445
3513
|
var ReactNativePlatformAnalyzer = class {
|
|
2446
3514
|
platform = "react-native";
|
|
2447
3515
|
async analyze(config, options) {
|
|
2448
|
-
const screenAnalyzer = new ScreenAnalyzer(config, {
|
|
2449
|
-
strictScreens: options.strictScreens ?? true,
|
|
2450
|
-
screenPatterns: options.screenPatterns
|
|
2451
|
-
});
|
|
2452
3516
|
const navigationAnalyzer = new NavigationAnalyzer(config, {
|
|
2453
3517
|
navigationInclude: options.navigationInclude,
|
|
2454
3518
|
navigationExclude: options.navigationExclude
|
|
@@ -2456,14 +3520,31 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2456
3520
|
const componentAnalyzer = new ComponentAnalyzer(config);
|
|
2457
3521
|
const formAnalyzer = new FormAnalyzer(config);
|
|
2458
3522
|
console.log("[ReactNativePlatformAnalyzer] Running analyzers...");
|
|
2459
|
-
const
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
3523
|
+
const navigation = await navigationAnalyzer.analyze();
|
|
3524
|
+
const screenAnalyzer = new ScreenAnalyzer(config, {
|
|
3525
|
+
strictScreens: options.strictScreens ?? true,
|
|
3526
|
+
screenPatterns: options.screenPatterns,
|
|
3527
|
+
// Menos os arquivos que declaram navegador: ver `navigatorFiles`.
|
|
3528
|
+
routeTargetFiles: new Set(
|
|
3529
|
+
[...navigationAnalyzer.routeTargets.values()].filter(
|
|
3530
|
+
(file) => !navigationAnalyzer.navigatorFiles.has(file)
|
|
3531
|
+
)
|
|
3532
|
+
)
|
|
3533
|
+
});
|
|
3534
|
+
const screens = await screenAnalyzer.analyze();
|
|
3535
|
+
const composed = await composeAffordances({
|
|
3536
|
+
screens,
|
|
3537
|
+
graph: navigationAnalyzer.graph,
|
|
3538
|
+
analyzeFile: (file) => screenAnalyzer.analyzeFile(file),
|
|
3539
|
+
screenFiles: new Set(screens.map((s) => s.filePath).filter(Boolean))
|
|
3540
|
+
});
|
|
3541
|
+
console.log(
|
|
3542
|
+
`[ReactNativePlatformAnalyzer] Composi\xE7\xE3o: ${composed.filesMerged} componente(s) exclusivo(s) fundido(s) em ${composed.screensEnriched} tela(s)`
|
|
3543
|
+
);
|
|
2463
3544
|
console.log(
|
|
2464
3545
|
`[ReactNativePlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens`
|
|
2465
3546
|
);
|
|
2466
|
-
const screenFiles = await
|
|
3547
|
+
const screenFiles = await globSorted(config.include || ["**/*.tsx", "**/*.ts"], {
|
|
2467
3548
|
cwd: config.rootDir,
|
|
2468
3549
|
ignore: config.exclude || ["**/node_modules/**"]
|
|
2469
3550
|
});
|
|
@@ -2471,7 +3552,7 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2471
3552
|
`[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
|
|
2472
3553
|
);
|
|
2473
3554
|
const enrichmentPromises = screenFiles.map(async (file) => {
|
|
2474
|
-
const filePath =
|
|
3555
|
+
const filePath = path2__namespace.default.resolve(config.rootDir, file);
|
|
2475
3556
|
try {
|
|
2476
3557
|
const [components, forms] = await Promise.all([
|
|
2477
3558
|
componentAnalyzer.analyzeFile(filePath),
|
|
@@ -2526,7 +3607,8 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2526
3607
|
screens: enrichedScreens,
|
|
2527
3608
|
navigation,
|
|
2528
3609
|
analyzedFiles: screenFiles.length,
|
|
2529
|
-
...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
|
|
3610
|
+
...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {},
|
|
3611
|
+
diagnostics: { navigation: navigationAnalyzer.diagnostics }
|
|
2530
3612
|
};
|
|
2531
3613
|
}
|
|
2532
3614
|
mergeForm(target, source) {
|
|
@@ -2544,9 +3626,12 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2544
3626
|
findEquivalentField(fields, incoming) {
|
|
2545
3627
|
return fields.find((field) => {
|
|
2546
3628
|
if (field.name && incoming.name && field.name === incoming.name) return true;
|
|
2547
|
-
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
2548
|
-
|
|
2549
|
-
if (field.
|
|
3629
|
+
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
3630
|
+
return true;
|
|
3631
|
+
if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
|
|
3632
|
+
return true;
|
|
3633
|
+
if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
|
|
3634
|
+
return true;
|
|
2550
3635
|
if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
|
|
2551
3636
|
return true;
|
|
2552
3637
|
}
|
|
@@ -2579,7 +3664,9 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2579
3664
|
if (incoming.fields.length === 0) return void 0;
|
|
2580
3665
|
let best;
|
|
2581
3666
|
for (const form of forms) {
|
|
2582
|
-
const overlap = incoming.fields.filter(
|
|
3667
|
+
const overlap = incoming.fields.filter(
|
|
3668
|
+
(field) => this.findEquivalentField(form.fields, field)
|
|
3669
|
+
).length;
|
|
2583
3670
|
if (overlap > 0 && (!best || overlap > best.overlap)) {
|
|
2584
3671
|
best = { form, overlap };
|
|
2585
3672
|
}
|
|
@@ -2733,13 +3820,13 @@ function staticRoutePath(node) {
|
|
|
2733
3820
|
if (!node) return void 0;
|
|
2734
3821
|
if (BabelTypes__namespace.isStringLiteral(node)) return node.value;
|
|
2735
3822
|
if (BabelTypes__namespace.isTemplateLiteral(node)) {
|
|
2736
|
-
let
|
|
3823
|
+
let path11 = "";
|
|
2737
3824
|
node.quasis.forEach((quasi, index) => {
|
|
2738
|
-
|
|
3825
|
+
path11 += quasi.value.cooked ?? quasi.value.raw;
|
|
2739
3826
|
const expr = node.expressions[index];
|
|
2740
|
-
if (expr)
|
|
3827
|
+
if (expr) path11 += `:${paramNameOf(expr)}`;
|
|
2741
3828
|
});
|
|
2742
|
-
return
|
|
3829
|
+
return path11;
|
|
2743
3830
|
}
|
|
2744
3831
|
return void 0;
|
|
2745
3832
|
}
|
|
@@ -2824,17 +3911,17 @@ var WebScreenAnalyzer = class {
|
|
|
2824
3911
|
include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
|
|
2825
3912
|
exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
2826
3913
|
} = this.config;
|
|
2827
|
-
const files = (await
|
|
3914
|
+
const files = (await globSorted(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
|
|
2828
3915
|
(file) => !file.endsWith(".d.ts")
|
|
2829
3916
|
);
|
|
2830
|
-
const patternMatches = await
|
|
3917
|
+
const patternMatches = await globSorted(this.screenPatterns, {
|
|
2831
3918
|
cwd: this.config.rootDir,
|
|
2832
3919
|
ignore: exclude
|
|
2833
3920
|
});
|
|
2834
|
-
const patternSet = new Set(patternMatches.map((f) =>
|
|
3921
|
+
const patternSet = new Set(patternMatches.map((f) => path2__namespace.default.resolve(this.config.rootDir, f)));
|
|
2835
3922
|
const candidates = [];
|
|
2836
3923
|
for (const file of files) {
|
|
2837
|
-
const filePath =
|
|
3924
|
+
const filePath = path2__namespace.default.resolve(this.config.rootDir, file);
|
|
2838
3925
|
try {
|
|
2839
3926
|
const candidate = await this.analyzeFile(filePath);
|
|
2840
3927
|
if (!candidate) continue;
|
|
@@ -2878,7 +3965,7 @@ var WebScreenAnalyzer = class {
|
|
|
2878
3965
|
}
|
|
2879
3966
|
}
|
|
2880
3967
|
}
|
|
2881
|
-
const name = registerScreenMeta?.name || componentName ||
|
|
3968
|
+
const name = registerScreenMeta?.name || componentName || path2__namespace.default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
|
|
2882
3969
|
const descriptor = {
|
|
2883
3970
|
name,
|
|
2884
3971
|
filePath,
|
|
@@ -3767,7 +4854,9 @@ var WebNavigationAnalyzer = class {
|
|
|
3767
4854
|
for (const filePath of files) {
|
|
3768
4855
|
try {
|
|
3769
4856
|
const content = await fs$1.promises.readFile(filePath, "utf-8");
|
|
3770
|
-
if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
|
|
4857
|
+
if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
|
|
4858
|
+
content
|
|
4859
|
+
)) {
|
|
3771
4860
|
continue;
|
|
3772
4861
|
}
|
|
3773
4862
|
const ast = parseSource(content, this.config.parserPlugins);
|
|
@@ -3797,8 +4886,8 @@ var WebNavigationAnalyzer = class {
|
|
|
3797
4886
|
...this.config.exclude || [],
|
|
3798
4887
|
...this.navigationExclude
|
|
3799
4888
|
];
|
|
3800
|
-
const files = await
|
|
3801
|
-
return files.map((file) =>
|
|
4889
|
+
const files = await globSorted(patterns, { cwd: this.config.rootDir, ignore });
|
|
4890
|
+
return files.map((file) => path2__namespace.default.join(this.config.rootDir, file));
|
|
3802
4891
|
}
|
|
3803
4892
|
// ── JSX <Route> style ────────────────────────────────────────────
|
|
3804
4893
|
extractJsxRoutes(ast) {
|
|
@@ -3970,11 +5059,13 @@ var WebNavigationAnalyzer = class {
|
|
|
3970
5059
|
return {
|
|
3971
5060
|
screens,
|
|
3972
5061
|
initialScreen: initialRoute?.screenName ?? "",
|
|
3973
|
-
navigators: routes.length > 0 ? [
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
5062
|
+
navigators: routes.length > 0 ? [
|
|
5063
|
+
{
|
|
5064
|
+
name: navigatorName,
|
|
5065
|
+
type: WEB_NAVIGATOR_TYPE,
|
|
5066
|
+
screens: screenNames
|
|
5067
|
+
}
|
|
5068
|
+
] : []
|
|
3978
5069
|
};
|
|
3979
5070
|
}
|
|
3980
5071
|
};
|
|
@@ -4296,8 +5387,8 @@ var ZodParsedType = util.arrayToEnum([
|
|
|
4296
5387
|
"set"
|
|
4297
5388
|
]);
|
|
4298
5389
|
var getParsedType = (data) => {
|
|
4299
|
-
const
|
|
4300
|
-
switch (
|
|
5390
|
+
const t13 = typeof data;
|
|
5391
|
+
switch (t13) {
|
|
4301
5392
|
case "undefined":
|
|
4302
5393
|
return ZodParsedType.undefined;
|
|
4303
5394
|
case "string":
|
|
@@ -4569,8 +5660,8 @@ function getErrorMap() {
|
|
|
4569
5660
|
|
|
4570
5661
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
4571
5662
|
var makeIssue = (params) => {
|
|
4572
|
-
const { data, path:
|
|
4573
|
-
const fullPath = [...
|
|
5663
|
+
const { data, path: path11, errorMaps, issueData } = params;
|
|
5664
|
+
const fullPath = [...path11, ...issueData.path || []];
|
|
4574
5665
|
const fullIssue = {
|
|
4575
5666
|
...issueData,
|
|
4576
5667
|
path: fullPath
|
|
@@ -4686,11 +5777,11 @@ var errorUtil;
|
|
|
4686
5777
|
|
|
4687
5778
|
// ../../node_modules/zod/v3/types.js
|
|
4688
5779
|
var ParseInputLazyPath = class {
|
|
4689
|
-
constructor(parent, value,
|
|
5780
|
+
constructor(parent, value, path11, key) {
|
|
4690
5781
|
this._cachedPath = [];
|
|
4691
5782
|
this.parent = parent;
|
|
4692
5783
|
this.data = value;
|
|
4693
|
-
this._path =
|
|
5784
|
+
this._path = path11;
|
|
4694
5785
|
this._key = key;
|
|
4695
5786
|
}
|
|
4696
5787
|
get path() {
|
|
@@ -8131,7 +9222,18 @@ var coerce = {
|
|
|
8131
9222
|
};
|
|
8132
9223
|
var NEVER = INVALID;
|
|
8133
9224
|
|
|
8134
|
-
// ../shared/dist/chunk-
|
|
9225
|
+
// ../shared/dist/chunk-SWQOZWHP.mjs
|
|
9226
|
+
var SAFE_IMAGE_URL_RE = /^(?:https?:\/\/|data:image\/(?:png|jpe?g|gif|webp);base64,)/i;
|
|
9227
|
+
var HAS_SCHEME_RE = /^[\s\u0000-\u001f]*[a-z][a-z0-9+.-]*:/i;
|
|
9228
|
+
function isSafeImageUrl(value) {
|
|
9229
|
+
if (value === null || value === void 0) return true;
|
|
9230
|
+
const trimmed = value.trim();
|
|
9231
|
+
if (trimmed === "") return true;
|
|
9232
|
+
if (!HAS_SCHEME_RE.test(trimmed)) return true;
|
|
9233
|
+
return SAFE_IMAGE_URL_RE.test(trimmed);
|
|
9234
|
+
}
|
|
9235
|
+
|
|
9236
|
+
// ../shared/dist/chunk-NMQNNPSJ.mjs
|
|
8135
9237
|
var locatorSourceSchema = external_exports.enum([
|
|
8136
9238
|
"appilotsId",
|
|
8137
9239
|
"testID",
|
|
@@ -8310,6 +9412,23 @@ var actionDescriptorSchema = external_exports.object({
|
|
|
8310
9412
|
effect: external_exports.enum(["read", "write", "destructive"]).or(external_exports.string()).optional(),
|
|
8311
9413
|
riskLevel: external_exports.enum(["low", "medium", "high"]).or(external_exports.string()).optional(),
|
|
8312
9414
|
requiresConfirmation: external_exports.boolean().optional(),
|
|
9415
|
+
/**
|
|
9416
|
+
* How long this action's work is expected to take, in milliseconds,
|
|
9417
|
+
* DECLARED by the app — not inferred (that is `appilotsInferred`).
|
|
9418
|
+
*
|
|
9419
|
+
* The SDK's post-action wait was a constant: 6s, or 10s when the
|
|
9420
|
+
* generator could prove the handler awaits something. No constant
|
|
9421
|
+
* fits, because app operations run from ~100ms to minutes, and the
|
|
9422
|
+
* failure is silent in both directions — too short and the agent
|
|
9423
|
+
* photographs a loading screen with no controls on it, too long and
|
|
9424
|
+
* every fast press pays for the slowest one. Only the app knows.
|
|
9425
|
+
*
|
|
9426
|
+
* Absent means absent: the SDK keeps its current defaults, so a
|
|
9427
|
+
* document generated before this field existed behaves exactly as it
|
|
9428
|
+
* did. The SDK also clamps the value to its own safety ceiling — a
|
|
9429
|
+
* declared budget is a request, not a licence to hang the session.
|
|
9430
|
+
*/
|
|
9431
|
+
asyncBudgetMs: external_exports.number().int().positive().optional(),
|
|
8313
9432
|
appilotsInferred: appilotsInferredActionSchema.optional()
|
|
8314
9433
|
}).passthrough();
|
|
8315
9434
|
var screenPermissionDescriptorSchema = external_exports.object({
|
|
@@ -8492,7 +9611,7 @@ external_exports.object({
|
|
|
8492
9611
|
version: external_exports.string().default("1.0"),
|
|
8493
9612
|
content: external_exports.record(external_exports.unknown())
|
|
8494
9613
|
});
|
|
8495
|
-
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
|
|
9614
|
+
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator", "publish"]);
|
|
8496
9615
|
var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
|
|
8497
9616
|
external_exports.object({
|
|
8498
9617
|
name: external_exports.string().min(1).max(100),
|
|
@@ -8503,8 +9622,8 @@ external_exports.object({
|
|
|
8503
9622
|
scope: apiKeyScopeSchema.default("sdk"),
|
|
8504
9623
|
environment: apiKeyEnvironmentSchema.optional(),
|
|
8505
9624
|
expiresAt: external_exports.string().datetime().optional()
|
|
8506
|
-
}).refine((v) => v.scope
|
|
8507
|
-
message:
|
|
9625
|
+
}).refine((v) => v.scope === "operator" || !!v.projectId, {
|
|
9626
|
+
message: "projectId is required for SDK and publishing keys",
|
|
8508
9627
|
path: ["projectId"]
|
|
8509
9628
|
});
|
|
8510
9629
|
var boundedString = (max) => external_exports.string().max(max);
|
|
@@ -8529,6 +9648,15 @@ var snapshotInputSchema = external_exports.object({
|
|
|
8529
9648
|
type: boundedString(40).optional(),
|
|
8530
9649
|
required: external_exports.boolean().optional(),
|
|
8531
9650
|
invalid: external_exports.boolean().optional(),
|
|
9651
|
+
/**
|
|
9652
|
+
* Whether the field holds anything, as its own fact rather than an
|
|
9653
|
+
* inference over `value`. The value is the user's and is the first
|
|
9654
|
+
* thing a privacy policy withholds; the existence of a value is the
|
|
9655
|
+
* agent's and is what stops it filling the same field twice.
|
|
9656
|
+
*/
|
|
9657
|
+
filled: external_exports.boolean().optional(),
|
|
9658
|
+
/** This input has keyboard focus right now. */
|
|
9659
|
+
focused: external_exports.boolean().optional(),
|
|
8532
9660
|
inModal: external_exports.boolean().optional()
|
|
8533
9661
|
}).passthrough();
|
|
8534
9662
|
var snapshotButtonSchema = external_exports.object({
|
|
@@ -8684,6 +9812,32 @@ var agentSnapshotSchema = external_exports.object({
|
|
|
8684
9812
|
* it. Optional: older SDKs never clamp and never send it.
|
|
8685
9813
|
*/
|
|
8686
9814
|
truncated: external_exports.boolean().optional(),
|
|
9815
|
+
/**
|
|
9816
|
+
* What moved since the previous observation, computed on the device
|
|
9817
|
+
* because that is the only side holding both snapshots.
|
|
9818
|
+
*
|
|
9819
|
+
* Shape only — counts, booleans, and ids the app declared itself — so
|
|
9820
|
+
* it survives an observation whose content was withheld. ABSENT means
|
|
9821
|
+
* there was no previous observation to compare against; `unchanged:
|
|
9822
|
+
* true` means we compared and nothing moved, which is the strongest
|
|
9823
|
+
* evidence available that an action did nothing. The two must not be
|
|
9824
|
+
* collapsed, for the same reason `truncated` exists.
|
|
9825
|
+
*/
|
|
9826
|
+
delta: external_exports.object({
|
|
9827
|
+
routeChanged: external_exports.boolean().optional(),
|
|
9828
|
+
modalOpened: external_exports.boolean().optional(),
|
|
9829
|
+
modalClosed: external_exports.boolean().optional(),
|
|
9830
|
+
loadingStarted: external_exports.boolean().optional(),
|
|
9831
|
+
loadingFinished: external_exports.boolean().optional(),
|
|
9832
|
+
textsAdded: external_exports.number().int().nonnegative().optional(),
|
|
9833
|
+
textsRemoved: external_exports.number().int().nonnegative().optional(),
|
|
9834
|
+
visibleRowsDelta: external_exports.number().int().optional(),
|
|
9835
|
+
totalRowsDelta: external_exports.number().int().optional(),
|
|
9836
|
+
fieldsNewlyFilled: external_exports.array(boundedString(160)).max(12).optional(),
|
|
9837
|
+
fieldsCleared: external_exports.array(boundedString(160)).max(12).optional(),
|
|
9838
|
+
invalidAppeared: external_exports.boolean().optional(),
|
|
9839
|
+
unchanged: external_exports.boolean().optional()
|
|
9840
|
+
}).passthrough().optional(),
|
|
8687
9841
|
/**
|
|
8688
9842
|
* How many of this screen's controls the client could name, split by
|
|
8689
9843
|
* `identityProvenance`. Diagnostic only — the relay never grounds an
|
|
@@ -8717,7 +9871,19 @@ var agentContextSchema = external_exports.object({
|
|
|
8717
9871
|
rootRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
|
|
8718
9872
|
currentRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
|
|
8719
9873
|
routeNames: external_exports.array(boundedString(200)).max(500).optional(),
|
|
8720
|
-
canGoBack: external_exports.boolean().optional()
|
|
9874
|
+
canGoBack: external_exports.boolean().optional(),
|
|
9875
|
+
/**
|
|
9876
|
+
* The stack a back press pops through, oldest first, ending on
|
|
9877
|
+
* the current screen. `canGoBack` says a back exists; this says
|
|
9878
|
+
* where it goes.
|
|
9879
|
+
*/
|
|
9880
|
+
backStack: external_exports.array(boundedString(200)).max(50).optional(),
|
|
9881
|
+
/**
|
|
9882
|
+
* Screens the session has been on, oldest first. Names only —
|
|
9883
|
+
* params carry record ids and often personal data, and a route
|
|
9884
|
+
* name is structure.
|
|
9885
|
+
*/
|
|
9886
|
+
visited: external_exports.array(boundedString(200)).max(8).optional()
|
|
8721
9887
|
}).passthrough().optional(),
|
|
8722
9888
|
screenMetadata: external_exports.object({
|
|
8723
9889
|
name: boundedString(200).optional(),
|
|
@@ -8823,7 +9989,7 @@ var formFillPayloadSchema = external_exports.object({
|
|
|
8823
9989
|
submitAfterFill: external_exports.boolean().optional()
|
|
8824
9990
|
}).passthrough();
|
|
8825
9991
|
var uiInteractionPayloadSchema = external_exports.object({
|
|
8826
|
-
action: external_exports.enum(["press", "longPress", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
|
|
9992
|
+
action: external_exports.enum(["press", "longPress", "toggle", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
|
|
8827
9993
|
// `targetId` is the canonical server/LLM field. SDK runtimes still
|
|
8828
9994
|
// accept `componentId` as a compatibility alias.
|
|
8829
9995
|
targetId: external_exports.string().min(1),
|
|
@@ -8991,8 +10157,15 @@ external_exports.object({
|
|
|
8991
10157
|
// issue #169 — the observation the server grounds targets against, now
|
|
8992
10158
|
// validated + bounded at the border instead of z.record(z.unknown()).
|
|
8993
10159
|
context: agentContextSchema.optional(),
|
|
8994
|
-
/**
|
|
8995
|
-
|
|
10160
|
+
/**
|
|
10161
|
+
* Hop counter — server enforces a cap to prevent runaway loops.
|
|
10162
|
+
* The max here must stay ABOVE the server's `MAX_AGENT_HOPS` (10 since
|
|
10163
|
+
* #503, apps/api/src/modules/agents/routes.ts): the first over-budget
|
|
10164
|
+
* hop has to get through validation so the route can answer it with
|
|
10165
|
+
* the friendly automation-limit message instead of a 422. The +3
|
|
10166
|
+
* headroom mirrors what 7/10 was before the recalibration.
|
|
10167
|
+
*/
|
|
10168
|
+
hop: external_exports.number().int().min(1).max(13).optional()
|
|
8996
10169
|
});
|
|
8997
10170
|
var agentAccessLevelSchema = external_exports.enum(["read", "write", "none"]);
|
|
8998
10171
|
var screenPermissionSchema = external_exports.object({
|
|
@@ -9098,15 +10271,18 @@ var themeTokensSchema = external_exports.object({
|
|
|
9098
10271
|
*/
|
|
9099
10272
|
mode: external_exports.enum(["auto", "light", "dark"]).optional()
|
|
9100
10273
|
}).strict();
|
|
10274
|
+
var imageSourceField = external_exports.string().max(500).refine(isSafeImageUrl, {
|
|
10275
|
+
message: "Only http(s) URLs, base64 image data URIs, emoji or asset ids are allowed here"
|
|
10276
|
+
});
|
|
9101
10277
|
external_exports.object({
|
|
9102
10278
|
/** Tone / persona instructions appended to the system prompt. */
|
|
9103
10279
|
personaPrompt: external_exports.string().max(2048).nullable(),
|
|
9104
10280
|
/** Display name in the chat header (e.g. "Aria"). */
|
|
9105
10281
|
assistantName: external_exports.string().max(60).nullable(),
|
|
9106
10282
|
/** URL or remote asset id for the avatar shown next to assistant turns. */
|
|
9107
|
-
assistantAvatar:
|
|
10283
|
+
assistantAvatar: imageSourceField.nullable(),
|
|
9108
10284
|
/** Emoji or image URL for the empty-state icon. Auto-detected by prefix. */
|
|
9109
|
-
emptyStateIcon:
|
|
10285
|
+
emptyStateIcon: imageSourceField.nullable(),
|
|
9110
10286
|
/** First-message text shown in the empty state. */
|
|
9111
10287
|
welcomeMessage: external_exports.string().max(500).nullable(),
|
|
9112
10288
|
/** Title rendered at the top of the chat. */
|
|
@@ -9120,20 +10296,20 @@ external_exports.object({
|
|
|
9120
10296
|
/** Chat-open FAB background color. Null falls back to theme.colors.primary. */
|
|
9121
10297
|
triggerButtonColor: hexColorSchema.nullable(),
|
|
9122
10298
|
/** Image URL rendered inside the FAB instead of the default chat-bubble icon. */
|
|
9123
|
-
triggerButtonImageUrl:
|
|
10299
|
+
triggerButtonImageUrl: imageSourceField.nullable()
|
|
9124
10300
|
});
|
|
9125
10301
|
external_exports.object({
|
|
9126
10302
|
personaPrompt: external_exports.string().max(2048).nullable().optional(),
|
|
9127
10303
|
assistantName: external_exports.string().max(60).nullable().optional(),
|
|
9128
|
-
assistantAvatar:
|
|
9129
|
-
emptyStateIcon:
|
|
10304
|
+
assistantAvatar: imageSourceField.nullable().optional(),
|
|
10305
|
+
emptyStateIcon: imageSourceField.nullable().optional(),
|
|
9130
10306
|
welcomeMessage: external_exports.string().max(500).nullable().optional(),
|
|
9131
10307
|
chatTitle: external_exports.string().max(60).nullable().optional(),
|
|
9132
10308
|
poweredByVisible: external_exports.boolean().optional(),
|
|
9133
10309
|
theme: themeTokensSchema.nullable().optional(),
|
|
9134
10310
|
defaultLocale: localeSchema.nullable().optional(),
|
|
9135
10311
|
triggerButtonColor: hexColorSchema.nullable().optional(),
|
|
9136
|
-
triggerButtonImageUrl:
|
|
10312
|
+
triggerButtonImageUrl: imageSourceField.nullable().optional()
|
|
9137
10313
|
}).strict();
|
|
9138
10314
|
var sandboxObservationSchema = external_exports.object({
|
|
9139
10315
|
route: external_exports.string().max(200).optional(),
|
|
@@ -9153,7 +10329,19 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
9153
10329
|
type: external_exports.string().max(40).optional(),
|
|
9154
10330
|
placeholder: external_exports.string().max(200).optional(),
|
|
9155
10331
|
/** Inline validation state (SDK snapshot field). */
|
|
9156
|
-
invalid: external_exports.boolean().optional()
|
|
10332
|
+
invalid: external_exports.boolean().optional(),
|
|
10333
|
+
/**
|
|
10334
|
+
* Whether the field holds anything, as its own fact rather than
|
|
10335
|
+
* an inference over `value`.
|
|
10336
|
+
*
|
|
10337
|
+
* Here because the eval posts through this schema: a fact the
|
|
10338
|
+
* SDK produces and this contract has no word for is a fact the
|
|
10339
|
+
* corpus can never grade the agent on. `invalid` and `required`
|
|
10340
|
+
* were already here; these two were the half that was missing.
|
|
10341
|
+
*/
|
|
10342
|
+
filled: external_exports.boolean().optional(),
|
|
10343
|
+
/** This input has keyboard focus right now. */
|
|
10344
|
+
focused: external_exports.boolean().optional()
|
|
9157
10345
|
})
|
|
9158
10346
|
).max(100).optional(),
|
|
9159
10347
|
buttons: external_exports.array(
|
|
@@ -9632,7 +10820,7 @@ var manifestSchema = external_exports.object({
|
|
|
9632
10820
|
navigation: navigationGraphSchema.partial().optional()
|
|
9633
10821
|
}).passthrough();
|
|
9634
10822
|
async function loadManifest(rootDir, manifestPath) {
|
|
9635
|
-
const resolvedPath =
|
|
10823
|
+
const resolvedPath = path2__namespace.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
|
|
9636
10824
|
let raw;
|
|
9637
10825
|
try {
|
|
9638
10826
|
raw = await fs.readFile(resolvedPath, "utf-8");
|
|
@@ -9715,6 +10903,12 @@ function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
|
|
|
9715
10903
|
};
|
|
9716
10904
|
}
|
|
9717
10905
|
|
|
10906
|
+
// src/generators/checksum.ts
|
|
10907
|
+
function serializeForChecksum(document) {
|
|
10908
|
+
const { generatedAt: _generatedAt, ...content } = document;
|
|
10909
|
+
return JSON.stringify(content, null, 2);
|
|
10910
|
+
}
|
|
10911
|
+
|
|
9718
10912
|
// src/pipeline/enrichment.ts
|
|
9719
10913
|
function enrichScreenForAgent(screen) {
|
|
9720
10914
|
const targets = mergeTargets([
|
|
@@ -9791,7 +10985,10 @@ function mergeTargets(targets) {
|
|
|
9791
10985
|
for (const target of targets) {
|
|
9792
10986
|
if (!target.id) continue;
|
|
9793
10987
|
const existing = byId.get(target.id);
|
|
9794
|
-
byId.set(
|
|
10988
|
+
byId.set(
|
|
10989
|
+
target.id,
|
|
10990
|
+
existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target
|
|
10991
|
+
);
|
|
9795
10992
|
}
|
|
9796
10993
|
return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
9797
10994
|
}
|
|
@@ -9836,7 +11033,10 @@ function synthesizeFlows(screen, targets) {
|
|
|
9836
11033
|
intent: "destructive_action",
|
|
9837
11034
|
steps: [
|
|
9838
11035
|
{ type: "press", target: action.id, label: action.label },
|
|
9839
|
-
{
|
|
11036
|
+
{
|
|
11037
|
+
type: "confirm",
|
|
11038
|
+
description: "Wait for native or custom confirmation before continuing"
|
|
11039
|
+
},
|
|
9840
11040
|
{ type: "wait", description: describeWait(action) }
|
|
9841
11041
|
],
|
|
9842
11042
|
waitPolicy: waitPolicyForAction(action),
|
|
@@ -9851,10 +11051,19 @@ function synthesizeFlows(screen, targets) {
|
|
|
9851
11051
|
title: `Act on an item in ${collection.id}`,
|
|
9852
11052
|
intent: "list_action",
|
|
9853
11053
|
steps: [
|
|
9854
|
-
{
|
|
9855
|
-
|
|
11054
|
+
{
|
|
11055
|
+
type: "choose-list-item",
|
|
11056
|
+
target: collection.id,
|
|
11057
|
+
description: "Resolve the user reference to a visible or searchable row"
|
|
11058
|
+
},
|
|
11059
|
+
{
|
|
11060
|
+
type: "press",
|
|
11061
|
+
description: collection.rowAction?.description ?? "Open the row action"
|
|
11062
|
+
}
|
|
9856
11063
|
],
|
|
9857
|
-
waitPolicy: {
|
|
11064
|
+
waitPolicy: {
|
|
11065
|
+
expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback"
|
|
11066
|
+
}
|
|
9858
11067
|
});
|
|
9859
11068
|
}
|
|
9860
11069
|
}
|
|
@@ -9862,25 +11071,32 @@ function synthesizeFlows(screen, targets) {
|
|
|
9862
11071
|
}
|
|
9863
11072
|
function waitPolicyForAction(action) {
|
|
9864
11073
|
const expectedOutcome = action.successSignal?.type === "goBack" ? "goBack" : action.appilotsInferred?.expectedOutcome ?? (action.targetScreen ? "navigation" : void 0);
|
|
9865
|
-
const signals = [action.successSignal, action.failureSignal].filter(
|
|
11074
|
+
const signals = [action.successSignal, action.failureSignal].filter(
|
|
11075
|
+
Boolean
|
|
11076
|
+
);
|
|
11077
|
+
const maxMs = action.asyncBudgetMs ?? (action.appilotsInferred?.isAsyncTrigger ? 1e4 : void 0);
|
|
9866
11078
|
return {
|
|
9867
11079
|
...expectedOutcome ? { expectedOutcome } : {},
|
|
9868
11080
|
...signals && signals.length > 0 ? { signals } : {},
|
|
9869
|
-
...
|
|
11081
|
+
...maxMs !== void 0 ? { maxMs } : {}
|
|
9870
11082
|
};
|
|
9871
11083
|
}
|
|
9872
11084
|
function describeWait(action) {
|
|
9873
11085
|
if (action.successSignal?.description) return action.successSignal.description;
|
|
9874
|
-
if (action.successSignal?.type === "goBack")
|
|
11086
|
+
if (action.successSignal?.type === "goBack")
|
|
11087
|
+
return "Wait for the app to return to the previous screen";
|
|
9875
11088
|
if (action.targetScreen) return `Wait for navigation to ${action.targetScreen}`;
|
|
9876
|
-
if (action.appilotsInferred?.expectedOutcome)
|
|
11089
|
+
if (action.appilotsInferred?.expectedOutcome)
|
|
11090
|
+
return `Wait for ${action.appilotsInferred.expectedOutcome}`;
|
|
9877
11091
|
return "Wait for the UI to settle";
|
|
9878
11092
|
}
|
|
9879
11093
|
function synthesizeAgentHints(screen, targets, flows) {
|
|
9880
11094
|
const preferredTargets = targets.filter((target) => ["submit", "button", "list"].includes(target.role)).slice(0, 8).map((target) => target.id);
|
|
9881
11095
|
const commonTasks = flows.slice(0, 6).map((flow) => flow.title);
|
|
9882
11096
|
const safetyNotes = screen.actions.filter((action) => action.destructive || action.requiresConfirmation).map((action) => `${action.id} requires confirmation`);
|
|
9883
|
-
const firstAsyncAction = screen.actions.find(
|
|
11097
|
+
const firstAsyncAction = screen.actions.find(
|
|
11098
|
+
(action) => action.appilotsInferred?.isAsyncTrigger || action.asyncBudgetMs !== void 0
|
|
11099
|
+
);
|
|
9884
11100
|
const hints = {
|
|
9885
11101
|
primaryGoal: synthesizePrimaryGoal(screen),
|
|
9886
11102
|
commonTasks,
|
|
@@ -9907,7 +11123,9 @@ function synthesizePrimaryGoal(screen) {
|
|
|
9907
11123
|
}
|
|
9908
11124
|
const fieldCount = uniqueFields.size;
|
|
9909
11125
|
if (fieldCount > 0) {
|
|
9910
|
-
const requiredCount = Array.from(uniqueFields.values()).filter(
|
|
11126
|
+
const requiredCount = Array.from(uniqueFields.values()).filter(
|
|
11127
|
+
(field) => field.required
|
|
11128
|
+
).length;
|
|
9911
11129
|
const fieldLabel = fieldCount === 1 ? "field" : "fields";
|
|
9912
11130
|
goals.push(
|
|
9913
11131
|
requiredCount > 0 ? `Complete a form with ${fieldCount} ${fieldLabel} (${requiredCount} required)` : `Complete a form with ${fieldCount} ${fieldLabel}`
|
|
@@ -9921,7 +11139,9 @@ function synthesizePrimaryGoal(screen) {
|
|
|
9921
11139
|
if (submitCount > 0) {
|
|
9922
11140
|
goals.push(`Submit ${submitCount === 1 ? "the primary form" : `${submitCount} forms/actions`}`);
|
|
9923
11141
|
}
|
|
9924
|
-
const asyncCount = screen.actions.filter(
|
|
11142
|
+
const asyncCount = screen.actions.filter(
|
|
11143
|
+
(action) => action.appilotsInferred?.isAsyncTrigger
|
|
11144
|
+
).length;
|
|
9925
11145
|
if (asyncCount > 0) {
|
|
9926
11146
|
goals.push(`Wait for ${asyncCount === 1 ? "async feedback" : "async action feedback"}`);
|
|
9927
11147
|
}
|
|
@@ -9996,7 +11216,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
9996
11216
|
const agentReadyScreens = mergedScreens.map(
|
|
9997
11217
|
(screen) => enrichScreenForAgent({
|
|
9998
11218
|
...screen,
|
|
9999
|
-
filePath: screen.filePath ?
|
|
11219
|
+
filePath: screen.filePath ? path2__namespace.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
|
|
10000
11220
|
})
|
|
10001
11221
|
);
|
|
10002
11222
|
const projectInfo = await this.getProjectInfo();
|
|
@@ -10020,11 +11240,11 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10020
11240
|
}
|
|
10021
11241
|
};
|
|
10022
11242
|
const serialized = JSON.stringify(document, null, 2);
|
|
10023
|
-
const checksum = this.calculateChecksum(
|
|
10024
|
-
const filePath =
|
|
11243
|
+
const checksum = this.calculateChecksum(serializeForChecksum(document));
|
|
11244
|
+
const filePath = path2__namespace.default.resolve(outputDir, `mcp-document.${this.options.format}`);
|
|
10025
11245
|
await fs.writeFile(filePath, serialized, "utf-8");
|
|
10026
11246
|
console.log(`[MCPGenerator] Document written to: ${filePath}`);
|
|
10027
|
-
const checksumFilePath =
|
|
11247
|
+
const checksumFilePath = path2__namespace.default.resolve(outputDir, ".appilots-checksum");
|
|
10028
11248
|
await fs.writeFile(checksumFilePath, checksum, "utf-8");
|
|
10029
11249
|
console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
|
|
10030
11250
|
console.log("[MCPGenerator] Generation complete!");
|
|
@@ -10032,7 +11252,8 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10032
11252
|
document,
|
|
10033
11253
|
filePath,
|
|
10034
11254
|
format: this.options.format,
|
|
10035
|
-
checksum
|
|
11255
|
+
checksum,
|
|
11256
|
+
...analyzed.diagnostics ? { diagnostics: analyzed.diagnostics } : {}
|
|
10036
11257
|
};
|
|
10037
11258
|
}
|
|
10038
11259
|
/**
|
|
@@ -10044,7 +11265,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10044
11265
|
* after calling `generate()`.
|
|
10045
11266
|
*/
|
|
10046
11267
|
static async readPreviousChecksum(outputDir) {
|
|
10047
|
-
const checksumFilePath =
|
|
11268
|
+
const checksumFilePath = path2__namespace.default.resolve(outputDir, ".appilots-checksum");
|
|
10048
11269
|
try {
|
|
10049
11270
|
const content = await fs.readFile(checksumFilePath, "utf-8");
|
|
10050
11271
|
return content.trim() || null;
|
|
@@ -10063,7 +11284,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10063
11284
|
*/
|
|
10064
11285
|
async getProjectInfo() {
|
|
10065
11286
|
try {
|
|
10066
|
-
const packageJsonPath =
|
|
11287
|
+
const packageJsonPath = path2__namespace.default.resolve(this.analyzerConfig.rootDir, "package.json");
|
|
10067
11288
|
const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
|
|
10068
11289
|
const packageJson = JSON.parse(packageJsonContent);
|
|
10069
11290
|
return {
|
|
@@ -10146,12 +11367,12 @@ function getEnvOverrides(env = process.env) {
|
|
|
10146
11367
|
return trimmed ? trimmed : void 0;
|
|
10147
11368
|
};
|
|
10148
11369
|
return {
|
|
10149
|
-
apiKey: clean(env.APPILOTS_API_KEY),
|
|
11370
|
+
apiKey: clean(env.APPILOTS_PUBLISH_KEY) ?? clean(env.APPILOTS_API_KEY),
|
|
10150
11371
|
projectId: clean(env.APPILOTS_PROJECT_ID),
|
|
10151
11372
|
serverUrl: clean(env.APPILOTS_SERVER_URL)
|
|
10152
11373
|
};
|
|
10153
11374
|
}
|
|
10154
|
-
function loadConfig(onWarn) {
|
|
11375
|
+
function loadConfig(onWarn, options = {}) {
|
|
10155
11376
|
const configPath = getConfigPath();
|
|
10156
11377
|
const env = getEnvOverrides();
|
|
10157
11378
|
let fileConfig = {};
|
|
@@ -10190,6 +11411,10 @@ function loadConfig(onWarn) {
|
|
|
10190
11411
|
};
|
|
10191
11412
|
const validation = validateConfig(merged);
|
|
10192
11413
|
validation.warnings.unshift(...legacyWarnings);
|
|
11414
|
+
if (options.requireApiKey === false) {
|
|
11415
|
+
validation.errors = validation.errors.filter((e) => !e.startsWith("apiKey"));
|
|
11416
|
+
validation.valid = validation.errors.length === 0;
|
|
11417
|
+
}
|
|
10193
11418
|
if (fileConfig.serverUrl === void 0 && env.serverUrl === void 0) {
|
|
10194
11419
|
for (const key of Object.keys(fileConfig)) {
|
|
10195
11420
|
if (KNOWN_CONFIG_KEYS.includes(key)) continue;
|
|
@@ -10219,7 +11444,7 @@ function loadConfig(onWarn) {
|
|
|
10219
11444
|
return merged;
|
|
10220
11445
|
}
|
|
10221
11446
|
function saveConfig(dir, config) {
|
|
10222
|
-
const configPath =
|
|
11447
|
+
const configPath = path2.join(dir, ".appilotsrc");
|
|
10223
11448
|
const existingConfig = getConfigPath() ? loadConfig() : null;
|
|
10224
11449
|
const mergedConfig = {
|
|
10225
11450
|
apiKey: config.apiKey || existingConfig?.apiKey || "",
|
|
@@ -10246,19 +11471,19 @@ function saveConfig(dir, config) {
|
|
|
10246
11471
|
}
|
|
10247
11472
|
}
|
|
10248
11473
|
function getConfigPath() {
|
|
10249
|
-
let currentDir =
|
|
10250
|
-
const root =
|
|
11474
|
+
let currentDir = path2.resolve(process.cwd());
|
|
11475
|
+
const root = path2.resolve("/");
|
|
10251
11476
|
while (currentDir !== root) {
|
|
10252
|
-
const configPath =
|
|
11477
|
+
const configPath = path2.join(currentDir, ".appilotsrc");
|
|
10253
11478
|
try {
|
|
10254
11479
|
if (fs$1.existsSync(configPath) && fs$1.statSync(configPath).isFile()) {
|
|
10255
11480
|
return configPath;
|
|
10256
11481
|
}
|
|
10257
11482
|
} catch {
|
|
10258
11483
|
}
|
|
10259
|
-
currentDir =
|
|
11484
|
+
currentDir = path2.resolve(currentDir, "..");
|
|
10260
11485
|
}
|
|
10261
|
-
const rootConfigPath =
|
|
11486
|
+
const rootConfigPath = path2.join(root, ".appilotsrc");
|
|
10262
11487
|
try {
|
|
10263
11488
|
if (fs$1.existsSync(rootConfigPath) && fs$1.statSync(rootConfigPath).isFile()) {
|
|
10264
11489
|
return rootConfigPath;
|
|
@@ -10622,7 +11847,7 @@ var AppilotsAPIClient = class {
|
|
|
10622
11847
|
};
|
|
10623
11848
|
|
|
10624
11849
|
// src/version.ts
|
|
10625
|
-
var CLI_VERSION = "0.11.
|
|
11850
|
+
var CLI_VERSION = "0.11.3";
|
|
10626
11851
|
|
|
10627
11852
|
exports.AppilotsAPIClient = AppilotsAPIClient;
|
|
10628
11853
|
exports.CLI_VERSION = CLI_VERSION;
|