@appilots/cli 0.10.0 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1791 -422
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +279 -5
- package/dist/index.d.ts +279 -5
- package/dist/index.js +1455 -197
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1449 -191
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import fs, { readFile, mkdir, writeFile } from 'fs/promises';
|
|
2
|
-
import * as
|
|
3
|
-
import
|
|
2
|
+
import * as path2 from 'path';
|
|
3
|
+
import path2__default, { join, resolve } from 'path';
|
|
4
4
|
import traverse4 from '@babel/traverse';
|
|
5
5
|
import * as BabelTypes from '@babel/types';
|
|
6
|
-
import
|
|
6
|
+
import fastGlob from 'fast-glob';
|
|
7
7
|
import * as parser from '@babel/parser';
|
|
8
8
|
import { parse } from '@babel/parser';
|
|
9
|
-
import { promises, readFileSync, writeFileSync, existsSync, statSync } from 'fs';
|
|
9
|
+
import { promises, readFileSync, writeFileSync, existsSync, statSync, readdirSync } from 'fs';
|
|
10
10
|
import { createHash } from 'crypto';
|
|
11
11
|
|
|
12
12
|
var __defProp = Object.defineProperty;
|
|
@@ -14,6 +14,13 @@ var __export = (target, all) => {
|
|
|
14
14
|
for (var name in all)
|
|
15
15
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
16
|
};
|
|
17
|
+
function byCodeUnit(a, b) {
|
|
18
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
19
|
+
}
|
|
20
|
+
async function globSorted(patterns, options) {
|
|
21
|
+
const files = await fastGlob(patterns, options);
|
|
22
|
+
return files.sort(byCodeUnit);
|
|
23
|
+
}
|
|
17
24
|
var DEFAULT_PARSER_PLUGINS = [
|
|
18
25
|
"jsx",
|
|
19
26
|
"typescript",
|
|
@@ -398,14 +405,25 @@ var ScreenAnalyzer = class {
|
|
|
398
405
|
strictScreens;
|
|
399
406
|
/** Glob patterns that identify screen files in strict mode */
|
|
400
407
|
screenPatterns;
|
|
408
|
+
/**
|
|
409
|
+
* Arquivos que uma ROTA monta (`component={…}` resolvido pelo
|
|
410
|
+
* `NavigationAnalyzer`). Passam pelo filtro estrito sem depender de
|
|
411
|
+
* convenção de nome, porque um componente que uma rota monta É uma tela por
|
|
412
|
+
* definição — não é heurística, é o que o app declarou.
|
|
413
|
+
*
|
|
414
|
+
* É isto que resolve o caso `rocketchat`, cujas telas se chamam `*View.tsx`
|
|
415
|
+
* em `app/views/`, e o `coopcycle`, que não tem diretório `screens/` nenhum.
|
|
416
|
+
*/
|
|
417
|
+
routeTargetFiles;
|
|
401
418
|
/** §D: Count of screens filtered out in strict mode (available after analyze()) */
|
|
402
419
|
screensFilteredOut = 0;
|
|
403
420
|
constructor(config, options) {
|
|
404
421
|
this.config = config;
|
|
422
|
+
this.routeTargetFiles = options?.routeTargetFiles ?? /* @__PURE__ */ new Set();
|
|
405
423
|
this.strictScreens = options?.strictScreens ?? false;
|
|
406
424
|
this.screenPatterns = options?.screenPatterns ?? [
|
|
407
|
-
"**/*Screen.{ts,tsx}",
|
|
408
|
-
"**/screens/**/*.{ts,tsx}"
|
|
425
|
+
"**/*Screen.{ts,tsx,js,jsx}",
|
|
426
|
+
"**/screens/**/*.{ts,tsx,js,jsx}"
|
|
409
427
|
];
|
|
410
428
|
}
|
|
411
429
|
/** Analyze all screens in the project */
|
|
@@ -417,7 +435,7 @@ var ScreenAnalyzer = class {
|
|
|
417
435
|
console.log(`[ScreenAnalyzer] Strict mode ON \u2014 screen patterns:`, this.screenPatterns);
|
|
418
436
|
}
|
|
419
437
|
}
|
|
420
|
-
const files = await
|
|
438
|
+
const files = await globSorted(include, {
|
|
421
439
|
cwd: this.config.rootDir,
|
|
422
440
|
ignore: exclude
|
|
423
441
|
});
|
|
@@ -426,23 +444,24 @@ var ScreenAnalyzer = class {
|
|
|
426
444
|
}
|
|
427
445
|
let screenPatternFiles = null;
|
|
428
446
|
if (this.strictScreens) {
|
|
429
|
-
const matched = await
|
|
447
|
+
const matched = await globSorted(this.screenPatterns, {
|
|
430
448
|
cwd: this.config.rootDir,
|
|
431
449
|
ignore: exclude
|
|
432
450
|
});
|
|
433
|
-
screenPatternFiles = new Set(matched.map((f) =>
|
|
451
|
+
screenPatternFiles = new Set(matched.map((f) => path2__default.resolve(this.config.rootDir, f)));
|
|
434
452
|
}
|
|
435
453
|
const screens = [];
|
|
436
454
|
this.screensFilteredOut = 0;
|
|
437
455
|
for (const file of files) {
|
|
438
|
-
const filePath =
|
|
456
|
+
const filePath = path2__default.resolve(this.config.rootDir, file);
|
|
439
457
|
try {
|
|
440
458
|
const descriptor = await this.analyzeFile(filePath);
|
|
441
459
|
if (!descriptor) continue;
|
|
442
460
|
if (this.strictScreens) {
|
|
443
461
|
const hasRegisterScreen = descriptor.__hasRegisterScreen === true;
|
|
444
462
|
const matchesPattern = screenPatternFiles?.has(filePath) ?? false;
|
|
445
|
-
|
|
463
|
+
const isRouteTarget = this.routeTargetFiles.has(filePath);
|
|
464
|
+
if (!hasRegisterScreen && !matchesPattern && !isRouteTarget) {
|
|
446
465
|
this.screensFilteredOut++;
|
|
447
466
|
if (this.verbose) {
|
|
448
467
|
console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file}`);
|
|
@@ -650,6 +669,8 @@ var ScreenAnalyzer = class {
|
|
|
650
669
|
action.riskLevel = value.value;
|
|
651
670
|
} else if (key === "nativeConfirmationExpected" && BabelTypes.isBooleanLiteral(value)) {
|
|
652
671
|
action.nativeConfirmationExpected = value.value;
|
|
672
|
+
} else if (key === "asyncBudgetMs" && BabelTypes.isNumericLiteral(value) && Number.isInteger(value.value) && value.value > 0) {
|
|
673
|
+
action.asyncBudgetMs = value.value;
|
|
653
674
|
} else if (key === "appilotsInferred" && BabelTypes.isObjectExpression(value)) {
|
|
654
675
|
action.appilotsInferred = this.parseAppilotsInferredObject(value);
|
|
655
676
|
}
|
|
@@ -1629,43 +1650,871 @@ var ScreenAnalyzer = class {
|
|
|
1629
1650
|
* E.g., /src/screens/ItemListScreen.tsx -> ItemListScreen
|
|
1630
1651
|
*/
|
|
1631
1652
|
extractScreenName(filePath) {
|
|
1632
|
-
const basename2 =
|
|
1653
|
+
const basename2 = path2__default.basename(filePath);
|
|
1633
1654
|
return basename2.replace(/\.(tsx?|jsx?)$/, "");
|
|
1634
1655
|
}
|
|
1635
1656
|
};
|
|
1657
|
+
var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
|
|
1658
|
+
var MAX_HOPS = 8;
|
|
1659
|
+
var ModuleGraph = class {
|
|
1660
|
+
asts = /* @__PURE__ */ new Map();
|
|
1661
|
+
resolved = /* @__PURE__ */ new Map();
|
|
1662
|
+
/** `@src/*` → `<root>/src/*`, lido do tsconfig do app. */
|
|
1663
|
+
aliases;
|
|
1664
|
+
/** `uniswap` → `<repo>/packages/uniswap`, lido do workspace do monorepo. */
|
|
1665
|
+
workspacePackages;
|
|
1666
|
+
constructor(rootDir) {
|
|
1667
|
+
this.aliases = rootDir ? readTsconfigAliases(rootDir) : [];
|
|
1668
|
+
this.workspacePackages = rootDir ? readWorkspacePackages(rootDir) : [];
|
|
1669
|
+
}
|
|
1670
|
+
/** AST de um arquivo, memoizada. `null` quando não parseia. */
|
|
1671
|
+
parse(file) {
|
|
1672
|
+
const cached = this.asts.get(file);
|
|
1673
|
+
if (cached !== void 0) return cached;
|
|
1674
|
+
let ast = null;
|
|
1675
|
+
try {
|
|
1676
|
+
ast = parser.parse(readFileSync(file, "utf-8"), {
|
|
1677
|
+
sourceType: "module",
|
|
1678
|
+
plugins: ["jsx", "typescript"]
|
|
1679
|
+
});
|
|
1680
|
+
} catch {
|
|
1681
|
+
ast = null;
|
|
1682
|
+
}
|
|
1683
|
+
this.asts.set(file, ast);
|
|
1684
|
+
return ast;
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* `./account/Home` a partir de `src/navigation/index.tsx` → caminho absoluto.
|
|
1688
|
+
* Só resolve caminho relativo: import de pacote (`@react-navigation/native`)
|
|
1689
|
+
* é de terceiro e não tem tela nossa dentro.
|
|
1690
|
+
*/
|
|
1691
|
+
resolve(fromFile, spec) {
|
|
1692
|
+
const key = `${fromFile} ${spec}`;
|
|
1693
|
+
const cached = this.resolved.get(key);
|
|
1694
|
+
if (cached !== void 0) return cached;
|
|
1695
|
+
let base = null;
|
|
1696
|
+
if (spec.startsWith(".")) {
|
|
1697
|
+
base = path2__default.resolve(path2__default.dirname(fromFile), spec);
|
|
1698
|
+
} else {
|
|
1699
|
+
for (const { prefix, target } of this.aliases) {
|
|
1700
|
+
if (spec === prefix || spec.startsWith(prefix + "/")) {
|
|
1701
|
+
base = path2__default.join(target, spec.slice(prefix.length));
|
|
1702
|
+
break;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
if (!base) {
|
|
1706
|
+
for (const pkg of this.workspacePackages) {
|
|
1707
|
+
if (spec === pkg.name || spec.startsWith(pkg.name + "/")) {
|
|
1708
|
+
base = path2__default.join(pkg.dir, spec.slice(pkg.name.length));
|
|
1709
|
+
break;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
if (!base) {
|
|
1715
|
+
this.resolved.set(key, null);
|
|
1716
|
+
return null;
|
|
1717
|
+
}
|
|
1718
|
+
const candidates = [
|
|
1719
|
+
base,
|
|
1720
|
+
...EXTENSIONS.map((e) => base + e),
|
|
1721
|
+
...EXTENSIONS.map((e) => path2__default.join(base, "index" + e))
|
|
1722
|
+
];
|
|
1723
|
+
let found = null;
|
|
1724
|
+
for (const c of candidates) {
|
|
1725
|
+
try {
|
|
1726
|
+
if (existsSync(c) && statSync(c).isFile()) {
|
|
1727
|
+
found = c;
|
|
1728
|
+
break;
|
|
1729
|
+
}
|
|
1730
|
+
} catch {
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
this.resolved.set(key, found);
|
|
1734
|
+
return found;
|
|
1735
|
+
}
|
|
1736
|
+
/** `import`s do arquivo, por nome local. */
|
|
1737
|
+
imports(file) {
|
|
1738
|
+
const out = /* @__PURE__ */ new Map();
|
|
1739
|
+
const ast = this.parse(file);
|
|
1740
|
+
if (!ast) return out;
|
|
1741
|
+
for (const stmt of ast.program.body) {
|
|
1742
|
+
if (!BabelTypes.isImportDeclaration(stmt)) continue;
|
|
1743
|
+
const source = stmt.source.value;
|
|
1744
|
+
for (const spec of stmt.specifiers) {
|
|
1745
|
+
if (BabelTypes.isImportDefaultSpecifier(spec)) {
|
|
1746
|
+
out.set(spec.local.name, { source, imported: "default" });
|
|
1747
|
+
} else if (BabelTypes.isImportNamespaceSpecifier(spec)) {
|
|
1748
|
+
out.set(spec.local.name, { source, imported: "*" });
|
|
1749
|
+
} else if (BabelTypes.isImportSpecifier(spec)) {
|
|
1750
|
+
const imported = BabelTypes.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
|
|
1751
|
+
out.set(spec.local.name, { source, imported });
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
return out;
|
|
1756
|
+
}
|
|
1757
|
+
/** `const X = <init>` no topo do arquivo, incluindo `export const`. */
|
|
1758
|
+
topLevelInit(file, name) {
|
|
1759
|
+
const ast = this.parse(file);
|
|
1760
|
+
if (!ast) return null;
|
|
1761
|
+
for (const stmt of ast.program.body) {
|
|
1762
|
+
const decl = BabelTypes.isExportNamedDeclaration(stmt) ? stmt.declaration : stmt;
|
|
1763
|
+
if (!BabelTypes.isVariableDeclaration(decl)) continue;
|
|
1764
|
+
for (const d of decl.declarations) {
|
|
1765
|
+
if (BabelTypes.isIdentifier(d.id) && d.id.name === name && d.init) {
|
|
1766
|
+
return BabelTypes.isTSAsExpression(d.init) ? d.init.expression : d.init;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
return null;
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Onde `name` é DEFINIDO — segue import e reexport de barrel.
|
|
1774
|
+
* Devolve o arquivo e o nome sob o qual ele é definido lá.
|
|
1775
|
+
*/
|
|
1776
|
+
resolveBinding(file, name, hops = 0) {
|
|
1777
|
+
if (hops > MAX_HOPS) return null;
|
|
1778
|
+
if (this.topLevelInit(file, name) !== null) return { file, name };
|
|
1779
|
+
const binding = this.imports(file).get(name);
|
|
1780
|
+
if (binding) {
|
|
1781
|
+
const target = this.resolve(file, binding.source);
|
|
1782
|
+
if (!target) return null;
|
|
1783
|
+
const next = binding.imported === "default" || binding.imported === "*" ? name : binding.imported;
|
|
1784
|
+
const deeper = this.resolveBinding(target, next, hops + 1);
|
|
1785
|
+
return deeper ?? { file: target, name: next };
|
|
1786
|
+
}
|
|
1787
|
+
const ast = this.parse(file);
|
|
1788
|
+
if (ast) {
|
|
1789
|
+
for (const stmt of ast.program.body) {
|
|
1790
|
+
if (!BabelTypes.isExportNamedDeclaration(stmt) || !stmt.source) continue;
|
|
1791
|
+
for (const spec of stmt.specifiers) {
|
|
1792
|
+
if (!BabelTypes.isExportSpecifier(spec)) continue;
|
|
1793
|
+
const exported = BabelTypes.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value;
|
|
1794
|
+
if (exported !== name) continue;
|
|
1795
|
+
const target = this.resolve(file, stmt.source.value);
|
|
1796
|
+
if (!target) return null;
|
|
1797
|
+
const local = spec.local.name;
|
|
1798
|
+
return this.resolveBinding(target, local, hops + 1) ?? { file: target, name: local };
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
if (ast) {
|
|
1803
|
+
for (const stmt of ast.program.body) {
|
|
1804
|
+
if (!BabelTypes.isExportAllDeclaration(stmt)) continue;
|
|
1805
|
+
const target = this.resolve(file, stmt.source.value);
|
|
1806
|
+
if (!target || target === file) continue;
|
|
1807
|
+
const deeper = this.resolveBinding(target, name, hops + 1);
|
|
1808
|
+
if (deeper) return deeper;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (ast) {
|
|
1812
|
+
for (const stmt of ast.program.body) {
|
|
1813
|
+
if (!BabelTypes.isExportDefaultDeclaration(stmt)) continue;
|
|
1814
|
+
if (BabelTypes.isIdentifier(stmt.declaration)) {
|
|
1815
|
+
const local = stmt.declaration.name;
|
|
1816
|
+
if (local === name) return null;
|
|
1817
|
+
return this.resolveBinding(file, local, hops + 1) ?? { file, name: local };
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return null;
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* O valor string de uma expressão de nome de rota, ou `null`.
|
|
1825
|
+
*
|
|
1826
|
+
* Cobre `"Chat"`, `ROUTES.CHAT`, `ROUTES.ONBOARDING.SPLASH` e `SOME_CONST` —
|
|
1827
|
+
* seguindo import quando o objeto vem de outro arquivo. NÃO cobre template
|
|
1828
|
+
* com interpolação nem valor calculado, de propósito.
|
|
1829
|
+
*/
|
|
1830
|
+
stringConstant(file, node, hops = 0) {
|
|
1831
|
+
if (!node || hops > MAX_HOPS) return null;
|
|
1832
|
+
if (BabelTypes.isStringLiteral(node)) return node.value;
|
|
1833
|
+
if (BabelTypes.isTemplateLiteral(node)) {
|
|
1834
|
+
return node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? null : null;
|
|
1835
|
+
}
|
|
1836
|
+
if (BabelTypes.isTSAsExpression(node)) return this.stringConstant(file, node.expression, hops + 1);
|
|
1837
|
+
const chain = memberChain(node);
|
|
1838
|
+
if (!chain) return null;
|
|
1839
|
+
const origin = this.resolveBinding(file, chain.root);
|
|
1840
|
+
if (!origin) return null;
|
|
1841
|
+
let current = this.topLevelInit(origin.file, origin.name);
|
|
1842
|
+
if (!current) return null;
|
|
1843
|
+
for (const key of chain.path) {
|
|
1844
|
+
if (!BabelTypes.isObjectExpression(current)) return null;
|
|
1845
|
+
const prop = objectProperty(current, key);
|
|
1846
|
+
if (!prop) return null;
|
|
1847
|
+
current = BabelTypes.isTSAsExpression(prop) ? prop.expression : prop;
|
|
1848
|
+
}
|
|
1849
|
+
return BabelTypes.isStringLiteral(current) ? current.value : null;
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* O ARQUIVO onde vive o componente de uma rota, ou `null`.
|
|
1853
|
+
*
|
|
1854
|
+
* Aceita as três formas que o corpus mostrou: identificador
|
|
1855
|
+
* (`component={Home}`), membro de barrel (`component={screens.AccountHome}`)
|
|
1856
|
+
* e componente embrulhado em HOC (`component={gestureHandlerRootHOC(Chat)}`,
|
|
1857
|
+
* que é como o `pocketpal` monta todas as telas do Drawer).
|
|
1858
|
+
*/
|
|
1859
|
+
componentFile(file, node, hops = 0) {
|
|
1860
|
+
if (!node || hops > MAX_HOPS) return null;
|
|
1861
|
+
if (BabelTypes.isCallExpression(node)) {
|
|
1862
|
+
for (const arg of node.arguments) {
|
|
1863
|
+
if (BabelTypes.isIdentifier(arg) || BabelTypes.isMemberExpression(arg)) {
|
|
1864
|
+
const inner = this.componentFile(file, arg, hops + 1);
|
|
1865
|
+
if (inner) return inner;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
return null;
|
|
1869
|
+
}
|
|
1870
|
+
if (BabelTypes.isTSAsExpression(node)) return this.componentFile(file, node.expression, hops + 1);
|
|
1871
|
+
const chain = memberChain(node);
|
|
1872
|
+
if (!chain) return null;
|
|
1873
|
+
const origin = this.resolveBinding(file, chain.root);
|
|
1874
|
+
if (!origin) return null;
|
|
1875
|
+
if (chain.path.length === 0) return origin.file;
|
|
1876
|
+
const init = this.topLevelInit(origin.file, origin.name);
|
|
1877
|
+
if (init && BabelTypes.isObjectExpression(init)) {
|
|
1878
|
+
const prop = objectProperty(init, chain.path[0]);
|
|
1879
|
+
if (prop) return this.componentFile(origin.file, prop, hops + 1);
|
|
1880
|
+
}
|
|
1881
|
+
const viaExport = this.resolveBinding(origin.file, chain.path[0], hops + 1);
|
|
1882
|
+
return viaExport?.file ?? null;
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Os arquivos DO PROJETO que este arquivo renderiza como JSX.
|
|
1886
|
+
*
|
|
1887
|
+
* `<ChatView …/>` em `ChatScreen.tsx` → `components/ChatView/ChatView.tsx`.
|
|
1888
|
+
* Import de pacote devolve `null` no `resolve` e fica de fora: componente de
|
|
1889
|
+
* terceiro não tem tela nossa dentro.
|
|
1890
|
+
*
|
|
1891
|
+
* Ignora `<X.Screen>` e `<X.Navigator>` de propósito — navegação é outro
|
|
1892
|
+
* extractor, e um navegador não é conteúdo de tela.
|
|
1893
|
+
*/
|
|
1894
|
+
renderedComponentFiles(file) {
|
|
1895
|
+
const ast = this.parse(file);
|
|
1896
|
+
if (!ast) return [];
|
|
1897
|
+
const names = /* @__PURE__ */ new Set();
|
|
1898
|
+
const visit = (node) => {
|
|
1899
|
+
if (!node || typeof node !== "object") return;
|
|
1900
|
+
if (BabelTypes.isJSXOpeningElement(node) && BabelTypes.isJSXIdentifier(node.name)) {
|
|
1901
|
+
const n = node.name.name;
|
|
1902
|
+
if (/^[A-Z]/.test(n)) names.add(n);
|
|
1903
|
+
}
|
|
1904
|
+
for (const key of Object.keys(node)) {
|
|
1905
|
+
const value = node[key];
|
|
1906
|
+
if (Array.isArray(value)) {
|
|
1907
|
+
for (const item of value) visit(item);
|
|
1908
|
+
} else if (value && typeof value === "object" && "type" in value) {
|
|
1909
|
+
visit(value);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
visit(ast.program);
|
|
1914
|
+
const out = /* @__PURE__ */ new Set();
|
|
1915
|
+
for (const name of names) {
|
|
1916
|
+
const origin = this.resolveBinding(file, name);
|
|
1917
|
+
if (origin && origin.file !== file) out.add(origin.file);
|
|
1918
|
+
}
|
|
1919
|
+
return [...out];
|
|
1920
|
+
}
|
|
1921
|
+
};
|
|
1922
|
+
function memberChain(node) {
|
|
1923
|
+
const chain = [];
|
|
1924
|
+
let current = node;
|
|
1925
|
+
while (BabelTypes.isMemberExpression(current)) {
|
|
1926
|
+
if (current.computed) {
|
|
1927
|
+
if (!BabelTypes.isStringLiteral(current.property)) return null;
|
|
1928
|
+
chain.unshift(current.property.value);
|
|
1929
|
+
} else if (BabelTypes.isIdentifier(current.property)) {
|
|
1930
|
+
chain.unshift(current.property.name);
|
|
1931
|
+
} else {
|
|
1932
|
+
return null;
|
|
1933
|
+
}
|
|
1934
|
+
current = current.object;
|
|
1935
|
+
}
|
|
1936
|
+
return BabelTypes.isIdentifier(current) ? { root: current.name, path: chain } : null;
|
|
1937
|
+
}
|
|
1938
|
+
function objectProperty(obj, key) {
|
|
1939
|
+
for (const prop of obj.properties) {
|
|
1940
|
+
if (!BabelTypes.isObjectProperty(prop)) continue;
|
|
1941
|
+
const name = BabelTypes.isIdentifier(prop.key) ? prop.key.name : BabelTypes.isStringLiteral(prop.key) ? prop.key.value : null;
|
|
1942
|
+
if (name === key && BabelTypes.isExpression(prop.value)) return prop.value;
|
|
1943
|
+
}
|
|
1944
|
+
return null;
|
|
1945
|
+
}
|
|
1946
|
+
function readTsconfigAliases(rootDir) {
|
|
1947
|
+
const file = path2__default.join(rootDir, "tsconfig.json");
|
|
1948
|
+
if (!existsSync(file)) return [];
|
|
1949
|
+
let parsed;
|
|
1950
|
+
try {
|
|
1951
|
+
const raw = stripJsonComments(readFileSync(file, "utf-8")).replace(/,(\s*[}\]])/g, "$1");
|
|
1952
|
+
parsed = JSON.parse(raw);
|
|
1953
|
+
} catch {
|
|
1954
|
+
return [];
|
|
1955
|
+
}
|
|
1956
|
+
const paths = parsed.compilerOptions?.paths;
|
|
1957
|
+
if (!paths) return [];
|
|
1958
|
+
const baseUrl = path2__default.resolve(rootDir, parsed.compilerOptions?.baseUrl ?? ".");
|
|
1959
|
+
const out = [];
|
|
1960
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
1961
|
+
const first = targets[0];
|
|
1962
|
+
if (typeof first !== "string") continue;
|
|
1963
|
+
out.push({
|
|
1964
|
+
prefix: pattern.replace(/\/?\*$/, ""),
|
|
1965
|
+
target: path2__default.resolve(baseUrl, first.replace(/\/?\*$/, ""))
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
return out.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
1969
|
+
}
|
|
1970
|
+
function stripJsonComments(text) {
|
|
1971
|
+
let out = "";
|
|
1972
|
+
let inString = false;
|
|
1973
|
+
let escaped = false;
|
|
1974
|
+
for (let i = 0; i < text.length; i++) {
|
|
1975
|
+
const c = text[i];
|
|
1976
|
+
if (inString) {
|
|
1977
|
+
out += c;
|
|
1978
|
+
if (escaped) escaped = false;
|
|
1979
|
+
else if (c === "\\") escaped = true;
|
|
1980
|
+
else if (c === '"') inString = false;
|
|
1981
|
+
continue;
|
|
1982
|
+
}
|
|
1983
|
+
if (c === '"') {
|
|
1984
|
+
inString = true;
|
|
1985
|
+
out += c;
|
|
1986
|
+
continue;
|
|
1987
|
+
}
|
|
1988
|
+
if (c === "/" && text[i + 1] === "/") {
|
|
1989
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
1990
|
+
out += "\n";
|
|
1991
|
+
continue;
|
|
1992
|
+
}
|
|
1993
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
1994
|
+
i += 2;
|
|
1995
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
1996
|
+
i++;
|
|
1997
|
+
continue;
|
|
1998
|
+
}
|
|
1999
|
+
out += c;
|
|
2000
|
+
}
|
|
2001
|
+
return out;
|
|
2002
|
+
}
|
|
2003
|
+
function readWorkspacePackages(rootDir) {
|
|
2004
|
+
const start = path2__default.resolve(rootDir);
|
|
2005
|
+
let dir = start;
|
|
2006
|
+
for (let i = 0; i < 6; i++) {
|
|
2007
|
+
const globs = workspaceGlobs(dir);
|
|
2008
|
+
if (globs.length > 0) {
|
|
2009
|
+
const packages = expandWorkspaceGlobs(dir, globs);
|
|
2010
|
+
const containsApp = packages.some(
|
|
2011
|
+
(p) => start === p.dir || start.startsWith(p.dir + path2__default.sep)
|
|
2012
|
+
);
|
|
2013
|
+
return containsApp ? packages : [];
|
|
2014
|
+
}
|
|
2015
|
+
const parent = path2__default.dirname(dir);
|
|
2016
|
+
if (parent === dir) break;
|
|
2017
|
+
dir = parent;
|
|
2018
|
+
}
|
|
2019
|
+
return [];
|
|
2020
|
+
}
|
|
2021
|
+
function workspaceGlobs(dir) {
|
|
2022
|
+
const pnpm = path2__default.join(dir, "pnpm-workspace.yaml");
|
|
2023
|
+
if (existsSync(pnpm)) {
|
|
2024
|
+
try {
|
|
2025
|
+
const lines = readFileSync(pnpm, "utf-8").split(/\r?\n/);
|
|
2026
|
+
const out = [];
|
|
2027
|
+
let inPackages = false;
|
|
2028
|
+
for (const line of lines) {
|
|
2029
|
+
if (/^packages:/.test(line)) {
|
|
2030
|
+
inPackages = true;
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
2033
|
+
if (inPackages) {
|
|
2034
|
+
const m = /^\s*-\s*["']?([^"'#]+?)["']?\s*$/.exec(line);
|
|
2035
|
+
if (m) out.push(m[1].trim());
|
|
2036
|
+
else if (/^\S/.test(line)) break;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
if (out.length) return out;
|
|
2040
|
+
} catch {
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
const pkgPath = path2__default.join(dir, "package.json");
|
|
2044
|
+
if (!existsSync(pkgPath)) return [];
|
|
2045
|
+
try {
|
|
2046
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
2047
|
+
const ws = pkg.workspaces;
|
|
2048
|
+
if (Array.isArray(ws)) return ws;
|
|
2049
|
+
if (ws && Array.isArray(ws.packages)) return ws.packages;
|
|
2050
|
+
} catch {
|
|
2051
|
+
}
|
|
2052
|
+
return [];
|
|
2053
|
+
}
|
|
2054
|
+
function expandWorkspaceGlobs(root, globs) {
|
|
2055
|
+
const out = [];
|
|
2056
|
+
const add = (dir) => {
|
|
2057
|
+
const pkgPath = path2__default.join(dir, "package.json");
|
|
2058
|
+
if (!existsSync(pkgPath)) return;
|
|
2059
|
+
try {
|
|
2060
|
+
const name = JSON.parse(readFileSync(pkgPath, "utf-8")).name;
|
|
2061
|
+
if (name) out.push({ name, dir });
|
|
2062
|
+
} catch {
|
|
2063
|
+
}
|
|
2064
|
+
};
|
|
2065
|
+
for (const glob of globs) {
|
|
2066
|
+
if (glob.endsWith("/*")) {
|
|
2067
|
+
const parent = path2__default.join(root, glob.slice(0, -2));
|
|
2068
|
+
let entries = [];
|
|
2069
|
+
try {
|
|
2070
|
+
entries = readdirSync(parent);
|
|
2071
|
+
} catch {
|
|
2072
|
+
continue;
|
|
2073
|
+
}
|
|
2074
|
+
for (const entry of entries) {
|
|
2075
|
+
const dir = path2__default.join(parent, entry);
|
|
2076
|
+
try {
|
|
2077
|
+
if (statSync(dir).isDirectory()) add(dir);
|
|
2078
|
+
} catch {
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
} else if (!glob.includes("*")) {
|
|
2082
|
+
add(path2__default.join(root, glob));
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
return out.sort((a, b) => b.name.length - a.name.length);
|
|
2086
|
+
}
|
|
2087
|
+
var ROUTE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
2088
|
+
var PLATFORM_SUFFIXES = [".ios", ".android", ".native", ".web"];
|
|
2089
|
+
var ROUTE_DIR_CANDIDATES = ["app", "src/app"];
|
|
2090
|
+
function baseName(file) {
|
|
2091
|
+
let name = file;
|
|
2092
|
+
for (const ext of ROUTE_EXTENSIONS) {
|
|
2093
|
+
if (name.endsWith(ext)) {
|
|
2094
|
+
name = name.slice(0, -ext.length);
|
|
2095
|
+
break;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
for (const suffix of PLATFORM_SUFFIXES) {
|
|
2099
|
+
if (name.endsWith(suffix)) return name.slice(0, -suffix.length);
|
|
2100
|
+
}
|
|
2101
|
+
return name;
|
|
2102
|
+
}
|
|
2103
|
+
function isRouteFile(file) {
|
|
2104
|
+
return ROUTE_EXTENSIONS.some((ext) => file.endsWith(ext));
|
|
2105
|
+
}
|
|
2106
|
+
function isSpecial(base) {
|
|
2107
|
+
if (base === "_layout") return true;
|
|
2108
|
+
if (base.startsWith("+")) return base !== "+not-found";
|
|
2109
|
+
return base.startsWith("_");
|
|
2110
|
+
}
|
|
2111
|
+
function layoutType(file) {
|
|
2112
|
+
try {
|
|
2113
|
+
const src = readFileSync(file, "utf-8");
|
|
2114
|
+
if (/<(?:Native)?Tabs\b/.test(src)) return "tab";
|
|
2115
|
+
if (/<Drawer\b/.test(src)) return "drawer";
|
|
2116
|
+
} catch {
|
|
2117
|
+
}
|
|
2118
|
+
return "stack";
|
|
2119
|
+
}
|
|
2120
|
+
function findLayout(dir) {
|
|
2121
|
+
for (const ext of ROUTE_EXTENSIONS) {
|
|
2122
|
+
const candidate = path2__default.join(dir, "_layout" + ext);
|
|
2123
|
+
if (existsSync(candidate)) return candidate;
|
|
2124
|
+
}
|
|
2125
|
+
return null;
|
|
2126
|
+
}
|
|
2127
|
+
function analyzeExpoRouter(appRoot) {
|
|
2128
|
+
let routeDir = null;
|
|
2129
|
+
for (const candidate of ROUTE_DIR_CANDIDATES) {
|
|
2130
|
+
const full = path2__default.join(appRoot, candidate);
|
|
2131
|
+
if (existsSync(full) && findLayout(full)) {
|
|
2132
|
+
routeDir = full;
|
|
2133
|
+
break;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
if (!routeDir) return null;
|
|
2137
|
+
const routes = [];
|
|
2138
|
+
const navigators = /* @__PURE__ */ new Map();
|
|
2139
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2140
|
+
const walk = (dir, segments, navigator) => {
|
|
2141
|
+
const layout = findLayout(dir);
|
|
2142
|
+
const current = layout ? segments.join("/") || "/" : navigator;
|
|
2143
|
+
if (layout && !navigators.has(current)) navigators.set(current, layoutType(layout));
|
|
2144
|
+
let entries;
|
|
2145
|
+
try {
|
|
2146
|
+
entries = readdirSync(dir);
|
|
2147
|
+
} catch {
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
for (const entry of entries.sort()) {
|
|
2151
|
+
const full = path2__default.join(dir, entry);
|
|
2152
|
+
let isDir = false;
|
|
2153
|
+
try {
|
|
2154
|
+
isDir = statSync(full).isDirectory();
|
|
2155
|
+
} catch {
|
|
2156
|
+
continue;
|
|
2157
|
+
}
|
|
2158
|
+
if (isDir) {
|
|
2159
|
+
if (entry === "node_modules" || entry.startsWith(".")) continue;
|
|
2160
|
+
walk(full, [...segments, entry], current);
|
|
2161
|
+
continue;
|
|
2162
|
+
}
|
|
2163
|
+
if (!isRouteFile(entry)) continue;
|
|
2164
|
+
const base = baseName(entry);
|
|
2165
|
+
if (isSpecial(base)) continue;
|
|
2166
|
+
const routeSegments = base === "index" ? segments : [...segments, base];
|
|
2167
|
+
const name = "/" + routeSegments.join("/");
|
|
2168
|
+
if (seen.has(name)) continue;
|
|
2169
|
+
seen.add(name);
|
|
2170
|
+
routes.push({
|
|
2171
|
+
name,
|
|
2172
|
+
componentFile: full,
|
|
2173
|
+
navigatorName: current,
|
|
2174
|
+
navigatorType: navigators.get(current) ?? "stack",
|
|
2175
|
+
params: routeSegments.filter((s) => s.startsWith("[") && s.endsWith("]")).map((s) => s.slice(1, -1).replace(/^\.\.\./, ""))
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
};
|
|
2179
|
+
walk(routeDir, [], "/");
|
|
2180
|
+
return {
|
|
2181
|
+
routeDir: path2__default.relative(appRoot, routeDir),
|
|
2182
|
+
routes,
|
|
2183
|
+
navigators: [...navigators.entries()].map(([name, type]) => ({ name, type }))
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
// src/analyzers/NavigationAnalyzer.ts
|
|
2188
|
+
var NAVIGATOR_FACTORIES = {
|
|
2189
|
+
createStackNavigator: "stack",
|
|
2190
|
+
createNativeStackNavigator: "stack",
|
|
2191
|
+
createBottomTabNavigator: "tab",
|
|
2192
|
+
createTabNavigator: "tab",
|
|
2193
|
+
createMaterialTopTabNavigator: "tab",
|
|
2194
|
+
createMaterialBottomTabNavigator: "tab",
|
|
2195
|
+
createDrawerNavigator: "drawer"
|
|
2196
|
+
};
|
|
2197
|
+
var CUSTOM_FACTORY = /^create[A-Za-z0-9]*(Navigator|Stack|Tabs?|Drawer)$/;
|
|
2198
|
+
function unwrapStaticScreen(value) {
|
|
2199
|
+
if (BabelTypes.isCallExpression(value)) {
|
|
2200
|
+
const arg = value.arguments[0];
|
|
2201
|
+
return arg ? unwrapStaticScreen(arg) : null;
|
|
2202
|
+
}
|
|
2203
|
+
if (BabelTypes.isObjectExpression(value)) {
|
|
2204
|
+
for (const prop of value.properties) {
|
|
2205
|
+
if (!BabelTypes.isObjectProperty(prop)) continue;
|
|
2206
|
+
const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : null;
|
|
2207
|
+
if (key === "screen" && BabelTypes.isExpression(prop.value)) return prop.value;
|
|
2208
|
+
}
|
|
2209
|
+
return null;
|
|
2210
|
+
}
|
|
2211
|
+
return BabelTypes.isExpression(value) ? value : null;
|
|
2212
|
+
}
|
|
2213
|
+
function inferNavigatorType(factoryName) {
|
|
2214
|
+
if (/drawer/i.test(factoryName)) return "drawer";
|
|
2215
|
+
if (/tab/i.test(factoryName)) return "tab";
|
|
2216
|
+
return "stack";
|
|
2217
|
+
}
|
|
2218
|
+
var NAVIGATION_EVIDENCE = new RegExp(
|
|
2219
|
+
// `\.Navigator`/`\.Screen` entram porque o JSX pode estar num arquivo que
|
|
2220
|
+
// não menciona fábrica nenhuma: `comapeo` declara `RootStack` em
|
|
2221
|
+
// `Stack/RootStack.ts` e escreve todas as 120 rotas em `Stack/index.tsx`,
|
|
2222
|
+
// que não cita `createNativeStackNavigator` uma vez sequer. Sem isto a
|
|
2223
|
+
// junção entre arquivos nunca chega a ser tentada.
|
|
2224
|
+
// Sem `\\b` depois da palavra — `createNativeStackNavigatorWithAuth` não tem
|
|
2225
|
+
// fronteira ali, e exigi-la fecharia o portão para o arquivo que declara os
|
|
2226
|
+
// seis navegadores do `bluesky`.
|
|
2227
|
+
`create[A-Za-z0-9]*(?:Navigator|Stack|Tabs?|Drawer)|ParamList|\\.Navigator\\b|\\.Screen\\b`
|
|
2228
|
+
);
|
|
1636
2229
|
var NavigationAnalyzer = class {
|
|
1637
2230
|
config;
|
|
1638
2231
|
/** Extra glob patterns for navigation file discovery (added to defaults) */
|
|
1639
2232
|
navigationInclude;
|
|
1640
2233
|
/** Extra glob patterns to exclude from navigation analysis */
|
|
1641
2234
|
navigationExclude;
|
|
2235
|
+
/** Exposto para a composição reusar o cache de AST em vez de reparsear a árvore. */
|
|
2236
|
+
graph;
|
|
2237
|
+
/**
|
|
2238
|
+
* Rota → arquivo do componente que ela monta. É a única ligação entre o grafo
|
|
2239
|
+
* de navegação e a árvore de telas; sem ela as duas metades do documento
|
|
2240
|
+
* falam de coisas diferentes. Populada por `analyze()`.
|
|
2241
|
+
*/
|
|
2242
|
+
routeTargets = /* @__PURE__ */ new Map();
|
|
2243
|
+
/**
|
|
2244
|
+
* Arquivos que DECLARAM uma fábrica de navegador.
|
|
2245
|
+
*
|
|
2246
|
+
* Um navegador aninhado é montado como `component=` de uma rota — no
|
|
2247
|
+
* `apps/example-app`, `<Stack.Screen name="Main" component={MainTabNavigator} />`.
|
|
2248
|
+
* Promover esse arquivo a tela criaria seis "telas" sem uma única ação, que é
|
|
2249
|
+
* exatamente o falso positivo que o corpus existe para não repetir. Um
|
|
2250
|
+
* navegador é um contêiner de rotas; a tela está um nível abaixo.
|
|
2251
|
+
*
|
|
2252
|
+
* Entram aqui os dois lados: o arquivo que CHAMA a fábrica e o arquivo que
|
|
2253
|
+
* RENDERIZA `<X.Navigator>`. Não são o mesmo — `bluewallet` declara
|
|
2254
|
+
* `DetailViewStack` num arquivo e escreve o JSX em
|
|
2255
|
+
* `navigation/DetailViewScreensStack.tsx`, e checar só o primeiro deixava o
|
|
2256
|
+
* segundo entrar como tela.
|
|
2257
|
+
*/
|
|
2258
|
+
navigatorFiles = /* @__PURE__ */ new Set();
|
|
2259
|
+
/** Preenchido por `analyze()`. Vazio antes disso. */
|
|
2260
|
+
diagnostics = {
|
|
2261
|
+
filesScanned: 0,
|
|
2262
|
+
filesWithEvidence: 0,
|
|
2263
|
+
navigatorsDeclared: 0,
|
|
2264
|
+
navigatorsWithJsx: 0,
|
|
2265
|
+
routes: 0,
|
|
2266
|
+
routesLinkedToFile: 0
|
|
2267
|
+
};
|
|
1642
2268
|
constructor(config, options) {
|
|
1643
2269
|
this.config = config;
|
|
2270
|
+
this.graph = new ModuleGraph(config.rootDir);
|
|
1644
2271
|
this.navigationInclude = options?.navigationInclude ?? [];
|
|
1645
2272
|
this.navigationExclude = options?.navigationExclude ?? [];
|
|
1646
2273
|
}
|
|
1647
2274
|
/** Build the full navigation graph */
|
|
1648
2275
|
async analyze() {
|
|
1649
2276
|
const navigationFiles = await this.findNavigationFiles();
|
|
1650
|
-
const parsedNavigators = [];
|
|
1651
2277
|
const typeExports = [];
|
|
2278
|
+
this.routeTargets.clear();
|
|
2279
|
+
const declarations = /* @__PURE__ */ new Map();
|
|
2280
|
+
const evidenceFiles = [];
|
|
2281
|
+
this.navigatorFiles.clear();
|
|
1652
2282
|
for (const filePath of navigationFiles) {
|
|
1653
2283
|
try {
|
|
1654
2284
|
const content = await promises.readFile(filePath, "utf-8");
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
2285
|
+
if (!NAVIGATION_EVIDENCE.test(content)) continue;
|
|
2286
|
+
evidenceFiles.push(filePath);
|
|
2287
|
+
for (const decl of this.collectDeclarations(filePath)) {
|
|
2288
|
+
declarations.set(`${decl.file}#${decl.name}`, decl);
|
|
2289
|
+
if (decl.confident) this.navigatorFiles.add(decl.file);
|
|
2290
|
+
}
|
|
2291
|
+
typeExports.push(...this.parseParamTypes(content, filePath));
|
|
1659
2292
|
} catch (error) {
|
|
1660
2293
|
console.warn(`Failed to parse ${filePath}:`, error);
|
|
1661
2294
|
}
|
|
1662
2295
|
}
|
|
2296
|
+
const parsedNavigators = [];
|
|
2297
|
+
for (const filePath of evidenceFiles) {
|
|
2298
|
+
try {
|
|
2299
|
+
parsedNavigators.push(...this.parseNavigatorUsages(filePath, declarations));
|
|
2300
|
+
} catch (error) {
|
|
2301
|
+
console.warn(`Failed to parse navigators in ${filePath}:`, error);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
let detachedScreens = 0;
|
|
2305
|
+
for (const filePath of evidenceFiles) {
|
|
2306
|
+
try {
|
|
2307
|
+
for (const detached of this.parseDetachedScreens(filePath, declarations)) {
|
|
2308
|
+
const existing = parsedNavigators.find(
|
|
2309
|
+
(nav) => nav.name === detached.name && nav.type === detached.type
|
|
2310
|
+
);
|
|
2311
|
+
if (!existing) {
|
|
2312
|
+
parsedNavigators.push(detached);
|
|
2313
|
+
detachedScreens += detached.screens.length;
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
2316
|
+
const seen = new Set(existing.screens.map((screen) => screen.name));
|
|
2317
|
+
for (const screen of detached.screens) {
|
|
2318
|
+
if (seen.has(screen.name)) continue;
|
|
2319
|
+
seen.add(screen.name);
|
|
2320
|
+
existing.screens.push(screen);
|
|
2321
|
+
detachedScreens++;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
} catch (error) {
|
|
2325
|
+
console.warn(`Failed to parse detached screens in ${filePath}:`, error);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
const expo = analyzeExpoRouter(this.config.rootDir);
|
|
2329
|
+
if (expo) {
|
|
2330
|
+
console.log(
|
|
2331
|
+
`[NavigationAnalyzer] expo-router em "${expo.routeDir}": ${expo.routes.length} rotas, ${expo.navigators.length} layouts`
|
|
2332
|
+
);
|
|
2333
|
+
this.diagnostics.fileBasedRouter = {
|
|
2334
|
+
kind: "expo-router",
|
|
2335
|
+
routeDir: expo.routeDir,
|
|
2336
|
+
routes: expo.routes.length
|
|
2337
|
+
};
|
|
2338
|
+
const byNavigator = /* @__PURE__ */ new Map();
|
|
2339
|
+
for (const nav of expo.navigators) {
|
|
2340
|
+
byNavigator.set(nav.name, { name: nav.name, type: nav.type, screens: [] });
|
|
2341
|
+
}
|
|
2342
|
+
for (const route of expo.routes) {
|
|
2343
|
+
const navigator = byNavigator.get(route.navigatorName) ?? byNavigator.set(route.navigatorName, {
|
|
2344
|
+
name: route.navigatorName,
|
|
2345
|
+
type: route.navigatorType,
|
|
2346
|
+
screens: []
|
|
2347
|
+
}).get(route.navigatorName);
|
|
2348
|
+
navigator.screens.push({
|
|
2349
|
+
name: route.name,
|
|
2350
|
+
navigatorName: route.navigatorName,
|
|
2351
|
+
navigatorType: route.navigatorType,
|
|
2352
|
+
componentFile: route.componentFile,
|
|
2353
|
+
...route.params.length ? {
|
|
2354
|
+
params: route.params.map((name) => ({ name, type: "string", required: true }))
|
|
2355
|
+
} : {}
|
|
2356
|
+
});
|
|
2357
|
+
this.routeTargets.set(route.name, route.componentFile);
|
|
2358
|
+
}
|
|
2359
|
+
parsedNavigators.push(...byNavigator.values());
|
|
2360
|
+
}
|
|
2361
|
+
let staticOnly = 0;
|
|
2362
|
+
for (const decl of declarations.values()) {
|
|
2363
|
+
if (!decl.confident || !decl.staticScreens?.length) continue;
|
|
2364
|
+
const existing = parsedNavigators.find((n) => n.name === decl.name && n.type === decl.type);
|
|
2365
|
+
if (existing) {
|
|
2366
|
+
const seen = new Set(existing.screens.map((s) => s.name));
|
|
2367
|
+
for (const screen of decl.staticScreens) {
|
|
2368
|
+
if (!seen.has(screen.name)) existing.screens.push(screen);
|
|
2369
|
+
}
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
parsedNavigators.push({ name: decl.name, type: decl.type, screens: decl.staticScreens });
|
|
2373
|
+
staticOnly++;
|
|
2374
|
+
}
|
|
2375
|
+
const confidentDeclarations = [...declarations.values()].filter((d) => d.confident).length;
|
|
2376
|
+
const uniqueRoutes = new Set(
|
|
2377
|
+
parsedNavigators.flatMap((nav) => nav.screens.map((screen) => screen.name))
|
|
2378
|
+
);
|
|
2379
|
+
this.diagnostics = {
|
|
2380
|
+
...this.diagnostics,
|
|
2381
|
+
filesScanned: navigationFiles.length,
|
|
2382
|
+
filesWithEvidence: evidenceFiles.length,
|
|
2383
|
+
navigatorsDeclared: confidentDeclarations,
|
|
2384
|
+
navigatorsWithJsx: parsedNavigators.length,
|
|
2385
|
+
routes: uniqueRoutes.size,
|
|
2386
|
+
// `routeTargets` é indexado por nome de rota, e uma rota pode ter sido
|
|
2387
|
+
// registrada por um navegador que não sobreviveu. O mínimo evita a
|
|
2388
|
+
// cobertura acima de 100% que a medição do corpus expôs no `coopcycle`.
|
|
2389
|
+
routesLinkedToFile: Math.min(this.routeTargets.size, uniqueRoutes.size)
|
|
2390
|
+
};
|
|
2391
|
+
console.log(
|
|
2392
|
+
`[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`
|
|
2393
|
+
);
|
|
1663
2394
|
this.attachParamsToNavigators(parsedNavigators, typeExports);
|
|
1664
2395
|
return this.buildNavigationGraph(parsedNavigators);
|
|
1665
2396
|
}
|
|
1666
|
-
/**
|
|
2397
|
+
/** Fase 1: as fábricas `create*Navigator()` atribuídas a um nome neste arquivo. */
|
|
2398
|
+
collectDeclarations(filePath) {
|
|
2399
|
+
const ast = this.graph.parse(filePath);
|
|
2400
|
+
if (!ast) return [];
|
|
2401
|
+
const out = /* @__PURE__ */ new Map();
|
|
2402
|
+
traverse4(ast, {
|
|
2403
|
+
CallExpression: (nodePath) => {
|
|
2404
|
+
const callee = nodePath.node.callee;
|
|
2405
|
+
if (!BabelTypes.isIdentifier(callee)) return;
|
|
2406
|
+
const known = NAVIGATOR_FACTORIES[callee.name] ?? (CUSTOM_FACTORY.test(callee.name) ? inferNavigatorType(callee.name) : null);
|
|
2407
|
+
const type = known ?? inferNavigatorType(callee.name);
|
|
2408
|
+
const confident = known !== null;
|
|
2409
|
+
let up = nodePath.parentPath;
|
|
2410
|
+
for (let i = 0; i < 4 && up; i++) {
|
|
2411
|
+
if (BabelTypes.isVariableDeclarator(up.node)) break;
|
|
2412
|
+
if (!BabelTypes.isMemberExpression(up.node) && !BabelTypes.isCallExpression(up.node)) {
|
|
2413
|
+
up = null;
|
|
2414
|
+
break;
|
|
2415
|
+
}
|
|
2416
|
+
up = up.parentPath;
|
|
2417
|
+
}
|
|
2418
|
+
const declarator = up?.node;
|
|
2419
|
+
if (!declarator || !BabelTypes.isVariableDeclarator(declarator) || !BabelTypes.isIdentifier(declarator.id)) {
|
|
2420
|
+
return;
|
|
2421
|
+
}
|
|
2422
|
+
const name = declarator.id.name;
|
|
2423
|
+
if (out.get(name)?.confident) return;
|
|
2424
|
+
const staticScreens = confident ? this.parseStaticScreens(filePath, nodePath.node.arguments[0], name, type) : [];
|
|
2425
|
+
out.set(name, {
|
|
2426
|
+
file: filePath,
|
|
2427
|
+
name,
|
|
2428
|
+
type,
|
|
2429
|
+
confident,
|
|
2430
|
+
...staticScreens.length ? { staticScreens } : {}
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
});
|
|
2434
|
+
return [...out.values()];
|
|
2435
|
+
}
|
|
2436
|
+
/**
|
|
2437
|
+
* As rotas de `createXNavigator({ screens: { … } })`.
|
|
2438
|
+
*
|
|
2439
|
+
* Quatro formas no corpus, todas em `rocketchat`:
|
|
2440
|
+
* `NewServerView` — shorthand, nome = componente
|
|
2441
|
+
* `LoginView: createNativeStackScreen({ screen })` — embrulho da própria lib
|
|
2442
|
+
* `SelectListView: SelectListViewScreen` — identificador direto
|
|
2443
|
+
* `ChatsStackNavigator: ChatsStack` — navegador aninhado
|
|
2444
|
+
*
|
|
2445
|
+
* `groups: { G: { screens: { … } } }` também entra: faz parte da API estática
|
|
2446
|
+
* e agrupar rotas não muda o que elas são.
|
|
2447
|
+
*/
|
|
2448
|
+
parseStaticScreens(filePath, config, navigatorName, navigatorType, depth = 0) {
|
|
2449
|
+
if (!config || !BabelTypes.isObjectExpression(config) || depth > 4) return [];
|
|
2450
|
+
const out = [];
|
|
2451
|
+
for (const prop of config.properties) {
|
|
2452
|
+
if (!BabelTypes.isObjectProperty(prop)) continue;
|
|
2453
|
+
const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : BabelTypes.isStringLiteral(prop.key) ? prop.key.value : null;
|
|
2454
|
+
if (!key) continue;
|
|
2455
|
+
if (key === "groups" && BabelTypes.isObjectExpression(prop.value)) {
|
|
2456
|
+
for (const group of prop.value.properties) {
|
|
2457
|
+
if (!BabelTypes.isObjectProperty(group) || !BabelTypes.isExpression(group.value)) continue;
|
|
2458
|
+
out.push(
|
|
2459
|
+
...this.parseStaticScreens(
|
|
2460
|
+
filePath,
|
|
2461
|
+
group.value,
|
|
2462
|
+
navigatorName,
|
|
2463
|
+
navigatorType,
|
|
2464
|
+
depth + 1
|
|
2465
|
+
)
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
continue;
|
|
2469
|
+
}
|
|
2470
|
+
if (key !== "screens" || !BabelTypes.isObjectExpression(prop.value)) continue;
|
|
2471
|
+
for (const entry of prop.value.properties) {
|
|
2472
|
+
if (!BabelTypes.isObjectProperty(entry)) continue;
|
|
2473
|
+
const routeName = BabelTypes.isIdentifier(entry.key) ? entry.key.name : BabelTypes.isStringLiteral(entry.key) ? entry.key.value : null;
|
|
2474
|
+
if (!routeName) continue;
|
|
2475
|
+
const componentExpr = unwrapStaticScreen(entry.value);
|
|
2476
|
+
const componentFile = componentExpr ? this.graph.componentFile(filePath, componentExpr) : null;
|
|
2477
|
+
if (componentFile) this.routeTargets.set(routeName, componentFile);
|
|
2478
|
+
out.push({
|
|
2479
|
+
name: routeName,
|
|
2480
|
+
navigatorName,
|
|
2481
|
+
navigatorType,
|
|
2482
|
+
...componentFile ? { componentFile } : {}
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
return out;
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Todo arquivo-fonte do projeto — não os que ficam num diretório com o nome
|
|
2490
|
+
* certo.
|
|
2491
|
+
*
|
|
2492
|
+
* POR QUE ISTO MUDOU. A lista anterior era `**\/navigation/**`,
|
|
2493
|
+
* `**\/navigator*` e `**\/routes*`, o que fazia da descoberta de navegação uma
|
|
2494
|
+
* convenção de caminho. Medido contra 20 apps React Native de terceiros
|
|
2495
|
+
* (`qa/eval-corpus`), o filtro errava por motivos que nada têm a ver com o
|
|
2496
|
+
* app não ter navegação:
|
|
2497
|
+
*
|
|
2498
|
+
* - `pocketpal` declara 4 navegadores em `App.tsx` e dentro de `src/screens/`;
|
|
2499
|
+
* - `comapeo` usa `src/frontend/Navigation/` — `N` maiúsculo, e o glob é
|
|
2500
|
+
* sensível a caixa;
|
|
2501
|
+
* - `abacus` usa `src/routes/index.tsx` — `routes` é o DIRETÓRIO, e o glob
|
|
2502
|
+
* pedia um arquivo chamado `routes*`;
|
|
2503
|
+
* - `discourse` declara em `js/Discourse.js` — o glob só aceitava `.ts`/`.tsx`;
|
|
2504
|
+
* - `rainbow` tem 11 arquivos com navegador e o glob alcançava 1.
|
|
2505
|
+
*
|
|
2506
|
+
* Varrer tudo não custa uma varredura nova: `ReactNativePlatformAnalyzer` já
|
|
2507
|
+
* globa e parseia a árvore inteira para `ComponentAnalyzer` e `FormAnalyzer`.
|
|
2508
|
+
* O que segura o custo aqui é o portão por evidência em `analyze()`, que só
|
|
2509
|
+
* parseia arquivo cujo texto menciona uma fábrica de navegador.
|
|
2510
|
+
*
|
|
2511
|
+
* Os globs antigos continuam na lista: se um projeto restringir `include`, o
|
|
2512
|
+
* que era encontrado antes continua sendo.
|
|
2513
|
+
*/
|
|
1667
2514
|
async findNavigationFiles() {
|
|
1668
2515
|
const patterns = [
|
|
2516
|
+
...this.config.include ?? ["**/*.{ts,tsx,js,jsx}"],
|
|
2517
|
+
// Piso de compatibilidade — nunca encontrar MENOS que a versão anterior.
|
|
1669
2518
|
"**/navigation/**/*.{ts,tsx}",
|
|
1670
2519
|
"**/navigator*.{ts,tsx}",
|
|
1671
2520
|
"**/routes*.{ts,tsx}",
|
|
@@ -1676,91 +2525,188 @@ var NavigationAnalyzer = class {
|
|
|
1676
2525
|
"**/node_modules/**",
|
|
1677
2526
|
"**/dist/**",
|
|
1678
2527
|
"**/build/**",
|
|
2528
|
+
// Um navegador declarado dentro de um teste é fixture, não a navegação
|
|
2529
|
+
// do app. Isto passou a importar quando a varredura deixou de ser por
|
|
2530
|
+
// caminho: em `expensify`, os únicos arquivos que o glob antigo
|
|
2531
|
+
// alcançava eram três de `tests/`.
|
|
2532
|
+
"**/__tests__/**",
|
|
2533
|
+
"**/test/**",
|
|
2534
|
+
"**/tests/**",
|
|
2535
|
+
"**/*.test.{ts,tsx,js,jsx}",
|
|
2536
|
+
"**/*.tests.{ts,tsx,js,jsx}",
|
|
2537
|
+
"**/*.spec.{ts,tsx,js,jsx}",
|
|
2538
|
+
// Sufixo `Test` sem ponto: `expensify` chama os fixtures dele de
|
|
2539
|
+
// `LegalNameStepTest.tsx`, e cada um monta um `<Stack.Screen>` próprio.
|
|
2540
|
+
// Auditando as 144 rotas lidas dele contra `SCREENS.ts`, 2 vinham daqui —
|
|
2541
|
+
// destinos que existem no teste e não no app, para os quais o agente
|
|
2542
|
+
// tentaria navegar.
|
|
2543
|
+
"**/*Test.{ts,tsx,js,jsx}",
|
|
2544
|
+
"**/*Tests.{ts,tsx,js,jsx}",
|
|
1679
2545
|
...this.config.exclude || [],
|
|
1680
2546
|
// §5: Add user-configured navigation excludes
|
|
1681
2547
|
...this.navigationExclude
|
|
1682
2548
|
];
|
|
1683
|
-
const files = await
|
|
2549
|
+
const files = await globSorted(patterns, {
|
|
1684
2550
|
cwd: this.config.rootDir,
|
|
1685
2551
|
ignore: excludePatterns
|
|
1686
2552
|
});
|
|
1687
|
-
return files.map((file) =>
|
|
2553
|
+
return files.map((file) => path2__default.join(this.config.rootDir, file));
|
|
1688
2554
|
}
|
|
1689
|
-
/**
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
traverse4(ast, {
|
|
1704
|
-
// Detect createStackNavigator / createTabNavigator / createDrawerNavigator calls
|
|
1705
|
-
CallExpression: (nodePath) => {
|
|
1706
|
-
const { node } = nodePath;
|
|
1707
|
-
const callee = node.callee;
|
|
1708
|
-
let navigatorType = null;
|
|
1709
|
-
if (BabelTypes.isIdentifier(callee) && callee.name === "createNativeStackNavigator") {
|
|
1710
|
-
navigatorType = "stack";
|
|
1711
|
-
} else if (BabelTypes.isIdentifier(callee) && callee.name === "createStackNavigator") {
|
|
1712
|
-
navigatorType = "stack";
|
|
1713
|
-
} else if (BabelTypes.isIdentifier(callee) && callee.name === "createBottomTabNavigator") {
|
|
1714
|
-
navigatorType = "tab";
|
|
1715
|
-
} else if (BabelTypes.isIdentifier(callee) && callee.name === "createTabNavigator") {
|
|
1716
|
-
navigatorType = "tab";
|
|
1717
|
-
} else if (BabelTypes.isIdentifier(callee) && callee.name === "createDrawerNavigator") {
|
|
1718
|
-
navigatorType = "drawer";
|
|
1719
|
-
}
|
|
1720
|
-
if (navigatorType) {
|
|
1721
|
-
const parent = nodePath.parent;
|
|
1722
|
-
if (BabelTypes.isVariableDeclarator(parent) && BabelTypes.isIdentifier(parent.id)) {
|
|
1723
|
-
const varName = parent.id.name;
|
|
1724
|
-
navigatorCalls.set(varName, {
|
|
1725
|
-
name: varName,
|
|
1726
|
-
type: navigatorType,
|
|
1727
|
-
screens: []
|
|
1728
|
-
});
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
},
|
|
1732
|
-
// Detect Stack.Navigator / Tab.Navigator JSX elements
|
|
1733
|
-
JSXElement: (nodePath) => {
|
|
1734
|
-
const { node } = nodePath;
|
|
1735
|
-
const openingElement = node.openingElement;
|
|
1736
|
-
if (BabelTypes.isJSXMemberExpression(openingElement.name) && BabelTypes.isJSXIdentifier(openingElement.name.object)) {
|
|
1737
|
-
const objectName = openingElement.name.object.name;
|
|
1738
|
-
const propertyName = BabelTypes.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
|
|
1739
|
-
if (propertyName === "Navigator") {
|
|
1740
|
-
const navigator = navigatorCalls.get(objectName);
|
|
1741
|
-
if (navigator) {
|
|
1742
|
-
const initialRouteAttr = openingElement.attributes.find(
|
|
1743
|
-
(attr) => BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
|
|
1744
|
-
);
|
|
1745
|
-
if (BabelTypes.isJSXAttribute(initialRouteAttr) && BabelTypes.isStringLiteral(initialRouteAttr.value)) {
|
|
1746
|
-
navigator.initialRouteName = initialRouteAttr.value.value;
|
|
1747
|
-
}
|
|
1748
|
-
const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
|
|
1749
|
-
screensByNavigator.set(objectName, screens);
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
2555
|
+
/**
|
|
2556
|
+
* Fase 2: o JSX `<X.Navigator>` deste arquivo, ligado à declaração de `X` —
|
|
2557
|
+
* que pode estar aqui ou em qualquer arquivo que este importe.
|
|
2558
|
+
*/
|
|
2559
|
+
parseNavigatorUsages(filePath, declarations) {
|
|
2560
|
+
const ast = this.graph.parse(filePath);
|
|
2561
|
+
if (!ast) return [];
|
|
2562
|
+
const found = /* @__PURE__ */ new Map();
|
|
2563
|
+
traverse4(ast, {
|
|
2564
|
+
JSXElement: (nodePath) => {
|
|
2565
|
+
const { node } = nodePath;
|
|
2566
|
+
const openingElement = node.openingElement;
|
|
2567
|
+
if (!BabelTypes.isJSXMemberExpression(openingElement.name) || !BabelTypes.isJSXIdentifier(openingElement.name.object)) {
|
|
2568
|
+
return;
|
|
1753
2569
|
}
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
2570
|
+
const objectName = openingElement.name.object.name;
|
|
2571
|
+
const propertyName = BabelTypes.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
|
|
2572
|
+
if (propertyName !== "Navigator") return;
|
|
2573
|
+
const decl = this.resolveNavigator(filePath, objectName, declarations);
|
|
2574
|
+
if (!decl) return;
|
|
2575
|
+
const navigator = found.get(objectName) ?? {
|
|
2576
|
+
name: objectName,
|
|
2577
|
+
type: decl.type,
|
|
2578
|
+
screens: []
|
|
2579
|
+
};
|
|
2580
|
+
const initialRouteAttr = openingElement.attributes.find(
|
|
2581
|
+
(attr) => BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
|
|
2582
|
+
);
|
|
2583
|
+
if (BabelTypes.isJSXAttribute(initialRouteAttr)) {
|
|
2584
|
+
const initial = this.attributeString(filePath, initialRouteAttr);
|
|
2585
|
+
if (initial) navigator.initialRouteName = initial;
|
|
2586
|
+
}
|
|
2587
|
+
this.navigatorFiles.add(filePath);
|
|
2588
|
+
this.navigatorFiles.add(decl.file);
|
|
2589
|
+
navigator.screens = this.extractScreensFromNavigator(filePath, node, objectName, decl.type);
|
|
2590
|
+
found.set(objectName, navigator);
|
|
2591
|
+
}
|
|
2592
|
+
});
|
|
2593
|
+
return [...found.values()];
|
|
2594
|
+
}
|
|
2595
|
+
/**
|
|
2596
|
+
* Rotas declaradas FORA de qualquer `<X.Navigator>`.
|
|
2597
|
+
*
|
|
2598
|
+
* O CASO. A fase 2 desce a partir do `<X.Navigator>` e lê as `<X.Screen>`
|
|
2599
|
+
* que estão DENTRO dele. Dois alvos do corpus não escrevem assim, e entre os
|
|
2600
|
+
* dois são 171 rotas invisíveis:
|
|
2601
|
+
*
|
|
2602
|
+
* `bluesky` — `function commonScreens(Stack: typeof Flat) { return (<>
|
|
2603
|
+
* <Stack.Screen name="NotFound" … /> … </>) }`, chamada de
|
|
2604
|
+
* dentro de seis navegadores diferentes. 70 rotas.
|
|
2605
|
+
* `comapeo` — `export const createAppScreens = ({intl}) => (<>
|
|
2606
|
+
* <RootStack.Group><RootStack.Screen … /></RootStack.Group></>)`,
|
|
2607
|
+
* num arquivo sem `<RootStack.Navigator>` nenhum. 101 rotas.
|
|
2608
|
+
*
|
|
2609
|
+
* A REGRA. Uma `<X.Screen name="…">` sem `<Y.Navigator>` ancestral é rota do
|
|
2610
|
+
* navegador ao qual `X` se resolve. Não há palpite em jogo: o nome da rota é
|
|
2611
|
+
* literal do fonte (ou constante que o resolvedor segue), e `X` precisa
|
|
2612
|
+
* chegar a uma declaração de navegador que já existe. O que não resolve não
|
|
2613
|
+
* vira nada.
|
|
2614
|
+
*
|
|
2615
|
+
* O ancestral é o que evita contar duas vezes — dentro do `<X.Navigator>` a
|
|
2616
|
+
* fase 2 já leu, e somar aqui duplicaria cada rota do corpus inteiro.
|
|
2617
|
+
*
|
|
2618
|
+
* DUAS FORMAS DE RESOLVER `X`, e a segunda é o que o `bluesky` exige. A
|
|
2619
|
+
* primeira é a de sempre (declaração local, ou `import` seguido até a
|
|
2620
|
+
* origem) e resolve o `comapeo`. A segunda lê a ANOTAÇÃO DE TIPO do
|
|
2621
|
+
* parâmetro: em `commonScreens(Stack: typeof Flat)`, quem diz que `Stack` é
|
|
2622
|
+
* o navegador `Flat` é o próprio app, no fonte.
|
|
2623
|
+
*
|
|
2624
|
+
* O QUE ISTO NÃO FAZ. As 70 do `bluesky` ficam atribuídas a `Flat` — que as
|
|
2625
|
+
* contém de fato (`{commonScreens(Flat, numUnread)}`) — e não aos outros
|
|
2626
|
+
* cinco stacks que também chamam a mesma função. Seguir os seis pontos de
|
|
2627
|
+
* chamada é análise interprocedural, e o ganho seria só de atribuição: o nome
|
|
2628
|
+
* da rota e o arquivo da tela, que é o que o agente usa, já saem certos.
|
|
2629
|
+
*/
|
|
2630
|
+
parseDetachedScreens(filePath, declarations) {
|
|
2631
|
+
const ast = this.graph.parse(filePath);
|
|
2632
|
+
if (!ast) return [];
|
|
2633
|
+
const byNavigator = /* @__PURE__ */ new Map();
|
|
2634
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
2635
|
+
traverse4(ast, {
|
|
2636
|
+
JSXElement: (nodePath) => {
|
|
2637
|
+
const name = nodePath.node.openingElement.name;
|
|
2638
|
+
if (!BabelTypes.isJSXMemberExpression(name) || !BabelTypes.isJSXIdentifier(name.object)) return;
|
|
2639
|
+
if (!BabelTypes.isJSXIdentifier(name.property) || name.property.name !== "Screen") return;
|
|
2640
|
+
const insideNavigator = nodePath.findParent((parent) => {
|
|
2641
|
+
if (!parent.isJSXElement()) return false;
|
|
2642
|
+
const parentName = parent.node.openingElement.name;
|
|
2643
|
+
return BabelTypes.isJSXMemberExpression(parentName) && BabelTypes.isJSXIdentifier(parentName.property) && parentName.property.name === "Navigator";
|
|
2644
|
+
});
|
|
2645
|
+
if (insideNavigator) return;
|
|
2646
|
+
const local = name.object.name;
|
|
2647
|
+
if (!resolved.has(local)) {
|
|
2648
|
+
resolved.set(
|
|
2649
|
+
local,
|
|
2650
|
+
this.resolveDetachedNavigator(filePath, local, nodePath, declarations)
|
|
2651
|
+
);
|
|
2652
|
+
}
|
|
2653
|
+
const decl = resolved.get(local);
|
|
2654
|
+
if (!decl) return;
|
|
2655
|
+
const screen = this.parseScreenElement(filePath, nodePath.node, decl.name, decl.type);
|
|
2656
|
+
if (!screen) return;
|
|
2657
|
+
const nav = byNavigator.get(decl.name) ?? {
|
|
2658
|
+
name: decl.name,
|
|
2659
|
+
type: decl.type,
|
|
2660
|
+
screens: []
|
|
2661
|
+
};
|
|
2662
|
+
if (!nav.screens.some((existing) => existing.name === screen.name))
|
|
2663
|
+
nav.screens.push(screen);
|
|
2664
|
+
byNavigator.set(decl.name, nav);
|
|
2665
|
+
}
|
|
2666
|
+
});
|
|
2667
|
+
return [...byNavigator.values()];
|
|
2668
|
+
}
|
|
2669
|
+
/**
|
|
2670
|
+
* De um `X` usado como `<X.Screen>` até a declaração do navegador.
|
|
2671
|
+
*
|
|
2672
|
+
* Além do caminho normal — declaração local ou `import` seguido até a origem
|
|
2673
|
+
* — aceita `X` como PARÂMETRO anotado com `typeof Y`. É o idioma do
|
|
2674
|
+
* `bluesky`, e a anotação é declaração do app: nada aqui é inferido do nome.
|
|
2675
|
+
*/
|
|
2676
|
+
resolveDetachedNavigator(filePath, localName, nodePath, declarations) {
|
|
2677
|
+
const direct = this.resolveNavigator(filePath, localName, declarations);
|
|
2678
|
+
if (direct) return direct;
|
|
2679
|
+
const binding = nodePath.scope.getBinding(localName);
|
|
2680
|
+
if (!binding || binding.kind !== "param") return null;
|
|
2681
|
+
const param = binding.path.node;
|
|
2682
|
+
if (!BabelTypes.isIdentifier(param) || !BabelTypes.isTSTypeAnnotation(param.typeAnnotation)) return null;
|
|
2683
|
+
const annotation = param.typeAnnotation.typeAnnotation;
|
|
2684
|
+
if (!BabelTypes.isTSTypeQuery(annotation) || !BabelTypes.isIdentifier(annotation.exprName)) return null;
|
|
2685
|
+
return this.resolveNavigator(filePath, annotation.exprName.name, declarations);
|
|
2686
|
+
}
|
|
2687
|
+
/**
|
|
2688
|
+
* De `<X.Navigator>` até a declaração de `X`.
|
|
2689
|
+
*
|
|
2690
|
+
* Primeiro no próprio arquivo; se não estiver, segue o `import` até onde `X`
|
|
2691
|
+
* é definido. Resolver pelo import é o que torna a junção entre arquivos
|
|
2692
|
+
* segura: dois `Stack` de arquivos diferentes nunca colidem, porque a chave
|
|
2693
|
+
* é o arquivo de DEFINIÇÃO.
|
|
2694
|
+
*/
|
|
2695
|
+
resolveNavigator(filePath, localName, declarations) {
|
|
2696
|
+
const local = declarations.get(`${filePath}#${localName}`);
|
|
2697
|
+
if (local) return local;
|
|
2698
|
+
const origin = this.graph.resolveBinding(filePath, localName);
|
|
2699
|
+
if (!origin) return null;
|
|
2700
|
+
return declarations.get(`${origin.file}#${origin.name}`) ?? null;
|
|
2701
|
+
}
|
|
2702
|
+
/** O valor string de um atributo JSX, resolvendo constante importada. */
|
|
2703
|
+
attributeString(filePath, attr) {
|
|
2704
|
+
const value = attr.value;
|
|
2705
|
+
if (BabelTypes.isStringLiteral(value)) return value.value;
|
|
2706
|
+
if (BabelTypes.isJSXExpressionContainer(value) && BabelTypes.isExpression(value.expression)) {
|
|
2707
|
+
return this.graph.stringConstant(filePath, value.expression);
|
|
1762
2708
|
}
|
|
1763
|
-
return
|
|
2709
|
+
return null;
|
|
1764
2710
|
}
|
|
1765
2711
|
/**
|
|
1766
2712
|
* Is this JSX element `<navigatorVarName.MEMBER …>`?
|
|
@@ -1855,22 +2801,48 @@ var NavigationAnalyzer = class {
|
|
|
1855
2801
|
return found;
|
|
1856
2802
|
}
|
|
1857
2803
|
/** Extract screens from a navigator JSX element */
|
|
1858
|
-
extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
|
|
2804
|
+
extractScreensFromNavigator(filePath, navigatorElement, navigatorVarName, navigatorType) {
|
|
1859
2805
|
const screens = [];
|
|
1860
2806
|
if (!navigatorElement.children) return screens;
|
|
1861
2807
|
const seen = /* @__PURE__ */ new Set();
|
|
1862
2808
|
for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
|
|
1863
|
-
const
|
|
1864
|
-
if (!
|
|
1865
|
-
seen.add(
|
|
1866
|
-
screens.push(
|
|
1867
|
-
name: screenName,
|
|
1868
|
-
navigatorName: navigatorVarName,
|
|
1869
|
-
navigatorType
|
|
1870
|
-
});
|
|
2809
|
+
const screen = this.parseScreenElement(filePath, element, navigatorVarName, navigatorType);
|
|
2810
|
+
if (!screen || seen.has(screen.name)) continue;
|
|
2811
|
+
seen.add(screen.name);
|
|
2812
|
+
screens.push(screen);
|
|
1871
2813
|
}
|
|
1872
2814
|
return screens;
|
|
1873
2815
|
}
|
|
2816
|
+
/**
|
|
2817
|
+
* Uma `<X.Screen>` isolada até a rota que ela declara.
|
|
2818
|
+
*
|
|
2819
|
+
* Separado de `extractScreensFromNavigator` porque a MESMA leitura serve para
|
|
2820
|
+
* a `<X.Screen>` que mora dentro do `<X.Navigator>` e para a que mora fora
|
|
2821
|
+
* dele — só a forma de chegar até o elemento muda.
|
|
2822
|
+
*/
|
|
2823
|
+
parseScreenElement(filePath, element, navigatorName, navigatorType) {
|
|
2824
|
+
const attrs = element.openingElement.attributes;
|
|
2825
|
+
const nameAttr = attrs.find(
|
|
2826
|
+
(a) => BabelTypes.isJSXAttribute(a) && BabelTypes.isJSXIdentifier(a.name) && a.name.name === "name"
|
|
2827
|
+
);
|
|
2828
|
+
const screenName = BabelTypes.isJSXAttribute(nameAttr) ? this.attributeString(filePath, nameAttr) : null;
|
|
2829
|
+
if (!screenName) return null;
|
|
2830
|
+
const componentAttr = attrs.find(
|
|
2831
|
+
(a) => BabelTypes.isJSXAttribute(a) && BabelTypes.isJSXIdentifier(a.name) && a.name.name === "component"
|
|
2832
|
+
);
|
|
2833
|
+
let componentFile = null;
|
|
2834
|
+
if (BabelTypes.isJSXAttribute(componentAttr) && BabelTypes.isJSXExpressionContainer(componentAttr.value)) {
|
|
2835
|
+
const expr = componentAttr.value.expression;
|
|
2836
|
+
if (BabelTypes.isExpression(expr)) componentFile = this.graph.componentFile(filePath, expr);
|
|
2837
|
+
}
|
|
2838
|
+
if (componentFile) this.routeTargets.set(screenName, componentFile);
|
|
2839
|
+
return {
|
|
2840
|
+
name: screenName,
|
|
2841
|
+
navigatorName,
|
|
2842
|
+
navigatorType,
|
|
2843
|
+
...componentFile ? { componentFile } : {}
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
1874
2846
|
/** Extract string attribute value from JSX attributes */
|
|
1875
2847
|
extractAttributeValue(attributes, attrName) {
|
|
1876
2848
|
const attr = attributes.find(
|
|
@@ -1966,7 +2938,7 @@ var NavigationAnalyzer = class {
|
|
|
1966
2938
|
if (type.type === "TSUndefinedKeyword") return "undefined";
|
|
1967
2939
|
if (type.type === "TSNullKeyword") return "null";
|
|
1968
2940
|
if (type.type === "TSUnionType") {
|
|
1969
|
-
return type.types.map((
|
|
2941
|
+
return type.types.map((t13) => this.typeToString(t13)).join(" | ");
|
|
1970
2942
|
}
|
|
1971
2943
|
if (type.type === "TSTypeLiteral") {
|
|
1972
2944
|
return "object";
|
|
@@ -1987,7 +2959,7 @@ var NavigationAnalyzer = class {
|
|
|
1987
2959
|
/** Attach parsed type params to navigator screens */
|
|
1988
2960
|
attachParamsToNavigators(navigators, types) {
|
|
1989
2961
|
for (const navigator of navigators) {
|
|
1990
|
-
const matchingType = types.find((
|
|
2962
|
+
const matchingType = types.find((t13) => t13.type === navigator.type);
|
|
1991
2963
|
if (matchingType) {
|
|
1992
2964
|
for (const screen of navigator.screens) {
|
|
1993
2965
|
const screenParams = matchingType.paramEntries.get(screen.name);
|
|
@@ -2077,8 +3049,8 @@ var ComponentAnalyzer = class {
|
|
|
2077
3049
|
});
|
|
2078
3050
|
const components = [];
|
|
2079
3051
|
traverse4(ast, {
|
|
2080
|
-
JSXElement: (
|
|
2081
|
-
const component = this.extractComponentFromJSXElement(
|
|
3052
|
+
JSXElement: (path11) => {
|
|
3053
|
+
const component = this.extractComponentFromJSXElement(path11.node);
|
|
2082
3054
|
if (component) {
|
|
2083
3055
|
components.push(component);
|
|
2084
3056
|
}
|
|
@@ -2205,13 +3177,13 @@ var FormAnalyzer = class {
|
|
|
2205
3177
|
this.inputElements = [];
|
|
2206
3178
|
this.submitButtons = [];
|
|
2207
3179
|
traverse4(ast, {
|
|
2208
|
-
CallExpression: (
|
|
2209
|
-
this.extractStateVariables(
|
|
3180
|
+
CallExpression: (path11) => {
|
|
3181
|
+
this.extractStateVariables(path11.node);
|
|
2210
3182
|
}
|
|
2211
3183
|
});
|
|
2212
3184
|
traverse4(ast, {
|
|
2213
|
-
JSXElement: (
|
|
2214
|
-
this.extractFormElements(
|
|
3185
|
+
JSXElement: (path11) => {
|
|
3186
|
+
this.extractFormElements(path11.node);
|
|
2215
3187
|
}
|
|
2216
3188
|
});
|
|
2217
3189
|
const validationRules = this.extractValidationRules(ast);
|
|
@@ -2311,8 +3283,8 @@ var FormAnalyzer = class {
|
|
|
2311
3283
|
extractValidationRules(ast) {
|
|
2312
3284
|
const rules = {};
|
|
2313
3285
|
traverse4(ast, {
|
|
2314
|
-
IfStatement: (
|
|
2315
|
-
const test =
|
|
3286
|
+
IfStatement: (path11) => {
|
|
3287
|
+
const test = path11.node.test;
|
|
2316
3288
|
const rule = this.extractRuleFromCondition(test);
|
|
2317
3289
|
if (rule) {
|
|
2318
3290
|
const { field, description } = rule;
|
|
@@ -2366,7 +3338,7 @@ var FormAnalyzer = class {
|
|
|
2366
3338
|
}
|
|
2367
3339
|
buildForms(filePath, validationRules) {
|
|
2368
3340
|
if (this.inputElements.length === 0) return [];
|
|
2369
|
-
const fileName =
|
|
3341
|
+
const fileName = path2.basename(filePath, path2.extname(filePath));
|
|
2370
3342
|
const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
|
|
2371
3343
|
const fields = this.inputElements.map((input) => {
|
|
2372
3344
|
const fieldType = this.inferFieldType(input);
|
|
@@ -2415,13 +3387,105 @@ var FormAnalyzer = class {
|
|
|
2415
3387
|
return rule !== void 0 && rule.includes("required");
|
|
2416
3388
|
}
|
|
2417
3389
|
};
|
|
3390
|
+
|
|
3391
|
+
// src/pipeline/composition.ts
|
|
3392
|
+
var MAX_DEPTH = 3;
|
|
3393
|
+
var MAX_VISITED_PER_SCREEN = 60;
|
|
3394
|
+
var MAX_FAN_IN = 3;
|
|
3395
|
+
var ADDRESSABLE = /* @__PURE__ */ new Set(["testID", "appilotsId", "accessibilityLabel", "label"]);
|
|
3396
|
+
function descendants(graph, from, screenFiles) {
|
|
3397
|
+
const seen = /* @__PURE__ */ new Set([from]);
|
|
3398
|
+
const out = [];
|
|
3399
|
+
let frontier = [from];
|
|
3400
|
+
for (let depth = 0; depth < MAX_DEPTH && frontier.length > 0; depth++) {
|
|
3401
|
+
const next = [];
|
|
3402
|
+
for (const file of frontier) {
|
|
3403
|
+
for (const child of graph.renderedComponentFiles(file)) {
|
|
3404
|
+
if (seen.has(child)) continue;
|
|
3405
|
+
seen.add(child);
|
|
3406
|
+
if (screenFiles.has(child)) continue;
|
|
3407
|
+
out.push(child);
|
|
3408
|
+
next.push(child);
|
|
3409
|
+
if (out.length >= MAX_VISITED_PER_SCREEN) return out;
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
frontier = next;
|
|
3413
|
+
}
|
|
3414
|
+
return out;
|
|
3415
|
+
}
|
|
3416
|
+
function addressable(locator) {
|
|
3417
|
+
if (!locator || typeof locator !== "object") return false;
|
|
3418
|
+
const l = locator;
|
|
3419
|
+
if (typeof l["source"] === "string" && ADDRESSABLE.has(l["source"])) return true;
|
|
3420
|
+
return typeof l["testID"] === "string" && l["testID"].length > 0;
|
|
3421
|
+
}
|
|
3422
|
+
async function composeAffordances(options) {
|
|
3423
|
+
const { screens, graph, analyzeFile, screenFiles } = options;
|
|
3424
|
+
const byScreen = /* @__PURE__ */ new Map();
|
|
3425
|
+
const fanIn = /* @__PURE__ */ new Map();
|
|
3426
|
+
for (const screen of screens) {
|
|
3427
|
+
if (!screen.filePath) continue;
|
|
3428
|
+
const children = descendants(graph, screen.filePath, screenFiles);
|
|
3429
|
+
byScreen.set(screen, children);
|
|
3430
|
+
for (const child of children) fanIn.set(child, (fanIn.get(child) ?? 0) + 1);
|
|
3431
|
+
}
|
|
3432
|
+
const analyzed = /* @__PURE__ */ new Map();
|
|
3433
|
+
let screensEnriched = 0;
|
|
3434
|
+
let filesMerged = 0;
|
|
3435
|
+
for (const [screen, children] of byScreen) {
|
|
3436
|
+
const exclusive = children.filter((c) => (fanIn.get(c) ?? 0) <= MAX_FAN_IN);
|
|
3437
|
+
if (exclusive.length === 0) continue;
|
|
3438
|
+
const actionIds = new Set(screen.actions.map((a) => a.id));
|
|
3439
|
+
const targetIds = new Set((screen.targets ?? []).map((t13) => t13.id));
|
|
3440
|
+
const formIds = new Set(screen.forms.map((f) => f.id));
|
|
3441
|
+
let gained = false;
|
|
3442
|
+
for (const file of exclusive) {
|
|
3443
|
+
if (!analyzed.has(file)) {
|
|
3444
|
+
try {
|
|
3445
|
+
analyzed.set(file, await analyzeFile(file));
|
|
3446
|
+
} catch {
|
|
3447
|
+
analyzed.set(file, null);
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
const child = analyzed.get(file);
|
|
3451
|
+
if (!child) continue;
|
|
3452
|
+
let merged = false;
|
|
3453
|
+
for (const action of child.actions) {
|
|
3454
|
+
if (!addressable(action.locator)) continue;
|
|
3455
|
+
if (actionIds.has(action.id)) continue;
|
|
3456
|
+
actionIds.add(action.id);
|
|
3457
|
+
screen.actions.push(action);
|
|
3458
|
+
merged = true;
|
|
3459
|
+
}
|
|
3460
|
+
for (const target of child.targets ?? []) {
|
|
3461
|
+
if (!addressable(target.locator)) continue;
|
|
3462
|
+
if (targetIds.has(target.id)) continue;
|
|
3463
|
+
targetIds.add(target.id);
|
|
3464
|
+
(screen.targets ??= []).push(target);
|
|
3465
|
+
merged = true;
|
|
3466
|
+
}
|
|
3467
|
+
for (const form of child.forms) {
|
|
3468
|
+
if (form.fields.length === 0) continue;
|
|
3469
|
+
const id = formIds.has(form.id) ? `${form.id}:${child.name}` : form.id;
|
|
3470
|
+
if (formIds.has(id)) continue;
|
|
3471
|
+
formIds.add(id);
|
|
3472
|
+
screen.forms.push({ ...form, id });
|
|
3473
|
+
merged = true;
|
|
3474
|
+
}
|
|
3475
|
+
if (merged) {
|
|
3476
|
+
filesMerged++;
|
|
3477
|
+
gained = true;
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
if (gained) screensEnriched++;
|
|
3481
|
+
}
|
|
3482
|
+
return { screens, screensEnriched, filesMerged };
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
// src/analyzers/ReactNativePlatformAnalyzer.ts
|
|
2418
3486
|
var ReactNativePlatformAnalyzer = class {
|
|
2419
3487
|
platform = "react-native";
|
|
2420
3488
|
async analyze(config, options) {
|
|
2421
|
-
const screenAnalyzer = new ScreenAnalyzer(config, {
|
|
2422
|
-
strictScreens: options.strictScreens ?? true,
|
|
2423
|
-
screenPatterns: options.screenPatterns
|
|
2424
|
-
});
|
|
2425
3489
|
const navigationAnalyzer = new NavigationAnalyzer(config, {
|
|
2426
3490
|
navigationInclude: options.navigationInclude,
|
|
2427
3491
|
navigationExclude: options.navigationExclude
|
|
@@ -2429,14 +3493,31 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2429
3493
|
const componentAnalyzer = new ComponentAnalyzer(config);
|
|
2430
3494
|
const formAnalyzer = new FormAnalyzer(config);
|
|
2431
3495
|
console.log("[ReactNativePlatformAnalyzer] Running analyzers...");
|
|
2432
|
-
const
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
3496
|
+
const navigation = await navigationAnalyzer.analyze();
|
|
3497
|
+
const screenAnalyzer = new ScreenAnalyzer(config, {
|
|
3498
|
+
strictScreens: options.strictScreens ?? true,
|
|
3499
|
+
screenPatterns: options.screenPatterns,
|
|
3500
|
+
// Menos os arquivos que declaram navegador: ver `navigatorFiles`.
|
|
3501
|
+
routeTargetFiles: new Set(
|
|
3502
|
+
[...navigationAnalyzer.routeTargets.values()].filter(
|
|
3503
|
+
(file) => !navigationAnalyzer.navigatorFiles.has(file)
|
|
3504
|
+
)
|
|
3505
|
+
)
|
|
3506
|
+
});
|
|
3507
|
+
const screens = await screenAnalyzer.analyze();
|
|
3508
|
+
const composed = await composeAffordances({
|
|
3509
|
+
screens,
|
|
3510
|
+
graph: navigationAnalyzer.graph,
|
|
3511
|
+
analyzeFile: (file) => screenAnalyzer.analyzeFile(file),
|
|
3512
|
+
screenFiles: new Set(screens.map((s) => s.filePath).filter(Boolean))
|
|
3513
|
+
});
|
|
3514
|
+
console.log(
|
|
3515
|
+
`[ReactNativePlatformAnalyzer] Composi\xE7\xE3o: ${composed.filesMerged} componente(s) exclusivo(s) fundido(s) em ${composed.screensEnriched} tela(s)`
|
|
3516
|
+
);
|
|
2436
3517
|
console.log(
|
|
2437
3518
|
`[ReactNativePlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens`
|
|
2438
3519
|
);
|
|
2439
|
-
const screenFiles = await
|
|
3520
|
+
const screenFiles = await globSorted(config.include || ["**/*.tsx", "**/*.ts"], {
|
|
2440
3521
|
cwd: config.rootDir,
|
|
2441
3522
|
ignore: config.exclude || ["**/node_modules/**"]
|
|
2442
3523
|
});
|
|
@@ -2444,7 +3525,7 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2444
3525
|
`[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
|
|
2445
3526
|
);
|
|
2446
3527
|
const enrichmentPromises = screenFiles.map(async (file) => {
|
|
2447
|
-
const filePath =
|
|
3528
|
+
const filePath = path2__default.resolve(config.rootDir, file);
|
|
2448
3529
|
try {
|
|
2449
3530
|
const [components, forms] = await Promise.all([
|
|
2450
3531
|
componentAnalyzer.analyzeFile(filePath),
|
|
@@ -2499,7 +3580,8 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2499
3580
|
screens: enrichedScreens,
|
|
2500
3581
|
navigation,
|
|
2501
3582
|
analyzedFiles: screenFiles.length,
|
|
2502
|
-
...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
|
|
3583
|
+
...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {},
|
|
3584
|
+
diagnostics: { navigation: navigationAnalyzer.diagnostics }
|
|
2503
3585
|
};
|
|
2504
3586
|
}
|
|
2505
3587
|
mergeForm(target, source) {
|
|
@@ -2517,9 +3599,12 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2517
3599
|
findEquivalentField(fields, incoming) {
|
|
2518
3600
|
return fields.find((field) => {
|
|
2519
3601
|
if (field.name && incoming.name && field.name === incoming.name) return true;
|
|
2520
|
-
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
2521
|
-
|
|
2522
|
-
if (field.
|
|
3602
|
+
if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
|
|
3603
|
+
return true;
|
|
3604
|
+
if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
|
|
3605
|
+
return true;
|
|
3606
|
+
if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
|
|
3607
|
+
return true;
|
|
2523
3608
|
if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
|
|
2524
3609
|
return true;
|
|
2525
3610
|
}
|
|
@@ -2552,7 +3637,9 @@ var ReactNativePlatformAnalyzer = class {
|
|
|
2552
3637
|
if (incoming.fields.length === 0) return void 0;
|
|
2553
3638
|
let best;
|
|
2554
3639
|
for (const form of forms) {
|
|
2555
|
-
const overlap = incoming.fields.filter(
|
|
3640
|
+
const overlap = incoming.fields.filter(
|
|
3641
|
+
(field) => this.findEquivalentField(form.fields, field)
|
|
3642
|
+
).length;
|
|
2556
3643
|
if (overlap > 0 && (!best || overlap > best.overlap)) {
|
|
2557
3644
|
best = { form, overlap };
|
|
2558
3645
|
}
|
|
@@ -2706,13 +3793,13 @@ function staticRoutePath(node) {
|
|
|
2706
3793
|
if (!node) return void 0;
|
|
2707
3794
|
if (BabelTypes.isStringLiteral(node)) return node.value;
|
|
2708
3795
|
if (BabelTypes.isTemplateLiteral(node)) {
|
|
2709
|
-
let
|
|
3796
|
+
let path11 = "";
|
|
2710
3797
|
node.quasis.forEach((quasi, index) => {
|
|
2711
|
-
|
|
3798
|
+
path11 += quasi.value.cooked ?? quasi.value.raw;
|
|
2712
3799
|
const expr = node.expressions[index];
|
|
2713
|
-
if (expr)
|
|
3800
|
+
if (expr) path11 += `:${paramNameOf(expr)}`;
|
|
2714
3801
|
});
|
|
2715
|
-
return
|
|
3802
|
+
return path11;
|
|
2716
3803
|
}
|
|
2717
3804
|
return void 0;
|
|
2718
3805
|
}
|
|
@@ -2797,17 +3884,17 @@ var WebScreenAnalyzer = class {
|
|
|
2797
3884
|
include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
|
|
2798
3885
|
exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
2799
3886
|
} = this.config;
|
|
2800
|
-
const files = (await
|
|
3887
|
+
const files = (await globSorted(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
|
|
2801
3888
|
(file) => !file.endsWith(".d.ts")
|
|
2802
3889
|
);
|
|
2803
|
-
const patternMatches = await
|
|
3890
|
+
const patternMatches = await globSorted(this.screenPatterns, {
|
|
2804
3891
|
cwd: this.config.rootDir,
|
|
2805
3892
|
ignore: exclude
|
|
2806
3893
|
});
|
|
2807
|
-
const patternSet = new Set(patternMatches.map((f) =>
|
|
3894
|
+
const patternSet = new Set(patternMatches.map((f) => path2__default.resolve(this.config.rootDir, f)));
|
|
2808
3895
|
const candidates = [];
|
|
2809
3896
|
for (const file of files) {
|
|
2810
|
-
const filePath =
|
|
3897
|
+
const filePath = path2__default.resolve(this.config.rootDir, file);
|
|
2811
3898
|
try {
|
|
2812
3899
|
const candidate = await this.analyzeFile(filePath);
|
|
2813
3900
|
if (!candidate) continue;
|
|
@@ -2851,7 +3938,7 @@ var WebScreenAnalyzer = class {
|
|
|
2851
3938
|
}
|
|
2852
3939
|
}
|
|
2853
3940
|
}
|
|
2854
|
-
const name = registerScreenMeta?.name || componentName ||
|
|
3941
|
+
const name = registerScreenMeta?.name || componentName || path2__default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
|
|
2855
3942
|
const descriptor = {
|
|
2856
3943
|
name,
|
|
2857
3944
|
filePath,
|
|
@@ -3740,7 +4827,9 @@ var WebNavigationAnalyzer = class {
|
|
|
3740
4827
|
for (const filePath of files) {
|
|
3741
4828
|
try {
|
|
3742
4829
|
const content = await promises.readFile(filePath, "utf-8");
|
|
3743
|
-
if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
|
|
4830
|
+
if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
|
|
4831
|
+
content
|
|
4832
|
+
)) {
|
|
3744
4833
|
continue;
|
|
3745
4834
|
}
|
|
3746
4835
|
const ast = parseSource(content, this.config.parserPlugins);
|
|
@@ -3770,8 +4859,8 @@ var WebNavigationAnalyzer = class {
|
|
|
3770
4859
|
...this.config.exclude || [],
|
|
3771
4860
|
...this.navigationExclude
|
|
3772
4861
|
];
|
|
3773
|
-
const files = await
|
|
3774
|
-
return files.map((file) =>
|
|
4862
|
+
const files = await globSorted(patterns, { cwd: this.config.rootDir, ignore });
|
|
4863
|
+
return files.map((file) => path2__default.join(this.config.rootDir, file));
|
|
3775
4864
|
}
|
|
3776
4865
|
// ── JSX <Route> style ────────────────────────────────────────────
|
|
3777
4866
|
extractJsxRoutes(ast) {
|
|
@@ -3943,11 +5032,13 @@ var WebNavigationAnalyzer = class {
|
|
|
3943
5032
|
return {
|
|
3944
5033
|
screens,
|
|
3945
5034
|
initialScreen: initialRoute?.screenName ?? "",
|
|
3946
|
-
navigators: routes.length > 0 ? [
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
5035
|
+
navigators: routes.length > 0 ? [
|
|
5036
|
+
{
|
|
5037
|
+
name: navigatorName,
|
|
5038
|
+
type: WEB_NAVIGATOR_TYPE,
|
|
5039
|
+
screens: screenNames
|
|
5040
|
+
}
|
|
5041
|
+
] : []
|
|
3951
5042
|
};
|
|
3952
5043
|
}
|
|
3953
5044
|
};
|
|
@@ -4269,8 +5360,8 @@ var ZodParsedType = util.arrayToEnum([
|
|
|
4269
5360
|
"set"
|
|
4270
5361
|
]);
|
|
4271
5362
|
var getParsedType = (data) => {
|
|
4272
|
-
const
|
|
4273
|
-
switch (
|
|
5363
|
+
const t13 = typeof data;
|
|
5364
|
+
switch (t13) {
|
|
4274
5365
|
case "undefined":
|
|
4275
5366
|
return ZodParsedType.undefined;
|
|
4276
5367
|
case "string":
|
|
@@ -4542,8 +5633,8 @@ function getErrorMap() {
|
|
|
4542
5633
|
|
|
4543
5634
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
4544
5635
|
var makeIssue = (params) => {
|
|
4545
|
-
const { data, path:
|
|
4546
|
-
const fullPath = [...
|
|
5636
|
+
const { data, path: path11, errorMaps, issueData } = params;
|
|
5637
|
+
const fullPath = [...path11, ...issueData.path || []];
|
|
4547
5638
|
const fullIssue = {
|
|
4548
5639
|
...issueData,
|
|
4549
5640
|
path: fullPath
|
|
@@ -4659,11 +5750,11 @@ var errorUtil;
|
|
|
4659
5750
|
|
|
4660
5751
|
// ../../node_modules/zod/v3/types.js
|
|
4661
5752
|
var ParseInputLazyPath = class {
|
|
4662
|
-
constructor(parent, value,
|
|
5753
|
+
constructor(parent, value, path11, key) {
|
|
4663
5754
|
this._cachedPath = [];
|
|
4664
5755
|
this.parent = parent;
|
|
4665
5756
|
this.data = value;
|
|
4666
|
-
this._path =
|
|
5757
|
+
this._path = path11;
|
|
4667
5758
|
this._key = key;
|
|
4668
5759
|
}
|
|
4669
5760
|
get path() {
|
|
@@ -8104,7 +9195,18 @@ var coerce = {
|
|
|
8104
9195
|
};
|
|
8105
9196
|
var NEVER = INVALID;
|
|
8106
9197
|
|
|
8107
|
-
// ../shared/dist/chunk-
|
|
9198
|
+
// ../shared/dist/chunk-SWQOZWHP.mjs
|
|
9199
|
+
var SAFE_IMAGE_URL_RE = /^(?:https?:\/\/|data:image\/(?:png|jpe?g|gif|webp);base64,)/i;
|
|
9200
|
+
var HAS_SCHEME_RE = /^[\s\u0000-\u001f]*[a-z][a-z0-9+.-]*:/i;
|
|
9201
|
+
function isSafeImageUrl(value) {
|
|
9202
|
+
if (value === null || value === void 0) return true;
|
|
9203
|
+
const trimmed = value.trim();
|
|
9204
|
+
if (trimmed === "") return true;
|
|
9205
|
+
if (!HAS_SCHEME_RE.test(trimmed)) return true;
|
|
9206
|
+
return SAFE_IMAGE_URL_RE.test(trimmed);
|
|
9207
|
+
}
|
|
9208
|
+
|
|
9209
|
+
// ../shared/dist/chunk-NMQNNPSJ.mjs
|
|
8108
9210
|
var locatorSourceSchema = external_exports.enum([
|
|
8109
9211
|
"appilotsId",
|
|
8110
9212
|
"testID",
|
|
@@ -8283,6 +9385,23 @@ var actionDescriptorSchema = external_exports.object({
|
|
|
8283
9385
|
effect: external_exports.enum(["read", "write", "destructive"]).or(external_exports.string()).optional(),
|
|
8284
9386
|
riskLevel: external_exports.enum(["low", "medium", "high"]).or(external_exports.string()).optional(),
|
|
8285
9387
|
requiresConfirmation: external_exports.boolean().optional(),
|
|
9388
|
+
/**
|
|
9389
|
+
* How long this action's work is expected to take, in milliseconds,
|
|
9390
|
+
* DECLARED by the app — not inferred (that is `appilotsInferred`).
|
|
9391
|
+
*
|
|
9392
|
+
* The SDK's post-action wait was a constant: 6s, or 10s when the
|
|
9393
|
+
* generator could prove the handler awaits something. No constant
|
|
9394
|
+
* fits, because app operations run from ~100ms to minutes, and the
|
|
9395
|
+
* failure is silent in both directions — too short and the agent
|
|
9396
|
+
* photographs a loading screen with no controls on it, too long and
|
|
9397
|
+
* every fast press pays for the slowest one. Only the app knows.
|
|
9398
|
+
*
|
|
9399
|
+
* Absent means absent: the SDK keeps its current defaults, so a
|
|
9400
|
+
* document generated before this field existed behaves exactly as it
|
|
9401
|
+
* did. The SDK also clamps the value to its own safety ceiling — a
|
|
9402
|
+
* declared budget is a request, not a licence to hang the session.
|
|
9403
|
+
*/
|
|
9404
|
+
asyncBudgetMs: external_exports.number().int().positive().optional(),
|
|
8286
9405
|
appilotsInferred: appilotsInferredActionSchema.optional()
|
|
8287
9406
|
}).passthrough();
|
|
8288
9407
|
var screenPermissionDescriptorSchema = external_exports.object({
|
|
@@ -8430,9 +9549,16 @@ var createProjectSchema = external_exports.object({
|
|
|
8430
9549
|
// Reference to a tenant-scoped provider (preferred). Optional at
|
|
8431
9550
|
// creation so a project can be created and configured later.
|
|
8432
9551
|
providerId: external_exports.string().min(1).optional(),
|
|
8433
|
-
|
|
9552
|
+
// 16k chars (~4k tokens): um prompt de CS real não cabe em 4096 — o
|
|
9553
|
+
// primeiro cliente de produção chegou com 6.385 e tomou 'Invalid input'
|
|
9554
|
+
// sem saber por quê. O teto continua existindo porque o systemPrompt
|
|
9555
|
+
// entra em TODO hop do relay: é proteção de TPM, não de gosto.
|
|
9556
|
+
systemPrompt: external_exports.string().max(16e3).optional()
|
|
9557
|
+
});
|
|
9558
|
+
createProjectSchema.partial().extend({
|
|
9559
|
+
providerId: external_exports.string().min(1).nullable().optional(),
|
|
9560
|
+
systemPrompt: external_exports.string().max(16e3).nullable().optional()
|
|
8434
9561
|
});
|
|
8435
|
-
createProjectSchema.partial();
|
|
8436
9562
|
var providerKindSchema = external_exports.enum(["openai", "anthropic", "custom"]);
|
|
8437
9563
|
external_exports.object({
|
|
8438
9564
|
name: external_exports.string().min(1).max(100),
|
|
@@ -8458,7 +9584,7 @@ external_exports.object({
|
|
|
8458
9584
|
version: external_exports.string().default("1.0"),
|
|
8459
9585
|
content: external_exports.record(external_exports.unknown())
|
|
8460
9586
|
});
|
|
8461
|
-
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
|
|
9587
|
+
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator", "publish"]);
|
|
8462
9588
|
var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
|
|
8463
9589
|
external_exports.object({
|
|
8464
9590
|
name: external_exports.string().min(1).max(100),
|
|
@@ -8469,8 +9595,8 @@ external_exports.object({
|
|
|
8469
9595
|
scope: apiKeyScopeSchema.default("sdk"),
|
|
8470
9596
|
environment: apiKeyEnvironmentSchema.optional(),
|
|
8471
9597
|
expiresAt: external_exports.string().datetime().optional()
|
|
8472
|
-
}).refine((v) => v.scope
|
|
8473
|
-
message:
|
|
9598
|
+
}).refine((v) => v.scope === "operator" || !!v.projectId, {
|
|
9599
|
+
message: "projectId is required for SDK and publishing keys",
|
|
8474
9600
|
path: ["projectId"]
|
|
8475
9601
|
});
|
|
8476
9602
|
var boundedString = (max) => external_exports.string().max(max);
|
|
@@ -8495,6 +9621,15 @@ var snapshotInputSchema = external_exports.object({
|
|
|
8495
9621
|
type: boundedString(40).optional(),
|
|
8496
9622
|
required: external_exports.boolean().optional(),
|
|
8497
9623
|
invalid: external_exports.boolean().optional(),
|
|
9624
|
+
/**
|
|
9625
|
+
* Whether the field holds anything, as its own fact rather than an
|
|
9626
|
+
* inference over `value`. The value is the user's and is the first
|
|
9627
|
+
* thing a privacy policy withholds; the existence of a value is the
|
|
9628
|
+
* agent's and is what stops it filling the same field twice.
|
|
9629
|
+
*/
|
|
9630
|
+
filled: external_exports.boolean().optional(),
|
|
9631
|
+
/** This input has keyboard focus right now. */
|
|
9632
|
+
focused: external_exports.boolean().optional(),
|
|
8498
9633
|
inModal: external_exports.boolean().optional()
|
|
8499
9634
|
}).passthrough();
|
|
8500
9635
|
var snapshotButtonSchema = external_exports.object({
|
|
@@ -8650,6 +9785,32 @@ var agentSnapshotSchema = external_exports.object({
|
|
|
8650
9785
|
* it. Optional: older SDKs never clamp and never send it.
|
|
8651
9786
|
*/
|
|
8652
9787
|
truncated: external_exports.boolean().optional(),
|
|
9788
|
+
/**
|
|
9789
|
+
* What moved since the previous observation, computed on the device
|
|
9790
|
+
* because that is the only side holding both snapshots.
|
|
9791
|
+
*
|
|
9792
|
+
* Shape only — counts, booleans, and ids the app declared itself — so
|
|
9793
|
+
* it survives an observation whose content was withheld. ABSENT means
|
|
9794
|
+
* there was no previous observation to compare against; `unchanged:
|
|
9795
|
+
* true` means we compared and nothing moved, which is the strongest
|
|
9796
|
+
* evidence available that an action did nothing. The two must not be
|
|
9797
|
+
* collapsed, for the same reason `truncated` exists.
|
|
9798
|
+
*/
|
|
9799
|
+
delta: external_exports.object({
|
|
9800
|
+
routeChanged: external_exports.boolean().optional(),
|
|
9801
|
+
modalOpened: external_exports.boolean().optional(),
|
|
9802
|
+
modalClosed: external_exports.boolean().optional(),
|
|
9803
|
+
loadingStarted: external_exports.boolean().optional(),
|
|
9804
|
+
loadingFinished: external_exports.boolean().optional(),
|
|
9805
|
+
textsAdded: external_exports.number().int().nonnegative().optional(),
|
|
9806
|
+
textsRemoved: external_exports.number().int().nonnegative().optional(),
|
|
9807
|
+
visibleRowsDelta: external_exports.number().int().optional(),
|
|
9808
|
+
totalRowsDelta: external_exports.number().int().optional(),
|
|
9809
|
+
fieldsNewlyFilled: external_exports.array(boundedString(160)).max(12).optional(),
|
|
9810
|
+
fieldsCleared: external_exports.array(boundedString(160)).max(12).optional(),
|
|
9811
|
+
invalidAppeared: external_exports.boolean().optional(),
|
|
9812
|
+
unchanged: external_exports.boolean().optional()
|
|
9813
|
+
}).passthrough().optional(),
|
|
8653
9814
|
/**
|
|
8654
9815
|
* How many of this screen's controls the client could name, split by
|
|
8655
9816
|
* `identityProvenance`. Diagnostic only — the relay never grounds an
|
|
@@ -8683,7 +9844,19 @@ var agentContextSchema = external_exports.object({
|
|
|
8683
9844
|
rootRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
|
|
8684
9845
|
currentRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
|
|
8685
9846
|
routeNames: external_exports.array(boundedString(200)).max(500).optional(),
|
|
8686
|
-
canGoBack: external_exports.boolean().optional()
|
|
9847
|
+
canGoBack: external_exports.boolean().optional(),
|
|
9848
|
+
/**
|
|
9849
|
+
* The stack a back press pops through, oldest first, ending on
|
|
9850
|
+
* the current screen. `canGoBack` says a back exists; this says
|
|
9851
|
+
* where it goes.
|
|
9852
|
+
*/
|
|
9853
|
+
backStack: external_exports.array(boundedString(200)).max(50).optional(),
|
|
9854
|
+
/**
|
|
9855
|
+
* Screens the session has been on, oldest first. Names only —
|
|
9856
|
+
* params carry record ids and often personal data, and a route
|
|
9857
|
+
* name is structure.
|
|
9858
|
+
*/
|
|
9859
|
+
visited: external_exports.array(boundedString(200)).max(8).optional()
|
|
8687
9860
|
}).passthrough().optional(),
|
|
8688
9861
|
screenMetadata: external_exports.object({
|
|
8689
9862
|
name: boundedString(200).optional(),
|
|
@@ -8789,7 +9962,7 @@ var formFillPayloadSchema = external_exports.object({
|
|
|
8789
9962
|
submitAfterFill: external_exports.boolean().optional()
|
|
8790
9963
|
}).passthrough();
|
|
8791
9964
|
var uiInteractionPayloadSchema = external_exports.object({
|
|
8792
|
-
action: external_exports.enum(["press", "longPress", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
|
|
9965
|
+
action: external_exports.enum(["press", "longPress", "toggle", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
|
|
8793
9966
|
// `targetId` is the canonical server/LLM field. SDK runtimes still
|
|
8794
9967
|
// accept `componentId` as a compatibility alias.
|
|
8795
9968
|
targetId: external_exports.string().min(1),
|
|
@@ -8957,8 +10130,15 @@ external_exports.object({
|
|
|
8957
10130
|
// issue #169 — the observation the server grounds targets against, now
|
|
8958
10131
|
// validated + bounded at the border instead of z.record(z.unknown()).
|
|
8959
10132
|
context: agentContextSchema.optional(),
|
|
8960
|
-
/**
|
|
8961
|
-
|
|
10133
|
+
/**
|
|
10134
|
+
* Hop counter — server enforces a cap to prevent runaway loops.
|
|
10135
|
+
* The max here must stay ABOVE the server's `MAX_AGENT_HOPS` (10 since
|
|
10136
|
+
* #503, apps/api/src/modules/agents/routes.ts): the first over-budget
|
|
10137
|
+
* hop has to get through validation so the route can answer it with
|
|
10138
|
+
* the friendly automation-limit message instead of a 422. The +3
|
|
10139
|
+
* headroom mirrors what 7/10 was before the recalibration.
|
|
10140
|
+
*/
|
|
10141
|
+
hop: external_exports.number().int().min(1).max(13).optional()
|
|
8962
10142
|
});
|
|
8963
10143
|
var agentAccessLevelSchema = external_exports.enum(["read", "write", "none"]);
|
|
8964
10144
|
var screenPermissionSchema = external_exports.object({
|
|
@@ -9064,15 +10244,18 @@ var themeTokensSchema = external_exports.object({
|
|
|
9064
10244
|
*/
|
|
9065
10245
|
mode: external_exports.enum(["auto", "light", "dark"]).optional()
|
|
9066
10246
|
}).strict();
|
|
10247
|
+
var imageSourceField = external_exports.string().max(500).refine(isSafeImageUrl, {
|
|
10248
|
+
message: "Only http(s) URLs, base64 image data URIs, emoji or asset ids are allowed here"
|
|
10249
|
+
});
|
|
9067
10250
|
external_exports.object({
|
|
9068
10251
|
/** Tone / persona instructions appended to the system prompt. */
|
|
9069
10252
|
personaPrompt: external_exports.string().max(2048).nullable(),
|
|
9070
10253
|
/** Display name in the chat header (e.g. "Aria"). */
|
|
9071
10254
|
assistantName: external_exports.string().max(60).nullable(),
|
|
9072
10255
|
/** URL or remote asset id for the avatar shown next to assistant turns. */
|
|
9073
|
-
assistantAvatar:
|
|
10256
|
+
assistantAvatar: imageSourceField.nullable(),
|
|
9074
10257
|
/** Emoji or image URL for the empty-state icon. Auto-detected by prefix. */
|
|
9075
|
-
emptyStateIcon:
|
|
10258
|
+
emptyStateIcon: imageSourceField.nullable(),
|
|
9076
10259
|
/** First-message text shown in the empty state. */
|
|
9077
10260
|
welcomeMessage: external_exports.string().max(500).nullable(),
|
|
9078
10261
|
/** Title rendered at the top of the chat. */
|
|
@@ -9086,20 +10269,20 @@ external_exports.object({
|
|
|
9086
10269
|
/** Chat-open FAB background color. Null falls back to theme.colors.primary. */
|
|
9087
10270
|
triggerButtonColor: hexColorSchema.nullable(),
|
|
9088
10271
|
/** Image URL rendered inside the FAB instead of the default chat-bubble icon. */
|
|
9089
|
-
triggerButtonImageUrl:
|
|
10272
|
+
triggerButtonImageUrl: imageSourceField.nullable()
|
|
9090
10273
|
});
|
|
9091
10274
|
external_exports.object({
|
|
9092
10275
|
personaPrompt: external_exports.string().max(2048).nullable().optional(),
|
|
9093
10276
|
assistantName: external_exports.string().max(60).nullable().optional(),
|
|
9094
|
-
assistantAvatar:
|
|
9095
|
-
emptyStateIcon:
|
|
10277
|
+
assistantAvatar: imageSourceField.nullable().optional(),
|
|
10278
|
+
emptyStateIcon: imageSourceField.nullable().optional(),
|
|
9096
10279
|
welcomeMessage: external_exports.string().max(500).nullable().optional(),
|
|
9097
10280
|
chatTitle: external_exports.string().max(60).nullable().optional(),
|
|
9098
10281
|
poweredByVisible: external_exports.boolean().optional(),
|
|
9099
10282
|
theme: themeTokensSchema.nullable().optional(),
|
|
9100
10283
|
defaultLocale: localeSchema.nullable().optional(),
|
|
9101
10284
|
triggerButtonColor: hexColorSchema.nullable().optional(),
|
|
9102
|
-
triggerButtonImageUrl:
|
|
10285
|
+
triggerButtonImageUrl: imageSourceField.nullable().optional()
|
|
9103
10286
|
}).strict();
|
|
9104
10287
|
var sandboxObservationSchema = external_exports.object({
|
|
9105
10288
|
route: external_exports.string().max(200).optional(),
|
|
@@ -9119,7 +10302,19 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
9119
10302
|
type: external_exports.string().max(40).optional(),
|
|
9120
10303
|
placeholder: external_exports.string().max(200).optional(),
|
|
9121
10304
|
/** Inline validation state (SDK snapshot field). */
|
|
9122
|
-
invalid: external_exports.boolean().optional()
|
|
10305
|
+
invalid: external_exports.boolean().optional(),
|
|
10306
|
+
/**
|
|
10307
|
+
* Whether the field holds anything, as its own fact rather than
|
|
10308
|
+
* an inference over `value`.
|
|
10309
|
+
*
|
|
10310
|
+
* Here because the eval posts through this schema: a fact the
|
|
10311
|
+
* SDK produces and this contract has no word for is a fact the
|
|
10312
|
+
* corpus can never grade the agent on. `invalid` and `required`
|
|
10313
|
+
* were already here; these two were the half that was missing.
|
|
10314
|
+
*/
|
|
10315
|
+
filled: external_exports.boolean().optional(),
|
|
10316
|
+
/** This input has keyboard focus right now. */
|
|
10317
|
+
focused: external_exports.boolean().optional()
|
|
9123
10318
|
})
|
|
9124
10319
|
).max(100).optional(),
|
|
9125
10320
|
buttons: external_exports.array(
|
|
@@ -9598,7 +10793,7 @@ var manifestSchema = external_exports.object({
|
|
|
9598
10793
|
navigation: navigationGraphSchema.partial().optional()
|
|
9599
10794
|
}).passthrough();
|
|
9600
10795
|
async function loadManifest(rootDir, manifestPath) {
|
|
9601
|
-
const resolvedPath =
|
|
10796
|
+
const resolvedPath = path2__default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
|
|
9602
10797
|
let raw;
|
|
9603
10798
|
try {
|
|
9604
10799
|
raw = await readFile(resolvedPath, "utf-8");
|
|
@@ -9681,6 +10876,12 @@ function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
|
|
|
9681
10876
|
};
|
|
9682
10877
|
}
|
|
9683
10878
|
|
|
10879
|
+
// src/generators/checksum.ts
|
|
10880
|
+
function serializeForChecksum(document) {
|
|
10881
|
+
const { generatedAt: _generatedAt, ...content } = document;
|
|
10882
|
+
return JSON.stringify(content, null, 2);
|
|
10883
|
+
}
|
|
10884
|
+
|
|
9684
10885
|
// src/pipeline/enrichment.ts
|
|
9685
10886
|
function enrichScreenForAgent(screen) {
|
|
9686
10887
|
const targets = mergeTargets([
|
|
@@ -9757,7 +10958,10 @@ function mergeTargets(targets) {
|
|
|
9757
10958
|
for (const target of targets) {
|
|
9758
10959
|
if (!target.id) continue;
|
|
9759
10960
|
const existing = byId.get(target.id);
|
|
9760
|
-
byId.set(
|
|
10961
|
+
byId.set(
|
|
10962
|
+
target.id,
|
|
10963
|
+
existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target
|
|
10964
|
+
);
|
|
9761
10965
|
}
|
|
9762
10966
|
return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
9763
10967
|
}
|
|
@@ -9802,7 +11006,10 @@ function synthesizeFlows(screen, targets) {
|
|
|
9802
11006
|
intent: "destructive_action",
|
|
9803
11007
|
steps: [
|
|
9804
11008
|
{ type: "press", target: action.id, label: action.label },
|
|
9805
|
-
{
|
|
11009
|
+
{
|
|
11010
|
+
type: "confirm",
|
|
11011
|
+
description: "Wait for native or custom confirmation before continuing"
|
|
11012
|
+
},
|
|
9806
11013
|
{ type: "wait", description: describeWait(action) }
|
|
9807
11014
|
],
|
|
9808
11015
|
waitPolicy: waitPolicyForAction(action),
|
|
@@ -9817,10 +11024,19 @@ function synthesizeFlows(screen, targets) {
|
|
|
9817
11024
|
title: `Act on an item in ${collection.id}`,
|
|
9818
11025
|
intent: "list_action",
|
|
9819
11026
|
steps: [
|
|
9820
|
-
{
|
|
9821
|
-
|
|
11027
|
+
{
|
|
11028
|
+
type: "choose-list-item",
|
|
11029
|
+
target: collection.id,
|
|
11030
|
+
description: "Resolve the user reference to a visible or searchable row"
|
|
11031
|
+
},
|
|
11032
|
+
{
|
|
11033
|
+
type: "press",
|
|
11034
|
+
description: collection.rowAction?.description ?? "Open the row action"
|
|
11035
|
+
}
|
|
9822
11036
|
],
|
|
9823
|
-
waitPolicy: {
|
|
11037
|
+
waitPolicy: {
|
|
11038
|
+
expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback"
|
|
11039
|
+
}
|
|
9824
11040
|
});
|
|
9825
11041
|
}
|
|
9826
11042
|
}
|
|
@@ -9828,25 +11044,32 @@ function synthesizeFlows(screen, targets) {
|
|
|
9828
11044
|
}
|
|
9829
11045
|
function waitPolicyForAction(action) {
|
|
9830
11046
|
const expectedOutcome = action.successSignal?.type === "goBack" ? "goBack" : action.appilotsInferred?.expectedOutcome ?? (action.targetScreen ? "navigation" : void 0);
|
|
9831
|
-
const signals = [action.successSignal, action.failureSignal].filter(
|
|
11047
|
+
const signals = [action.successSignal, action.failureSignal].filter(
|
|
11048
|
+
Boolean
|
|
11049
|
+
);
|
|
11050
|
+
const maxMs = action.asyncBudgetMs ?? (action.appilotsInferred?.isAsyncTrigger ? 1e4 : void 0);
|
|
9832
11051
|
return {
|
|
9833
11052
|
...expectedOutcome ? { expectedOutcome } : {},
|
|
9834
11053
|
...signals && signals.length > 0 ? { signals } : {},
|
|
9835
|
-
...
|
|
11054
|
+
...maxMs !== void 0 ? { maxMs } : {}
|
|
9836
11055
|
};
|
|
9837
11056
|
}
|
|
9838
11057
|
function describeWait(action) {
|
|
9839
11058
|
if (action.successSignal?.description) return action.successSignal.description;
|
|
9840
|
-
if (action.successSignal?.type === "goBack")
|
|
11059
|
+
if (action.successSignal?.type === "goBack")
|
|
11060
|
+
return "Wait for the app to return to the previous screen";
|
|
9841
11061
|
if (action.targetScreen) return `Wait for navigation to ${action.targetScreen}`;
|
|
9842
|
-
if (action.appilotsInferred?.expectedOutcome)
|
|
11062
|
+
if (action.appilotsInferred?.expectedOutcome)
|
|
11063
|
+
return `Wait for ${action.appilotsInferred.expectedOutcome}`;
|
|
9843
11064
|
return "Wait for the UI to settle";
|
|
9844
11065
|
}
|
|
9845
11066
|
function synthesizeAgentHints(screen, targets, flows) {
|
|
9846
11067
|
const preferredTargets = targets.filter((target) => ["submit", "button", "list"].includes(target.role)).slice(0, 8).map((target) => target.id);
|
|
9847
11068
|
const commonTasks = flows.slice(0, 6).map((flow) => flow.title);
|
|
9848
11069
|
const safetyNotes = screen.actions.filter((action) => action.destructive || action.requiresConfirmation).map((action) => `${action.id} requires confirmation`);
|
|
9849
|
-
const firstAsyncAction = screen.actions.find(
|
|
11070
|
+
const firstAsyncAction = screen.actions.find(
|
|
11071
|
+
(action) => action.appilotsInferred?.isAsyncTrigger || action.asyncBudgetMs !== void 0
|
|
11072
|
+
);
|
|
9850
11073
|
const hints = {
|
|
9851
11074
|
primaryGoal: synthesizePrimaryGoal(screen),
|
|
9852
11075
|
commonTasks,
|
|
@@ -9873,7 +11096,9 @@ function synthesizePrimaryGoal(screen) {
|
|
|
9873
11096
|
}
|
|
9874
11097
|
const fieldCount = uniqueFields.size;
|
|
9875
11098
|
if (fieldCount > 0) {
|
|
9876
|
-
const requiredCount = Array.from(uniqueFields.values()).filter(
|
|
11099
|
+
const requiredCount = Array.from(uniqueFields.values()).filter(
|
|
11100
|
+
(field) => field.required
|
|
11101
|
+
).length;
|
|
9877
11102
|
const fieldLabel = fieldCount === 1 ? "field" : "fields";
|
|
9878
11103
|
goals.push(
|
|
9879
11104
|
requiredCount > 0 ? `Complete a form with ${fieldCount} ${fieldLabel} (${requiredCount} required)` : `Complete a form with ${fieldCount} ${fieldLabel}`
|
|
@@ -9887,7 +11112,9 @@ function synthesizePrimaryGoal(screen) {
|
|
|
9887
11112
|
if (submitCount > 0) {
|
|
9888
11113
|
goals.push(`Submit ${submitCount === 1 ? "the primary form" : `${submitCount} forms/actions`}`);
|
|
9889
11114
|
}
|
|
9890
|
-
const asyncCount = screen.actions.filter(
|
|
11115
|
+
const asyncCount = screen.actions.filter(
|
|
11116
|
+
(action) => action.appilotsInferred?.isAsyncTrigger
|
|
11117
|
+
).length;
|
|
9891
11118
|
if (asyncCount > 0) {
|
|
9892
11119
|
goals.push(`Wait for ${asyncCount === 1 ? "async feedback" : "async action feedback"}`);
|
|
9893
11120
|
}
|
|
@@ -9962,7 +11189,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
9962
11189
|
const agentReadyScreens = mergedScreens.map(
|
|
9963
11190
|
(screen) => enrichScreenForAgent({
|
|
9964
11191
|
...screen,
|
|
9965
|
-
filePath: screen.filePath ?
|
|
11192
|
+
filePath: screen.filePath ? path2__default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
|
|
9966
11193
|
})
|
|
9967
11194
|
);
|
|
9968
11195
|
const projectInfo = await this.getProjectInfo();
|
|
@@ -9986,11 +11213,11 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
9986
11213
|
}
|
|
9987
11214
|
};
|
|
9988
11215
|
const serialized = JSON.stringify(document, null, 2);
|
|
9989
|
-
const checksum = this.calculateChecksum(
|
|
9990
|
-
const filePath =
|
|
11216
|
+
const checksum = this.calculateChecksum(serializeForChecksum(document));
|
|
11217
|
+
const filePath = path2__default.resolve(outputDir, `mcp-document.${this.options.format}`);
|
|
9991
11218
|
await writeFile(filePath, serialized, "utf-8");
|
|
9992
11219
|
console.log(`[MCPGenerator] Document written to: ${filePath}`);
|
|
9993
|
-
const checksumFilePath =
|
|
11220
|
+
const checksumFilePath = path2__default.resolve(outputDir, ".appilots-checksum");
|
|
9994
11221
|
await writeFile(checksumFilePath, checksum, "utf-8");
|
|
9995
11222
|
console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
|
|
9996
11223
|
console.log("[MCPGenerator] Generation complete!");
|
|
@@ -9998,7 +11225,8 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
9998
11225
|
document,
|
|
9999
11226
|
filePath,
|
|
10000
11227
|
format: this.options.format,
|
|
10001
|
-
checksum
|
|
11228
|
+
checksum,
|
|
11229
|
+
...analyzed.diagnostics ? { diagnostics: analyzed.diagnostics } : {}
|
|
10002
11230
|
};
|
|
10003
11231
|
}
|
|
10004
11232
|
/**
|
|
@@ -10010,7 +11238,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10010
11238
|
* after calling `generate()`.
|
|
10011
11239
|
*/
|
|
10012
11240
|
static async readPreviousChecksum(outputDir) {
|
|
10013
|
-
const checksumFilePath =
|
|
11241
|
+
const checksumFilePath = path2__default.resolve(outputDir, ".appilots-checksum");
|
|
10014
11242
|
try {
|
|
10015
11243
|
const content = await readFile(checksumFilePath, "utf-8");
|
|
10016
11244
|
return content.trim() || null;
|
|
@@ -10029,7 +11257,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10029
11257
|
*/
|
|
10030
11258
|
async getProjectInfo() {
|
|
10031
11259
|
try {
|
|
10032
|
-
const packageJsonPath =
|
|
11260
|
+
const packageJsonPath = path2__default.resolve(this.analyzerConfig.rootDir, "package.json");
|
|
10033
11261
|
const packageJsonContent = await readFile(packageJsonPath, "utf-8");
|
|
10034
11262
|
const packageJson = JSON.parse(packageJsonContent);
|
|
10035
11263
|
return {
|
|
@@ -10112,12 +11340,12 @@ function getEnvOverrides(env = process.env) {
|
|
|
10112
11340
|
return trimmed ? trimmed : void 0;
|
|
10113
11341
|
};
|
|
10114
11342
|
return {
|
|
10115
|
-
apiKey: clean(env.APPILOTS_API_KEY),
|
|
11343
|
+
apiKey: clean(env.APPILOTS_PUBLISH_KEY) ?? clean(env.APPILOTS_API_KEY),
|
|
10116
11344
|
projectId: clean(env.APPILOTS_PROJECT_ID),
|
|
10117
11345
|
serverUrl: clean(env.APPILOTS_SERVER_URL)
|
|
10118
11346
|
};
|
|
10119
11347
|
}
|
|
10120
|
-
function loadConfig(onWarn) {
|
|
11348
|
+
function loadConfig(onWarn, options = {}) {
|
|
10121
11349
|
const configPath = getConfigPath();
|
|
10122
11350
|
const env = getEnvOverrides();
|
|
10123
11351
|
let fileConfig = {};
|
|
@@ -10132,6 +11360,18 @@ function loadConfig(onWarn) {
|
|
|
10132
11360
|
} else if (!env.apiKey) {
|
|
10133
11361
|
return null;
|
|
10134
11362
|
}
|
|
11363
|
+
const legacyWarnings = [];
|
|
11364
|
+
if (fileConfig.serverUrl === void 0) {
|
|
11365
|
+
for (const [alias, canonical] of Object.entries(KEY_ALIASES)) {
|
|
11366
|
+
if (canonical !== "serverUrl" || fileConfig[alias] === void 0) continue;
|
|
11367
|
+
fileConfig.serverUrl = fileConfig[alias];
|
|
11368
|
+
delete fileConfig[alias];
|
|
11369
|
+
legacyWarnings.push(
|
|
11370
|
+
`legacy key "${alias}" \u2014 using it as "serverUrl"; rename it in .appilotsrc`
|
|
11371
|
+
);
|
|
11372
|
+
break;
|
|
11373
|
+
}
|
|
11374
|
+
}
|
|
10135
11375
|
const merged = {
|
|
10136
11376
|
serverUrl: DEFAULT_SERVER_URL,
|
|
10137
11377
|
outputDir: ".appilots",
|
|
@@ -10143,6 +11383,24 @@ function loadConfig(onWarn) {
|
|
|
10143
11383
|
...env.serverUrl ? { serverUrl: env.serverUrl } : {}
|
|
10144
11384
|
};
|
|
10145
11385
|
const validation = validateConfig(merged);
|
|
11386
|
+
validation.warnings.unshift(...legacyWarnings);
|
|
11387
|
+
if (options.requireApiKey === false) {
|
|
11388
|
+
validation.errors = validation.errors.filter((e) => !e.startsWith("apiKey"));
|
|
11389
|
+
validation.valid = validation.errors.length === 0;
|
|
11390
|
+
}
|
|
11391
|
+
if (fileConfig.serverUrl === void 0 && env.serverUrl === void 0) {
|
|
11392
|
+
for (const key of Object.keys(fileConfig)) {
|
|
11393
|
+
if (KNOWN_CONFIG_KEYS.includes(key)) continue;
|
|
11394
|
+
if (suggestConfigKey(key) !== "serverUrl") continue;
|
|
11395
|
+
validation.warnings = validation.warnings.filter(
|
|
11396
|
+
(warning) => !warning.startsWith(`unknown key "${key}"`)
|
|
11397
|
+
);
|
|
11398
|
+
validation.errors.push(
|
|
11399
|
+
`unknown key "${key}" looks like "serverUrl" \u2014 not falling back to ${DEFAULT_SERVER_URL}; rename the key to "serverUrl"`
|
|
11400
|
+
);
|
|
11401
|
+
validation.valid = false;
|
|
11402
|
+
}
|
|
11403
|
+
}
|
|
10146
11404
|
if (onWarn) {
|
|
10147
11405
|
const source = configPath ?? "environment variables";
|
|
10148
11406
|
for (const warning of validation.warnings) {
|
|
@@ -10562,7 +11820,7 @@ var AppilotsAPIClient = class {
|
|
|
10562
11820
|
};
|
|
10563
11821
|
|
|
10564
11822
|
// src/version.ts
|
|
10565
|
-
var CLI_VERSION = "0.
|
|
11823
|
+
var CLI_VERSION = "0.11.3";
|
|
10566
11824
|
|
|
10567
11825
|
export { AppilotsAPIClient, CLI_VERSION, ComponentAnalyzer, DEFAULT_MANIFEST_FILENAME, DEFAULT_WEB_SCREEN_PATTERNS, FormAnalyzer, GenericPlatformAnalyzer, MCPGenerator, NavigationAnalyzer, ReactNativePlatformAnalyzer, ReactWebPlatformAnalyzer, ScreenAnalyzer, WebNavigationAnalyzer, WebScreenAnalyzer, formatMetadataWarnings, getConfigPath, getEnvOverrides, lintActionMetadata, loadConfig, loadManifest, mergeManifestNavigation, mergeManifestScreens, resolvePathToScreen, saveConfig, screenNameFromPath, validateConfig };
|
|
10568
11826
|
//# sourceMappingURL=index.mjs.map
|