@appilots/cli 0.11.0 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1756 -420
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +279 -5
- package/dist/index.d.ts +279 -5
- package/dist/index.js +1420 -195
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1414 -189
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.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({
|
|
@@ -8465,7 +9584,7 @@ external_exports.object({
|
|
|
8465
9584
|
version: external_exports.string().default("1.0"),
|
|
8466
9585
|
content: external_exports.record(external_exports.unknown())
|
|
8467
9586
|
});
|
|
8468
|
-
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
|
|
9587
|
+
var apiKeyScopeSchema = external_exports.enum(["sdk", "operator", "publish"]);
|
|
8469
9588
|
var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
|
|
8470
9589
|
external_exports.object({
|
|
8471
9590
|
name: external_exports.string().min(1).max(100),
|
|
@@ -8476,8 +9595,8 @@ external_exports.object({
|
|
|
8476
9595
|
scope: apiKeyScopeSchema.default("sdk"),
|
|
8477
9596
|
environment: apiKeyEnvironmentSchema.optional(),
|
|
8478
9597
|
expiresAt: external_exports.string().datetime().optional()
|
|
8479
|
-
}).refine((v) => v.scope
|
|
8480
|
-
message:
|
|
9598
|
+
}).refine((v) => v.scope === "operator" || !!v.projectId, {
|
|
9599
|
+
message: "projectId is required for SDK and publishing keys",
|
|
8481
9600
|
path: ["projectId"]
|
|
8482
9601
|
});
|
|
8483
9602
|
var boundedString = (max) => external_exports.string().max(max);
|
|
@@ -8502,6 +9621,15 @@ var snapshotInputSchema = external_exports.object({
|
|
|
8502
9621
|
type: boundedString(40).optional(),
|
|
8503
9622
|
required: external_exports.boolean().optional(),
|
|
8504
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(),
|
|
8505
9633
|
inModal: external_exports.boolean().optional()
|
|
8506
9634
|
}).passthrough();
|
|
8507
9635
|
var snapshotButtonSchema = external_exports.object({
|
|
@@ -8657,6 +9785,32 @@ var agentSnapshotSchema = external_exports.object({
|
|
|
8657
9785
|
* it. Optional: older SDKs never clamp and never send it.
|
|
8658
9786
|
*/
|
|
8659
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(),
|
|
8660
9814
|
/**
|
|
8661
9815
|
* How many of this screen's controls the client could name, split by
|
|
8662
9816
|
* `identityProvenance`. Diagnostic only — the relay never grounds an
|
|
@@ -8690,7 +9844,19 @@ var agentContextSchema = external_exports.object({
|
|
|
8690
9844
|
rootRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
|
|
8691
9845
|
currentRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
|
|
8692
9846
|
routeNames: external_exports.array(boundedString(200)).max(500).optional(),
|
|
8693
|
-
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()
|
|
8694
9860
|
}).passthrough().optional(),
|
|
8695
9861
|
screenMetadata: external_exports.object({
|
|
8696
9862
|
name: boundedString(200).optional(),
|
|
@@ -8796,7 +9962,7 @@ var formFillPayloadSchema = external_exports.object({
|
|
|
8796
9962
|
submitAfterFill: external_exports.boolean().optional()
|
|
8797
9963
|
}).passthrough();
|
|
8798
9964
|
var uiInteractionPayloadSchema = external_exports.object({
|
|
8799
|
-
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(),
|
|
8800
9966
|
// `targetId` is the canonical server/LLM field. SDK runtimes still
|
|
8801
9967
|
// accept `componentId` as a compatibility alias.
|
|
8802
9968
|
targetId: external_exports.string().min(1),
|
|
@@ -8964,8 +10130,15 @@ external_exports.object({
|
|
|
8964
10130
|
// issue #169 — the observation the server grounds targets against, now
|
|
8965
10131
|
// validated + bounded at the border instead of z.record(z.unknown()).
|
|
8966
10132
|
context: agentContextSchema.optional(),
|
|
8967
|
-
/**
|
|
8968
|
-
|
|
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()
|
|
8969
10142
|
});
|
|
8970
10143
|
var agentAccessLevelSchema = external_exports.enum(["read", "write", "none"]);
|
|
8971
10144
|
var screenPermissionSchema = external_exports.object({
|
|
@@ -9071,15 +10244,18 @@ var themeTokensSchema = external_exports.object({
|
|
|
9071
10244
|
*/
|
|
9072
10245
|
mode: external_exports.enum(["auto", "light", "dark"]).optional()
|
|
9073
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
|
+
});
|
|
9074
10250
|
external_exports.object({
|
|
9075
10251
|
/** Tone / persona instructions appended to the system prompt. */
|
|
9076
10252
|
personaPrompt: external_exports.string().max(2048).nullable(),
|
|
9077
10253
|
/** Display name in the chat header (e.g. "Aria"). */
|
|
9078
10254
|
assistantName: external_exports.string().max(60).nullable(),
|
|
9079
10255
|
/** URL or remote asset id for the avatar shown next to assistant turns. */
|
|
9080
|
-
assistantAvatar:
|
|
10256
|
+
assistantAvatar: imageSourceField.nullable(),
|
|
9081
10257
|
/** Emoji or image URL for the empty-state icon. Auto-detected by prefix. */
|
|
9082
|
-
emptyStateIcon:
|
|
10258
|
+
emptyStateIcon: imageSourceField.nullable(),
|
|
9083
10259
|
/** First-message text shown in the empty state. */
|
|
9084
10260
|
welcomeMessage: external_exports.string().max(500).nullable(),
|
|
9085
10261
|
/** Title rendered at the top of the chat. */
|
|
@@ -9093,20 +10269,20 @@ external_exports.object({
|
|
|
9093
10269
|
/** Chat-open FAB background color. Null falls back to theme.colors.primary. */
|
|
9094
10270
|
triggerButtonColor: hexColorSchema.nullable(),
|
|
9095
10271
|
/** Image URL rendered inside the FAB instead of the default chat-bubble icon. */
|
|
9096
|
-
triggerButtonImageUrl:
|
|
10272
|
+
triggerButtonImageUrl: imageSourceField.nullable()
|
|
9097
10273
|
});
|
|
9098
10274
|
external_exports.object({
|
|
9099
10275
|
personaPrompt: external_exports.string().max(2048).nullable().optional(),
|
|
9100
10276
|
assistantName: external_exports.string().max(60).nullable().optional(),
|
|
9101
|
-
assistantAvatar:
|
|
9102
|
-
emptyStateIcon:
|
|
10277
|
+
assistantAvatar: imageSourceField.nullable().optional(),
|
|
10278
|
+
emptyStateIcon: imageSourceField.nullable().optional(),
|
|
9103
10279
|
welcomeMessage: external_exports.string().max(500).nullable().optional(),
|
|
9104
10280
|
chatTitle: external_exports.string().max(60).nullable().optional(),
|
|
9105
10281
|
poweredByVisible: external_exports.boolean().optional(),
|
|
9106
10282
|
theme: themeTokensSchema.nullable().optional(),
|
|
9107
10283
|
defaultLocale: localeSchema.nullable().optional(),
|
|
9108
10284
|
triggerButtonColor: hexColorSchema.nullable().optional(),
|
|
9109
|
-
triggerButtonImageUrl:
|
|
10285
|
+
triggerButtonImageUrl: imageSourceField.nullable().optional()
|
|
9110
10286
|
}).strict();
|
|
9111
10287
|
var sandboxObservationSchema = external_exports.object({
|
|
9112
10288
|
route: external_exports.string().max(200).optional(),
|
|
@@ -9126,7 +10302,19 @@ var sandboxObservationSchema = external_exports.object({
|
|
|
9126
10302
|
type: external_exports.string().max(40).optional(),
|
|
9127
10303
|
placeholder: external_exports.string().max(200).optional(),
|
|
9128
10304
|
/** Inline validation state (SDK snapshot field). */
|
|
9129
|
-
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()
|
|
9130
10318
|
})
|
|
9131
10319
|
).max(100).optional(),
|
|
9132
10320
|
buttons: external_exports.array(
|
|
@@ -9605,7 +10793,7 @@ var manifestSchema = external_exports.object({
|
|
|
9605
10793
|
navigation: navigationGraphSchema.partial().optional()
|
|
9606
10794
|
}).passthrough();
|
|
9607
10795
|
async function loadManifest(rootDir, manifestPath) {
|
|
9608
|
-
const resolvedPath =
|
|
10796
|
+
const resolvedPath = path2__default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
|
|
9609
10797
|
let raw;
|
|
9610
10798
|
try {
|
|
9611
10799
|
raw = await readFile(resolvedPath, "utf-8");
|
|
@@ -9688,6 +10876,12 @@ function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
|
|
|
9688
10876
|
};
|
|
9689
10877
|
}
|
|
9690
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
|
+
|
|
9691
10885
|
// src/pipeline/enrichment.ts
|
|
9692
10886
|
function enrichScreenForAgent(screen) {
|
|
9693
10887
|
const targets = mergeTargets([
|
|
@@ -9764,7 +10958,10 @@ function mergeTargets(targets) {
|
|
|
9764
10958
|
for (const target of targets) {
|
|
9765
10959
|
if (!target.id) continue;
|
|
9766
10960
|
const existing = byId.get(target.id);
|
|
9767
|
-
byId.set(
|
|
10961
|
+
byId.set(
|
|
10962
|
+
target.id,
|
|
10963
|
+
existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target
|
|
10964
|
+
);
|
|
9768
10965
|
}
|
|
9769
10966
|
return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
9770
10967
|
}
|
|
@@ -9809,7 +11006,10 @@ function synthesizeFlows(screen, targets) {
|
|
|
9809
11006
|
intent: "destructive_action",
|
|
9810
11007
|
steps: [
|
|
9811
11008
|
{ type: "press", target: action.id, label: action.label },
|
|
9812
|
-
{
|
|
11009
|
+
{
|
|
11010
|
+
type: "confirm",
|
|
11011
|
+
description: "Wait for native or custom confirmation before continuing"
|
|
11012
|
+
},
|
|
9813
11013
|
{ type: "wait", description: describeWait(action) }
|
|
9814
11014
|
],
|
|
9815
11015
|
waitPolicy: waitPolicyForAction(action),
|
|
@@ -9824,10 +11024,19 @@ function synthesizeFlows(screen, targets) {
|
|
|
9824
11024
|
title: `Act on an item in ${collection.id}`,
|
|
9825
11025
|
intent: "list_action",
|
|
9826
11026
|
steps: [
|
|
9827
|
-
{
|
|
9828
|
-
|
|
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
|
+
}
|
|
9829
11036
|
],
|
|
9830
|
-
waitPolicy: {
|
|
11037
|
+
waitPolicy: {
|
|
11038
|
+
expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback"
|
|
11039
|
+
}
|
|
9831
11040
|
});
|
|
9832
11041
|
}
|
|
9833
11042
|
}
|
|
@@ -9835,25 +11044,32 @@ function synthesizeFlows(screen, targets) {
|
|
|
9835
11044
|
}
|
|
9836
11045
|
function waitPolicyForAction(action) {
|
|
9837
11046
|
const expectedOutcome = action.successSignal?.type === "goBack" ? "goBack" : action.appilotsInferred?.expectedOutcome ?? (action.targetScreen ? "navigation" : void 0);
|
|
9838
|
-
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);
|
|
9839
11051
|
return {
|
|
9840
11052
|
...expectedOutcome ? { expectedOutcome } : {},
|
|
9841
11053
|
...signals && signals.length > 0 ? { signals } : {},
|
|
9842
|
-
...
|
|
11054
|
+
...maxMs !== void 0 ? { maxMs } : {}
|
|
9843
11055
|
};
|
|
9844
11056
|
}
|
|
9845
11057
|
function describeWait(action) {
|
|
9846
11058
|
if (action.successSignal?.description) return action.successSignal.description;
|
|
9847
|
-
if (action.successSignal?.type === "goBack")
|
|
11059
|
+
if (action.successSignal?.type === "goBack")
|
|
11060
|
+
return "Wait for the app to return to the previous screen";
|
|
9848
11061
|
if (action.targetScreen) return `Wait for navigation to ${action.targetScreen}`;
|
|
9849
|
-
if (action.appilotsInferred?.expectedOutcome)
|
|
11062
|
+
if (action.appilotsInferred?.expectedOutcome)
|
|
11063
|
+
return `Wait for ${action.appilotsInferred.expectedOutcome}`;
|
|
9850
11064
|
return "Wait for the UI to settle";
|
|
9851
11065
|
}
|
|
9852
11066
|
function synthesizeAgentHints(screen, targets, flows) {
|
|
9853
11067
|
const preferredTargets = targets.filter((target) => ["submit", "button", "list"].includes(target.role)).slice(0, 8).map((target) => target.id);
|
|
9854
11068
|
const commonTasks = flows.slice(0, 6).map((flow) => flow.title);
|
|
9855
11069
|
const safetyNotes = screen.actions.filter((action) => action.destructive || action.requiresConfirmation).map((action) => `${action.id} requires confirmation`);
|
|
9856
|
-
const firstAsyncAction = screen.actions.find(
|
|
11070
|
+
const firstAsyncAction = screen.actions.find(
|
|
11071
|
+
(action) => action.appilotsInferred?.isAsyncTrigger || action.asyncBudgetMs !== void 0
|
|
11072
|
+
);
|
|
9857
11073
|
const hints = {
|
|
9858
11074
|
primaryGoal: synthesizePrimaryGoal(screen),
|
|
9859
11075
|
commonTasks,
|
|
@@ -9880,7 +11096,9 @@ function synthesizePrimaryGoal(screen) {
|
|
|
9880
11096
|
}
|
|
9881
11097
|
const fieldCount = uniqueFields.size;
|
|
9882
11098
|
if (fieldCount > 0) {
|
|
9883
|
-
const requiredCount = Array.from(uniqueFields.values()).filter(
|
|
11099
|
+
const requiredCount = Array.from(uniqueFields.values()).filter(
|
|
11100
|
+
(field) => field.required
|
|
11101
|
+
).length;
|
|
9884
11102
|
const fieldLabel = fieldCount === 1 ? "field" : "fields";
|
|
9885
11103
|
goals.push(
|
|
9886
11104
|
requiredCount > 0 ? `Complete a form with ${fieldCount} ${fieldLabel} (${requiredCount} required)` : `Complete a form with ${fieldCount} ${fieldLabel}`
|
|
@@ -9894,7 +11112,9 @@ function synthesizePrimaryGoal(screen) {
|
|
|
9894
11112
|
if (submitCount > 0) {
|
|
9895
11113
|
goals.push(`Submit ${submitCount === 1 ? "the primary form" : `${submitCount} forms/actions`}`);
|
|
9896
11114
|
}
|
|
9897
|
-
const asyncCount = screen.actions.filter(
|
|
11115
|
+
const asyncCount = screen.actions.filter(
|
|
11116
|
+
(action) => action.appilotsInferred?.isAsyncTrigger
|
|
11117
|
+
).length;
|
|
9898
11118
|
if (asyncCount > 0) {
|
|
9899
11119
|
goals.push(`Wait for ${asyncCount === 1 ? "async feedback" : "async action feedback"}`);
|
|
9900
11120
|
}
|
|
@@ -9969,7 +11189,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
9969
11189
|
const agentReadyScreens = mergedScreens.map(
|
|
9970
11190
|
(screen) => enrichScreenForAgent({
|
|
9971
11191
|
...screen,
|
|
9972
|
-
filePath: screen.filePath ?
|
|
11192
|
+
filePath: screen.filePath ? path2__default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
|
|
9973
11193
|
})
|
|
9974
11194
|
);
|
|
9975
11195
|
const projectInfo = await this.getProjectInfo();
|
|
@@ -9993,11 +11213,11 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
9993
11213
|
}
|
|
9994
11214
|
};
|
|
9995
11215
|
const serialized = JSON.stringify(document, null, 2);
|
|
9996
|
-
const checksum = this.calculateChecksum(
|
|
9997
|
-
const filePath =
|
|
11216
|
+
const checksum = this.calculateChecksum(serializeForChecksum(document));
|
|
11217
|
+
const filePath = path2__default.resolve(outputDir, `mcp-document.${this.options.format}`);
|
|
9998
11218
|
await writeFile(filePath, serialized, "utf-8");
|
|
9999
11219
|
console.log(`[MCPGenerator] Document written to: ${filePath}`);
|
|
10000
|
-
const checksumFilePath =
|
|
11220
|
+
const checksumFilePath = path2__default.resolve(outputDir, ".appilots-checksum");
|
|
10001
11221
|
await writeFile(checksumFilePath, checksum, "utf-8");
|
|
10002
11222
|
console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
|
|
10003
11223
|
console.log("[MCPGenerator] Generation complete!");
|
|
@@ -10005,7 +11225,8 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10005
11225
|
document,
|
|
10006
11226
|
filePath,
|
|
10007
11227
|
format: this.options.format,
|
|
10008
|
-
checksum
|
|
11228
|
+
checksum,
|
|
11229
|
+
...analyzed.diagnostics ? { diagnostics: analyzed.diagnostics } : {}
|
|
10009
11230
|
};
|
|
10010
11231
|
}
|
|
10011
11232
|
/**
|
|
@@ -10017,7 +11238,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10017
11238
|
* after calling `generate()`.
|
|
10018
11239
|
*/
|
|
10019
11240
|
static async readPreviousChecksum(outputDir) {
|
|
10020
|
-
const checksumFilePath =
|
|
11241
|
+
const checksumFilePath = path2__default.resolve(outputDir, ".appilots-checksum");
|
|
10021
11242
|
try {
|
|
10022
11243
|
const content = await readFile(checksumFilePath, "utf-8");
|
|
10023
11244
|
return content.trim() || null;
|
|
@@ -10036,7 +11257,7 @@ var MCPGenerator = class _MCPGenerator {
|
|
|
10036
11257
|
*/
|
|
10037
11258
|
async getProjectInfo() {
|
|
10038
11259
|
try {
|
|
10039
|
-
const packageJsonPath =
|
|
11260
|
+
const packageJsonPath = path2__default.resolve(this.analyzerConfig.rootDir, "package.json");
|
|
10040
11261
|
const packageJsonContent = await readFile(packageJsonPath, "utf-8");
|
|
10041
11262
|
const packageJson = JSON.parse(packageJsonContent);
|
|
10042
11263
|
return {
|
|
@@ -10119,12 +11340,12 @@ function getEnvOverrides(env = process.env) {
|
|
|
10119
11340
|
return trimmed ? trimmed : void 0;
|
|
10120
11341
|
};
|
|
10121
11342
|
return {
|
|
10122
|
-
apiKey: clean(env.APPILOTS_API_KEY),
|
|
11343
|
+
apiKey: clean(env.APPILOTS_PUBLISH_KEY) ?? clean(env.APPILOTS_API_KEY),
|
|
10123
11344
|
projectId: clean(env.APPILOTS_PROJECT_ID),
|
|
10124
11345
|
serverUrl: clean(env.APPILOTS_SERVER_URL)
|
|
10125
11346
|
};
|
|
10126
11347
|
}
|
|
10127
|
-
function loadConfig(onWarn) {
|
|
11348
|
+
function loadConfig(onWarn, options = {}) {
|
|
10128
11349
|
const configPath = getConfigPath();
|
|
10129
11350
|
const env = getEnvOverrides();
|
|
10130
11351
|
let fileConfig = {};
|
|
@@ -10163,6 +11384,10 @@ function loadConfig(onWarn) {
|
|
|
10163
11384
|
};
|
|
10164
11385
|
const validation = validateConfig(merged);
|
|
10165
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
|
+
}
|
|
10166
11391
|
if (fileConfig.serverUrl === void 0 && env.serverUrl === void 0) {
|
|
10167
11392
|
for (const key of Object.keys(fileConfig)) {
|
|
10168
11393
|
if (KNOWN_CONFIG_KEYS.includes(key)) continue;
|
|
@@ -10595,7 +11820,7 @@ var AppilotsAPIClient = class {
|
|
|
10595
11820
|
};
|
|
10596
11821
|
|
|
10597
11822
|
// src/version.ts
|
|
10598
|
-
var CLI_VERSION = "0.11.
|
|
11823
|
+
var CLI_VERSION = "0.11.3";
|
|
10599
11824
|
|
|
10600
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 };
|
|
10601
11826
|
//# sourceMappingURL=index.mjs.map
|