swipium 1.0.0
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/LICENSE +21 -0
- package/README.md +295 -0
- package/dist/appMap/automationLink.js +74 -0
- package/dist/appMap/automationLink.js.map +1 -0
- package/dist/appMap/build.js +193 -0
- package/dist/appMap/build.js.map +1 -0
- package/dist/appMap/codeIndex.js +73 -0
- package/dist/appMap/codeIndex.js.map +1 -0
- package/dist/appMap/featureModel.js +83 -0
- package/dist/appMap/featureModel.js.map +1 -0
- package/dist/appMap/firstRunApply.js +131 -0
- package/dist/appMap/firstRunApply.js.map +1 -0
- package/dist/appMap/fsWalk.js +75 -0
- package/dist/appMap/fsWalk.js.map +1 -0
- package/dist/appMap/issues.js +227 -0
- package/dist/appMap/issues.js.map +1 -0
- package/dist/appMap/migrations.js +87 -0
- package/dist/appMap/migrations.js.map +1 -0
- package/dist/appMap/prelaunch.js +66 -0
- package/dist/appMap/prelaunch.js.map +1 -0
- package/dist/appMap/projectRegistry.js +70 -0
- package/dist/appMap/projectRegistry.js.map +1 -0
- package/dist/appMap/provenance.js +52 -0
- package/dist/appMap/provenance.js.map +1 -0
- package/dist/appMap/query.js +118 -0
- package/dist/appMap/query.js.map +1 -0
- package/dist/appMap/runtimeMerge.js +207 -0
- package/dist/appMap/runtimeMerge.js.map +1 -0
- package/dist/appMap/schema.js +65 -0
- package/dist/appMap/schema.js.map +1 -0
- package/dist/appMap/screenMatch.js +75 -0
- package/dist/appMap/screenMatch.js.map +1 -0
- package/dist/appMap/staticScan.js +614 -0
- package/dist/appMap/staticScan.js.map +1 -0
- package/dist/appMap/store.js +173 -0
- package/dist/appMap/store.js.map +1 -0
- package/dist/appMap/tsAstScan.js +184 -0
- package/dist/appMap/tsAstScan.js.map +1 -0
- package/dist/artifacts/bundletool.js +199 -0
- package/dist/artifacts/bundletool.js.map +1 -0
- package/dist/artifacts/resolve.js +388 -0
- package/dist/artifacts/resolve.js.map +1 -0
- package/dist/automationGen/appiumModel.js +129 -0
- package/dist/automationGen/appiumModel.js.map +1 -0
- package/dist/automationGen/ciEmitter.js +80 -0
- package/dist/automationGen/ciEmitter.js.map +1 -0
- package/dist/automationGen/jsEmitter.js +440 -0
- package/dist/automationGen/jsEmitter.js.map +1 -0
- package/dist/automationGen/packagePatch.js +56 -0
- package/dist/automationGen/packagePatch.js.map +1 -0
- package/dist/automationGen/projectProfile.js +317 -0
- package/dist/automationGen/projectProfile.js.map +1 -0
- package/dist/automationGen/pythonEmitter.js +386 -0
- package/dist/automationGen/pythonEmitter.js.map +1 -0
- package/dist/automationGen/readmeEmitter.js +66 -0
- package/dist/automationGen/readmeEmitter.js.map +1 -0
- package/dist/automationGen/suitePlan.js +94 -0
- package/dist/automationGen/suitePlan.js.map +1 -0
- package/dist/automationGen/validation.js +150 -0
- package/dist/automationGen/validation.js.map +1 -0
- package/dist/build/parseBuildLog.js +119 -0
- package/dist/build/parseBuildLog.js.map +1 -0
- package/dist/build/plan.js +150 -0
- package/dist/build/plan.js.map +1 -0
- package/dist/ci/preflight.js +85 -0
- package/dist/ci/preflight.js.map +1 -0
- package/dist/cli/init.js +105 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/scan.js +85 -0
- package/dist/cli/scan.js.map +1 -0
- package/dist/cli/suite.js +85 -0
- package/dist/cli/suite.js.map +1 -0
- package/dist/cli/verify.js +62 -0
- package/dist/cli/verify.js.map +1 -0
- package/dist/consent/consent.js +43 -0
- package/dist/consent/consent.js.map +1 -0
- package/dist/context/detect.js +156 -0
- package/dist/context/detect.js.map +1 -0
- package/dist/context/findApps.js +198 -0
- package/dist/context/findApps.js.map +1 -0
- package/dist/context/projectRoot.js +41 -0
- package/dist/context/projectRoot.js.map +1 -0
- package/dist/context/scan.js +167 -0
- package/dist/context/scan.js.map +1 -0
- package/dist/core/target.js +115 -0
- package/dist/core/target.js.map +1 -0
- package/dist/core/targetPlan.js +140 -0
- package/dist/core/targetPlan.js.map +1 -0
- package/dist/drivers/DirectDriver.js +228 -0
- package/dist/drivers/DirectDriver.js.map +1 -0
- package/dist/drivers/Driver.js +4 -0
- package/dist/drivers/Driver.js.map +1 -0
- package/dist/drivers/SimctlDriver.js +127 -0
- package/dist/drivers/SimctlDriver.js.map +1 -0
- package/dist/drivers/WdaDriver.js +250 -0
- package/dist/drivers/WdaDriver.js.map +1 -0
- package/dist/explore/candidates.js +93 -0
- package/dist/explore/candidates.js.map +1 -0
- package/dist/explore/graph.js +116 -0
- package/dist/explore/graph.js.map +1 -0
- package/dist/explore/memory.js +35 -0
- package/dist/explore/memory.js.map +1 -0
- package/dist/explore/planner.js +105 -0
- package/dist/explore/planner.js.map +1 -0
- package/dist/explore/policy.js +85 -0
- package/dist/explore/policy.js.map +1 -0
- package/dist/explore/runner.js +475 -0
- package/dist/explore/runner.js.map +1 -0
- package/dist/explore/signatures.js +35 -0
- package/dist/explore/signatures.js.map +1 -0
- package/dist/explore/suite.js +55 -0
- package/dist/explore/suite.js.map +1 -0
- package/dist/featureTesting/executionBootstrap.js +140 -0
- package/dist/featureTesting/executionBootstrap.js.map +1 -0
- package/dist/firstRun/authStateMachine.js +83 -0
- package/dist/firstRun/authStateMachine.js.map +1 -0
- package/dist/firstRun/classifyScreen.js +201 -0
- package/dist/firstRun/classifyScreen.js.map +1 -0
- package/dist/firstRun/firstRunPlanner.js +207 -0
- package/dist/firstRun/firstRunPlanner.js.map +1 -0
- package/dist/firstRun/firstRunRunner.js +331 -0
- package/dist/firstRun/firstRunRunner.js.map +1 -0
- package/dist/firstRun/generatedDataPolicy.js +215 -0
- package/dist/firstRun/generatedDataPolicy.js.map +1 -0
- package/dist/firstRun/inputPlanner.js +67 -0
- package/dist/firstRun/inputPlanner.js.map +1 -0
- package/dist/firstRun/onboardingStateMachine.js +46 -0
- package/dist/firstRun/onboardingStateMachine.js.map +1 -0
- package/dist/firstRun/paywallPolicy.js +54 -0
- package/dist/firstRun/paywallPolicy.js.map +1 -0
- package/dist/firstRun/types.js +7 -0
- package/dist/firstRun/types.js.map +1 -0
- package/dist/fixtures/catalog.js +131 -0
- package/dist/fixtures/catalog.js.map +1 -0
- package/dist/flows/discover.js +18 -0
- package/dist/flows/discover.js.map +1 -0
- package/dist/flows/generate.js +211 -0
- package/dist/flows/generate.js.map +1 -0
- package/dist/flows/lint.js +97 -0
- package/dist/flows/lint.js.map +1 -0
- package/dist/flows/pack.js +68 -0
- package/dist/flows/pack.js.map +1 -0
- package/dist/flows/run.js +738 -0
- package/dist/flows/run.js.map +1 -0
- package/dist/flows/schema.js +240 -0
- package/dist/flows/schema.js.map +1 -0
- package/dist/flows/seedExec.js +108 -0
- package/dist/flows/seedExec.js.map +1 -0
- package/dist/flows/templates.js +155 -0
- package/dist/flows/templates.js.map +1 -0
- package/dist/flows/waitGuards.js +12 -0
- package/dist/flows/waitGuards.js.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/ios/signing.js +182 -0
- package/dist/ios/signing.js.map +1 -0
- package/dist/issues/classify.js +186 -0
- package/dist/issues/classify.js.map +1 -0
- package/dist/issues/fingerprint.js +130 -0
- package/dist/issues/fingerprint.js.map +1 -0
- package/dist/issues/index.js +240 -0
- package/dist/issues/index.js.map +1 -0
- package/dist/issues/metrics.js +155 -0
- package/dist/issues/metrics.js.map +1 -0
- package/dist/issues/recurrence.js +193 -0
- package/dist/issues/recurrence.js.map +1 -0
- package/dist/issues/report.js +110 -0
- package/dist/issues/report.js.map +1 -0
- package/dist/issues/reportBridge.js +119 -0
- package/dist/issues/reportBridge.js.map +1 -0
- package/dist/issues/schema.js +90 -0
- package/dist/issues/schema.js.map +1 -0
- package/dist/issues/sourceRevision.js +108 -0
- package/dist/issues/sourceRevision.js.map +1 -0
- package/dist/issues/store.js +155 -0
- package/dist/issues/store.js.map +1 -0
- package/dist/lib/android.js +278 -0
- package/dist/lib/android.js.map +1 -0
- package/dist/lib/commandTemplate.js +55 -0
- package/dist/lib/commandTemplate.js.map +1 -0
- package/dist/lib/coordSpace.js +23 -0
- package/dist/lib/coordSpace.js.map +1 -0
- package/dist/lib/device.js +87 -0
- package/dist/lib/device.js.map +1 -0
- package/dist/lib/gitignore.js +22 -0
- package/dist/lib/gitignore.js.map +1 -0
- package/dist/lib/image.js +97 -0
- package/dist/lib/image.js.map +1 -0
- package/dist/lib/logger.js +8 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/metroState.js +45 -0
- package/dist/lib/metroState.js.map +1 -0
- package/dist/lib/needsInput.js +201 -0
- package/dist/lib/needsInput.js.map +1 -0
- package/dist/lib/png.js +103 -0
- package/dist/lib/png.js.map +1 -0
- package/dist/lib/redact.js +21 -0
- package/dist/lib/redact.js.map +1 -0
- package/dist/lib/result.js +87 -0
- package/dist/lib/result.js.map +1 -0
- package/dist/lib/schemaHash.js +65 -0
- package/dist/lib/schemaHash.js.map +1 -0
- package/dist/lib/sensitive.js +13 -0
- package/dist/lib/sensitive.js.map +1 -0
- package/dist/lib/simctl.js +149 -0
- package/dist/lib/simctl.js.map +1 -0
- package/dist/lib/spawn.js +144 -0
- package/dist/lib/spawn.js.map +1 -0
- package/dist/lib/wda.js +347 -0
- package/dist/lib/wda.js.map +1 -0
- package/dist/lib/wdaConfig.js +69 -0
- package/dist/lib/wdaConfig.js.map +1 -0
- package/dist/lib/wdaTune.js +58 -0
- package/dist/lib/wdaTune.js.map +1 -0
- package/dist/oracle/auth.js +22 -0
- package/dist/oracle/auth.js.map +1 -0
- package/dist/oracle/failures.js +193 -0
- package/dist/oracle/failures.js.map +1 -0
- package/dist/oracle/health.js +123 -0
- package/dist/oracle/health.js.map +1 -0
- package/dist/oracle/record.js +36 -0
- package/dist/oracle/record.js.map +1 -0
- package/dist/orchestration/envelope.js +52 -0
- package/dist/orchestration/envelope.js.map +1 -0
- package/dist/orchestration/goal.js +98 -0
- package/dist/orchestration/goal.js.map +1 -0
- package/dist/plan/plan.js +124 -0
- package/dist/plan/plan.js.map +1 -0
- package/dist/prompts/index.js +63 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/report/artifactManifest.js +44 -0
- package/dist/report/artifactManifest.js.map +1 -0
- package/dist/report/coverage.js +60 -0
- package/dist/report/coverage.js.map +1 -0
- package/dist/report/evidence.js +84 -0
- package/dist/report/evidence.js.map +1 -0
- package/dist/report/export.js +447 -0
- package/dist/report/export.js.map +1 -0
- package/dist/report/flake.js +61 -0
- package/dist/report/flake.js.map +1 -0
- package/dist/report/history.js +564 -0
- package/dist/report/history.js.map +1 -0
- package/dist/report/policy.js +98 -0
- package/dist/report/policy.js.map +1 -0
- package/dist/report/qaLevel.js +96 -0
- package/dist/report/qaLevel.js.map +1 -0
- package/dist/report/readiness.js +213 -0
- package/dist/report/readiness.js.map +1 -0
- package/dist/report/summary.js +42 -0
- package/dist/report/summary.js.map +1 -0
- package/dist/report/testCatalog.js +93 -0
- package/dist/report/testCatalog.js.map +1 -0
- package/dist/report/timing.js +37 -0
- package/dist/report/timing.js.map +1 -0
- package/dist/server.js +180 -0
- package/dist/server.js.map +1 -0
- package/dist/services/automationGenerate.js +60 -0
- package/dist/services/automationGenerate.js.map +1 -0
- package/dist/services/build.js +66 -0
- package/dist/services/build.js.map +1 -0
- package/dist/services/preflight.js +51 -0
- package/dist/services/preflight.js.map +1 -0
- package/dist/services/prepareAndroid.js +183 -0
- package/dist/services/prepareAndroid.js.map +1 -0
- package/dist/services/prepareIos.js +162 -0
- package/dist/services/prepareIos.js.map +1 -0
- package/dist/services/report.js +537 -0
- package/dist/services/report.js.map +1 -0
- package/dist/services/smoke.js +120 -0
- package/dist/services/smoke.js.map +1 -0
- package/dist/services/suiteGenerate.js +123 -0
- package/dist/services/suiteGenerate.js.map +1 -0
- package/dist/services/testSuiteKnowledge.js +150 -0
- package/dist/services/testSuiteKnowledge.js.map +1 -0
- package/dist/session/attach.js +47 -0
- package/dist/session/attach.js.map +1 -0
- package/dist/session/progress.js +56 -0
- package/dist/session/progress.js.map +1 -0
- package/dist/session/store.js +329 -0
- package/dist/session/store.js.map +1 -0
- package/dist/snapshot/overlays.js +132 -0
- package/dist/snapshot/overlays.js.map +1 -0
- package/dist/snapshot/parse.js +204 -0
- package/dist/snapshot/parse.js.map +1 -0
- package/dist/snapshot/present.js +12 -0
- package/dist/snapshot/present.js.map +1 -0
- package/dist/snapshot/settle.js +36 -0
- package/dist/snapshot/settle.js.map +1 -0
- package/dist/state/profile.js +318 -0
- package/dist/state/profile.js.map +1 -0
- package/dist/suite/compile.js +176 -0
- package/dist/suite/compile.js.map +1 -0
- package/dist/suite/lint.js +45 -0
- package/dist/suite/lint.js.map +1 -0
- package/dist/suite/pom.js +285 -0
- package/dist/suite/pom.js.map +1 -0
- package/dist/suite/testcase.js +87 -0
- package/dist/suite/testcase.js.map +1 -0
- package/dist/testSuite/generator.js +222 -0
- package/dist/testSuite/generator.js.map +1 -0
- package/dist/testSuite/history.js +44 -0
- package/dist/testSuite/history.js.map +1 -0
- package/dist/testSuite/issueLinks.js +96 -0
- package/dist/testSuite/issueLinks.js.map +1 -0
- package/dist/testSuite/merge.js +190 -0
- package/dist/testSuite/merge.js.map +1 -0
- package/dist/testSuite/schema.js +86 -0
- package/dist/testSuite/schema.js.map +1 -0
- package/dist/testSuite/store.js +116 -0
- package/dist/testSuite/store.js.map +1 -0
- package/dist/testSuite/traceability.js +55 -0
- package/dist/testSuite/traceability.js.map +1 -0
- package/dist/tools/act.js +457 -0
- package/dist/tools/act.js.map +1 -0
- package/dist/tools/agent.js +225 -0
- package/dist/tools/agent.js.map +1 -0
- package/dist/tools/appMap.js +317 -0
- package/dist/tools/appMap.js.map +1 -0
- package/dist/tools/assertVisual.js +74 -0
- package/dist/tools/assertVisual.js.map +1 -0
- package/dist/tools/automationGenerate.js +394 -0
- package/dist/tools/automationGenerate.js.map +1 -0
- package/dist/tools/capabilities.js +114 -0
- package/dist/tools/capabilities.js.map +1 -0
- package/dist/tools/clearOverlay.js +150 -0
- package/dist/tools/clearOverlay.js.map +1 -0
- package/dist/tools/detectContext.js +41 -0
- package/dist/tools/detectContext.js.map +1 -0
- package/dist/tools/doctor.js +116 -0
- package/dist/tools/doctor.js.map +1 -0
- package/dist/tools/explore.js +289 -0
- package/dist/tools/explore.js.map +1 -0
- package/dist/tools/firstRun.js +136 -0
- package/dist/tools/firstRun.js.map +1 -0
- package/dist/tools/flow.js +331 -0
- package/dist/tools/flow.js.map +1 -0
- package/dist/tools/flowGenerate.js +55 -0
- package/dist/tools/flowGenerate.js.map +1 -0
- package/dist/tools/getArtifact.js +39 -0
- package/dist/tools/getArtifact.js.map +1 -0
- package/dist/tools/health.js +41 -0
- package/dist/tools/health.js.map +1 -0
- package/dist/tools/ios.js +492 -0
- package/dist/tools/ios.js.map +1 -0
- package/dist/tools/jobs.js +20 -0
- package/dist/tools/jobs.js.map +1 -0
- package/dist/tools/network.js +50 -0
- package/dist/tools/network.js.map +1 -0
- package/dist/tools/note.js +69 -0
- package/dist/tools/note.js.map +1 -0
- package/dist/tools/plan.js +49 -0
- package/dist/tools/plan.js.map +1 -0
- package/dist/tools/prepareIosTarget.js +99 -0
- package/dist/tools/prepareIosTarget.js.map +1 -0
- package/dist/tools/prepareTarget.js +257 -0
- package/dist/tools/prepareTarget.js.map +1 -0
- package/dist/tools/report.js +42 -0
- package/dist/tools/report.js.map +1 -0
- package/dist/tools/screenRecord.js +24 -0
- package/dist/tools/screenRecord.js.map +1 -0
- package/dist/tools/screenshot.js +63 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/smoke.js +39 -0
- package/dist/tools/smoke.js.map +1 -0
- package/dist/tools/snapshot.js +129 -0
- package/dist/tools/snapshot.js.map +1 -0
- package/dist/tools/startSession.js +169 -0
- package/dist/tools/startSession.js.map +1 -0
- package/dist/tools/state.js +87 -0
- package/dist/tools/state.js.map +1 -0
- package/dist/tools/suite.js +412 -0
- package/dist/tools/suite.js.map +1 -0
- package/dist/tools/testThis.js +774 -0
- package/dist/tools/testThis.js.map +1 -0
- package/dist/tools/wda.js +477 -0
- package/dist/tools/wda.js.map +1 -0
- package/dist/version.js +62 -0
- package/dist/version.js.map +1 -0
- package/dist/visual/ocr.js +72 -0
- package/dist/visual/ocr.js.map +1 -0
- package/dist/visual/provider.js +75 -0
- package/dist/visual/provider.js.map +1 -0
- package/docs/README.md +19 -0
- package/docs/assets/swipium-lockup-ink-on-light.png +0 -0
- package/docs/mcp-server.md +135 -0
- package/docs/tools.md +130 -0
- package/package.json +70 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
// Framework-aware STATIC scanner (SWIPIUM-REQ-01). Additive + confidence-based: it never fails a
|
|
2
|
+
// run because one framework can't be parsed — parser problems are recorded in `parserNotes` and
|
|
3
|
+
// surface as reduced confidence. For JS/TS the TypeScript compiler API (Vision Gap Fix 10) is the
|
|
4
|
+
// PRIMARY parser — it resolves route-constant references, navigator <Stack.Screen> declarations, and
|
|
5
|
+
// multiline default-export components that regex misses — with line-aware regex as the fallback when
|
|
6
|
+
// the `typescript` package is not resolvable. Each scanner is independent so the surface can grow.
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { basename, join } from 'node:path';
|
|
9
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
10
|
+
import { parse as parseYaml } from 'yaml';
|
|
11
|
+
import { detectFramework } from '../context/detect.js';
|
|
12
|
+
import { fileHash, rel, readTextSafe, walkFiles } from './fsWalk.js';
|
|
13
|
+
import { scanTsSource, tsAstAvailable } from './tsAstScan.js';
|
|
14
|
+
const AUTH_LIB_RE = /(next-auth|expo-auth-session|react-native-app-auth|amazon-cognito|@clerk|@supabase\/supabase|firebase\/auth|@react-native-firebase\/auth|@auth0|msal|react-native-keychain|@okta)/i;
|
|
15
|
+
const FORM_LIB_RE = /(react-hook-form|formik|yup|zod|@hookform|final-form)/i;
|
|
16
|
+
const PAYWALL_LIB_RE = /(react-native-purchases|expo-in-app-purchases|react-native-iap|revenuecat|@stripe\/stripe-react-native|stripe)/i;
|
|
17
|
+
const ONBOARDING_RE = /(onboard|welcome|intro|get-?started|walkthrough|tutorial)/i;
|
|
18
|
+
function readJson(p) {
|
|
19
|
+
const t = readTextSafe(p);
|
|
20
|
+
if (!t)
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(t);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function uniq(values) {
|
|
30
|
+
return [...new Set(values.filter(Boolean))];
|
|
31
|
+
}
|
|
32
|
+
function slug(value) {
|
|
33
|
+
return value
|
|
34
|
+
.replace(/\.[a-zA-Z]+$/, '')
|
|
35
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
36
|
+
.replace(/^-+|-+$/g, '')
|
|
37
|
+
.toLowerCase() || 'screen';
|
|
38
|
+
}
|
|
39
|
+
function depsOf(root) {
|
|
40
|
+
const pkg = readJson(join(root, 'package.json'));
|
|
41
|
+
if (!pkg)
|
|
42
|
+
return {};
|
|
43
|
+
return { ...pkg.dependencies, ...pkg.devDependencies };
|
|
44
|
+
}
|
|
45
|
+
/** Fingerprint the files that define app structure (config + entry/route files) for incremental rescans. */
|
|
46
|
+
function fingerprint(root, files, generatedAt) {
|
|
47
|
+
const out = { generatedAt, files: [] };
|
|
48
|
+
for (const f of files.slice(0, 400)) {
|
|
49
|
+
const h = fileHash(f);
|
|
50
|
+
if (h)
|
|
51
|
+
out.files.push({ path: rel(root, f), hash: h.hash, mtimeMs: h.mtimeMs });
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Expo / React Native
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
function scanExpoRouter(root, topo) {
|
|
59
|
+
// Expo Router: file-system routes under app/** (or src/app/**).
|
|
60
|
+
const appDirs = [join(root, 'app'), join(root, 'src', 'app')].filter((d) => existsSync(d));
|
|
61
|
+
if (!appDirs.length)
|
|
62
|
+
return;
|
|
63
|
+
topo.router = topo.router ?? 'expo-router';
|
|
64
|
+
for (const dir of appDirs) {
|
|
65
|
+
const files = walkFiles(dir, { exts: ['.tsx', '.ts', '.jsx', '.js'] });
|
|
66
|
+
for (const f of files) {
|
|
67
|
+
const base = basename(f);
|
|
68
|
+
if (/^_/.test(base) && !/^_layout\./.test(base))
|
|
69
|
+
continue; // skip private files except layouts
|
|
70
|
+
const route = rel(dir, f).replace(/\.(tsx?|jsx?)$/, '');
|
|
71
|
+
const isLayout = /_layout$/.test(route);
|
|
72
|
+
const isNotFound = /\+not-found$/.test(route);
|
|
73
|
+
const isModal = /modal/i.test(route);
|
|
74
|
+
const isDynamic = /\[.+\]/.test(route);
|
|
75
|
+
const isTab = /\(tabs?\)/.test(route);
|
|
76
|
+
const groupless = route.replace(/\([^)]*\)\//g, '').replace(/\/index$/, '') || 'index';
|
|
77
|
+
const kind = isLayout ? 'layout' : isNotFound ? 'not_found' : isModal ? 'modal' : isTab ? 'tab' : 'route';
|
|
78
|
+
const reasons = ['expo_router_file'];
|
|
79
|
+
if (isDynamic)
|
|
80
|
+
reasons.push('dynamic_route');
|
|
81
|
+
if (isTab)
|
|
82
|
+
reasons.push('tab_group');
|
|
83
|
+
topo.screens.push({
|
|
84
|
+
id: `route:${slug(groupless)}`,
|
|
85
|
+
name: groupless,
|
|
86
|
+
route: '/' + groupless.replace(/index$/, ''),
|
|
87
|
+
kind,
|
|
88
|
+
sourceFiles: [rel(root, f)],
|
|
89
|
+
navParams: isDynamic ? (route.match(/\[([^\]]+)\]/g)?.map((m) => m.replace(/[[\]]/g, '')) ?? []) : [],
|
|
90
|
+
confidence: isLayout ? 0.6 : 0.85,
|
|
91
|
+
reasons,
|
|
92
|
+
});
|
|
93
|
+
if (!isLayout && !isNotFound) {
|
|
94
|
+
topo.edges.push({ to: `route:${slug(groupless)}`, kind: 'route_declaration', evidence: rel(root, f), confidence: 0.8 });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function scanReactNavigation(root, topo, jsFiles) {
|
|
100
|
+
const navSignals = /createNativeStackNavigator|createStackNavigator|createBottomTabNavigator|createDrawerNavigator|NavigationContainer/;
|
|
101
|
+
let found = false;
|
|
102
|
+
for (const f of jsFiles) {
|
|
103
|
+
const text = readTextSafe(f);
|
|
104
|
+
if (!text || !navSignals.test(text))
|
|
105
|
+
continue;
|
|
106
|
+
found = true;
|
|
107
|
+
const relPath = rel(root, f);
|
|
108
|
+
// Stack.Screen / Tab.Screen / Drawer.Screen name= declarations.
|
|
109
|
+
const screenRe = /<([A-Za-z0-9_]+)\.Screen\b[^>]*\bname=(?:["'`]([^"'`]+)["'`]|\{["'`]?([^}"'`]+)["'`]?\})/g;
|
|
110
|
+
let m;
|
|
111
|
+
while ((m = screenRe.exec(text))) {
|
|
112
|
+
const navType = m[1];
|
|
113
|
+
const name = (m[2] ?? m[3] ?? '').trim();
|
|
114
|
+
if (!name)
|
|
115
|
+
continue;
|
|
116
|
+
const kindMap = { Tab: 'tab', Drawer: 'drawer', Stack: 'stack' };
|
|
117
|
+
const navKind = Object.entries(kindMap).find(([k]) => navType.includes(k))?.[1] ?? 'navigation';
|
|
118
|
+
const id = `screen:${slug(name)}`;
|
|
119
|
+
if (!topo.screens.some((s) => s.id === id)) {
|
|
120
|
+
topo.screens.push({ id, name, route: name, kind: 'screen', sourceFiles: [relPath], confidence: 0.8, reasons: ['react_navigation_screen', `nav_${navKind}`] });
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const existing = topo.screens.find((s) => s.id === id);
|
|
124
|
+
existing.sourceFiles = uniq([...existing.sourceFiles, relPath]);
|
|
125
|
+
}
|
|
126
|
+
topo.edges.push({ to: id, kind: navKind, evidence: `${relPath}:${navType}.Screen`, confidence: 0.7 });
|
|
127
|
+
}
|
|
128
|
+
// navigation.navigate('Foo') / .push('Foo') call sites → probable edges.
|
|
129
|
+
const navCallRe = /\.(navigate|push|replace)\(\s*["'`]([^"'`]+)["'`]/g;
|
|
130
|
+
while ((m = navCallRe.exec(text))) {
|
|
131
|
+
const to = `screen:${slug(m[2])}`;
|
|
132
|
+
topo.edges.push({ to, kind: 'navigation', evidence: `${relPath}:navigation.${m[1]}`, confidence: 0.5 });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (found && !topo.router)
|
|
136
|
+
topo.router = 'react-navigation';
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Vision Gap Fix 10 — AST pass over JS/TS/TSX files. Folds AST-discovered navigator screens (with
|
|
140
|
+
* resolved route-constant names + bound components), exported screen components, navigation edges, and
|
|
141
|
+
* route constants into the topology. Bounded; parse failures become parserNotes, never hard failures.
|
|
142
|
+
*/
|
|
143
|
+
function scanTsAst(root, topo, jsFiles) {
|
|
144
|
+
if (!tsAstAvailable()) {
|
|
145
|
+
topo.parserNotes.push('TypeScript compiler API unavailable — JS/TS scanned with regex fallback only');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
let parsedFiles = 0;
|
|
149
|
+
let failedFiles = 0;
|
|
150
|
+
const MAX = 1500;
|
|
151
|
+
const MAX_BYTES = 512 * 1024;
|
|
152
|
+
for (const f of jsFiles.slice(0, MAX)) {
|
|
153
|
+
const relPath = rel(root, f);
|
|
154
|
+
if (/\.(test|spec|stories)\./.test(relPath))
|
|
155
|
+
continue;
|
|
156
|
+
const text = readTextSafe(f);
|
|
157
|
+
if (!text || text.length > MAX_BYTES)
|
|
158
|
+
continue;
|
|
159
|
+
const res = scanTsSource(relPath, text);
|
|
160
|
+
if (!res.parsed) {
|
|
161
|
+
failedFiles++;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
parsedFiles++;
|
|
165
|
+
// Fold ONLY explicit navigator (<Stack/Tab/Drawer.Screen>) declarations into the topology — these
|
|
166
|
+
// carry an authoritative route name (incl. constant-resolved) that regex misses. Generic component
|
|
167
|
+
// / default-export screens are intentionally NOT folded here to avoid duplicating expo-router's
|
|
168
|
+
// file-based routes and the screen-filename convention; scanTsSource still exposes them to callers.
|
|
169
|
+
for (const s of res.screens.filter((x) => x.reasons.includes('ast_navigator_screen'))) {
|
|
170
|
+
const id = `screen:${slug(s.name)}`;
|
|
171
|
+
const existing = topo.screens.find((x) => x.id === id);
|
|
172
|
+
if (existing) {
|
|
173
|
+
existing.sourceFiles = uniq([...existing.sourceFiles, relPath]);
|
|
174
|
+
existing.confidence = Math.max(existing.confidence, s.confidence);
|
|
175
|
+
existing.reasons = uniq([...existing.reasons, ...s.reasons]);
|
|
176
|
+
if (s.route && !existing.route)
|
|
177
|
+
existing.route = s.route;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
topo.screens.push({ id, name: s.name, route: s.route, kind: 'screen', sourceFiles: [relPath], confidence: s.confidence, reasons: ['ast', ...s.reasons] });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const e of res.navEdges) {
|
|
184
|
+
const to = `screen:${slug(e.to)}`;
|
|
185
|
+
if (!topo.edges.some((x) => x.to === to && x.evidence === `${relPath}:${e.via}`)) {
|
|
186
|
+
topo.edges.push({ to, kind: 'navigation', evidence: `${relPath}:${e.via}`, confidence: 0.6 });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const rc of res.routeConstants) {
|
|
190
|
+
if (!topo.routeConstants.some((x) => x.name === rc.name && x.file === relPath))
|
|
191
|
+
topo.routeConstants.push({ name: rc.name, value: rc.value, file: relPath });
|
|
192
|
+
}
|
|
193
|
+
for (const note of res.parserNotes)
|
|
194
|
+
topo.parserNotes.push(note);
|
|
195
|
+
}
|
|
196
|
+
if (!topo.router && parsedFiles && topo.screens.some((s) => (s.reasons ?? []).includes('ast_navigator_screen')))
|
|
197
|
+
topo.router = 'react-navigation';
|
|
198
|
+
topo.routeConstants = topo.routeConstants.slice(0, 200);
|
|
199
|
+
if (failedFiles)
|
|
200
|
+
topo.parserNotes.push(`AST scan: ${failedFiles} JS/TS file(s) failed to parse — reduced confidence`);
|
|
201
|
+
}
|
|
202
|
+
function scanRnScreenFiles(root, topo, jsFiles) {
|
|
203
|
+
// Heuristic: files under */screens/* or features/*/screens/* whose default export looks like a screen.
|
|
204
|
+
for (const f of jsFiles) {
|
|
205
|
+
const relPath = rel(root, f);
|
|
206
|
+
if (!/(^|\/)(screens|pages)\//.test(relPath) && !/features\/[^/]+\/screens\//.test(relPath))
|
|
207
|
+
continue;
|
|
208
|
+
if (/\.(test|spec|stories)\./.test(relPath))
|
|
209
|
+
continue;
|
|
210
|
+
const name = basename(f).replace(/\.(tsx?|jsx?)$/, '');
|
|
211
|
+
if (!/^[A-Z]/.test(name) || name.toLowerCase() === 'index')
|
|
212
|
+
continue;
|
|
213
|
+
const id = `screen:${slug(name)}`;
|
|
214
|
+
if (topo.screens.some((s) => s.id === id))
|
|
215
|
+
continue;
|
|
216
|
+
topo.screens.push({ id, name, kind: 'screen', sourceFiles: [relPath], confidence: 0.55, reasons: ['screen_filename_convention'] });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function scanRouteConstants(root, topo, jsFiles) {
|
|
220
|
+
// Exported route constant objects: e.g. export const Routes = { Home: 'Home', ... }
|
|
221
|
+
for (const f of jsFiles) {
|
|
222
|
+
const text = readTextSafe(f);
|
|
223
|
+
if (!text)
|
|
224
|
+
continue;
|
|
225
|
+
const relPath = rel(root, f);
|
|
226
|
+
const blockRe = /export\s+const\s+([A-Za-z0-9_]*[Rr]outes?[A-Za-z0-9_]*)\s*=\s*\{([^}]+)\}/g;
|
|
227
|
+
let m;
|
|
228
|
+
while ((m = blockRe.exec(text))) {
|
|
229
|
+
const body = m[2];
|
|
230
|
+
const pairRe = /([A-Za-z0-9_]+)\s*:\s*["'`]([^"'`]+)["'`]/g;
|
|
231
|
+
let p;
|
|
232
|
+
while ((p = pairRe.exec(body))) {
|
|
233
|
+
topo.routeConstants.push({ name: `${m[1]}.${p[1]}`, value: p[2], file: relPath });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
topo.routeConstants = topo.routeConstants.slice(0, 200);
|
|
238
|
+
}
|
|
239
|
+
function detectRnInputModels(jsFiles, root) {
|
|
240
|
+
const out = [];
|
|
241
|
+
const seen = new Set();
|
|
242
|
+
for (const f of jsFiles) {
|
|
243
|
+
const text = readTextSafe(f);
|
|
244
|
+
if (!text || !/TextInput|TextField|<Input\b/.test(text))
|
|
245
|
+
continue;
|
|
246
|
+
const relPath = rel(root, f);
|
|
247
|
+
// secureTextEntry → password; keyboardType email-address → email; placeholder hints.
|
|
248
|
+
if (/secureTextEntry/.test(text) && !seen.has('password')) {
|
|
249
|
+
out.push({ fieldPurpose: 'password', inputType: 'password', secret: true, fixtureRequired: true, safeGenerator: 'faker.internet.password', validation: 'non-empty', source: relPath });
|
|
250
|
+
seen.add('password');
|
|
251
|
+
}
|
|
252
|
+
if (/keyboardType\s*=\s*["'`]email-address["'`]|placeholder\s*=\s*["'`][^"'`]*email/i.test(text) && !seen.has('email')) {
|
|
253
|
+
out.push({ fieldPurpose: 'email', inputType: 'email', secret: false, fixtureRequired: true, safeGenerator: 'faker.internet.email', validation: 'email format', source: relPath });
|
|
254
|
+
seen.add('email');
|
|
255
|
+
}
|
|
256
|
+
if (/placeholder\s*=\s*["'`][^"'`]*search/i.test(text) && !seen.has('search')) {
|
|
257
|
+
out.push({ fieldPurpose: 'search', inputType: 'text', secret: false, fixtureRequired: false, safeGenerator: 'static.search-term', source: relPath });
|
|
258
|
+
seen.add('search');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
function scanExpoRn(root, fw, topo, result, jsFiles) {
|
|
264
|
+
// app.json / app.config.* / package.json identity
|
|
265
|
+
const appJson = readJson(join(root, 'app.json'));
|
|
266
|
+
const expo = (appJson?.expo ?? appJson);
|
|
267
|
+
if (expo) {
|
|
268
|
+
result.appIdentity.appName = expo.name ?? null;
|
|
269
|
+
result.appIdentity.version = expo.version ?? null;
|
|
270
|
+
result.appIdentity.androidPackage = expo.android?.package ?? null;
|
|
271
|
+
result.appIdentity.iosBundleId = expo.ios?.bundleIdentifier ?? null;
|
|
272
|
+
const schemes = Array.isArray(expo.scheme) ? expo.scheme : expo.scheme ? [expo.scheme] : [];
|
|
273
|
+
topo.deepLinks.push(...schemes.map((s) => `${s}://`));
|
|
274
|
+
}
|
|
275
|
+
// config presence notes (app.config.js/ts not evaluated — recorded as partial confidence)
|
|
276
|
+
for (const cfg of ['app.config.js', 'app.config.ts']) {
|
|
277
|
+
if (existsSync(join(root, cfg)))
|
|
278
|
+
topo.parserNotes.push(`${cfg} present but not evaluated (dynamic config) — identity may be incomplete`);
|
|
279
|
+
}
|
|
280
|
+
scanExpoRouter(root, topo);
|
|
281
|
+
scanReactNavigation(root, topo, jsFiles);
|
|
282
|
+
scanTsAst(root, topo, jsFiles); // Fix 10: AST pass augments/refines the regex results.
|
|
283
|
+
scanRnScreenFiles(root, topo, jsFiles);
|
|
284
|
+
scanRouteConstants(root, topo, jsFiles);
|
|
285
|
+
result.inputModels.push(...detectRnInputModels(jsFiles, root));
|
|
286
|
+
}
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// Native Android
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
function findManifest(root) {
|
|
291
|
+
for (const p of ['app/src/main/AndroidManifest.xml', 'android/app/src/main/AndroidManifest.xml', 'src/main/AndroidManifest.xml']) {
|
|
292
|
+
if (existsSync(join(root, p)))
|
|
293
|
+
return join(root, p);
|
|
294
|
+
}
|
|
295
|
+
const hits = walkFiles(root, { alsoNames: ['AndroidManifest.xml'], maxFiles: 50, exts: [] }).filter((f) => basename(f) === 'AndroidManifest.xml' && !/\/(test|androidTest|debug)\//.test(f));
|
|
296
|
+
return hits[0] ?? null;
|
|
297
|
+
}
|
|
298
|
+
function asArray(v) {
|
|
299
|
+
return v === undefined ? [] : Array.isArray(v) ? v : [v];
|
|
300
|
+
}
|
|
301
|
+
function scanNativeAndroid(root, topo, result) {
|
|
302
|
+
topo.router = topo.router ?? 'android';
|
|
303
|
+
const manifestPath = findManifest(root);
|
|
304
|
+
if (manifestPath) {
|
|
305
|
+
const text = readTextSafe(manifestPath);
|
|
306
|
+
if (text) {
|
|
307
|
+
try {
|
|
308
|
+
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_' });
|
|
309
|
+
const doc = parser.parse(text);
|
|
310
|
+
const manifest = doc.manifest ?? {};
|
|
311
|
+
result.appIdentity.androidPackage = result.appIdentity.androidPackage ?? (manifest['@_package'] ?? null);
|
|
312
|
+
const app = manifest.application ?? {};
|
|
313
|
+
const activities = asArray(app.activity);
|
|
314
|
+
for (const act of activities) {
|
|
315
|
+
const name = act['@_android:name'] ?? '';
|
|
316
|
+
if (!name)
|
|
317
|
+
continue;
|
|
318
|
+
const short = name.split('.').pop() ?? name;
|
|
319
|
+
topo.nativeActivities.push(name);
|
|
320
|
+
topo.screens.push({ id: `activity:${slug(short)}`, name: short, kind: 'activity', sourceFiles: [rel(root, manifestPath)], confidence: 0.85, reasons: ['android_manifest_activity'] });
|
|
321
|
+
// deep links from intent-filter data
|
|
322
|
+
for (const f of asArray(act['intent-filter'])) {
|
|
323
|
+
for (const d of asArray(f.data)) {
|
|
324
|
+
const scheme = d['@_android:scheme'];
|
|
325
|
+
const host = d['@_android:host'];
|
|
326
|
+
if (scheme)
|
|
327
|
+
topo.deepLinks.push(`${scheme}://${host ?? ''}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
for (const perm of asArray(manifest['uses-permission'])) {
|
|
332
|
+
const n = perm['@_android:name'];
|
|
333
|
+
if (n)
|
|
334
|
+
topo.permissions.push(n);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
topo.parserNotes.push(`Failed to parse ${rel(root, manifestPath)} — activities/permissions may be incomplete`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
topo.parserNotes.push('No AndroidManifest.xml found under common locations');
|
|
344
|
+
}
|
|
345
|
+
// Gradle namespace / applicationId
|
|
346
|
+
for (const g of ['app/build.gradle', 'app/build.gradle.kts', 'android/app/build.gradle', 'android/app/build.gradle.kts', 'build.gradle', 'build.gradle.kts']) {
|
|
347
|
+
const p = join(root, g);
|
|
348
|
+
const text = readTextSafe(p);
|
|
349
|
+
if (!text)
|
|
350
|
+
continue;
|
|
351
|
+
const appId = text.match(/applicationId\s*[=\s]\s*["']([^"']+)["']/);
|
|
352
|
+
const ns = text.match(/namespace\s*[=\s]\s*["']([^"']+)["']/);
|
|
353
|
+
if (appId && !result.appIdentity.androidPackage)
|
|
354
|
+
result.appIdentity.androidPackage = appId[1];
|
|
355
|
+
if (ns && !result.packageName)
|
|
356
|
+
result.packageName = ns[1];
|
|
357
|
+
const versionName = text.match(/versionName\s*[=\s]\s*["']([^"']+)["']/);
|
|
358
|
+
if (versionName && !result.appIdentity.version)
|
|
359
|
+
result.appIdentity.version = versionName[1];
|
|
360
|
+
}
|
|
361
|
+
// Jetpack Navigation XML graphs (res/navigation/*.xml) + Compose navigation calls.
|
|
362
|
+
const navXml = walkFiles(root, { exts: ['.xml'], maxFiles: 1500 }).filter((f) => /\/res\/navigation\//.test(f));
|
|
363
|
+
for (const f of navXml) {
|
|
364
|
+
const text = readTextSafe(f);
|
|
365
|
+
if (!text)
|
|
366
|
+
continue;
|
|
367
|
+
const fragRe = /android:name="([^"]+)"|android:label="([^"]+)"/g;
|
|
368
|
+
let m;
|
|
369
|
+
while ((m = fragRe.exec(text))) {
|
|
370
|
+
const name = (m[1] ?? m[2] ?? '').split('.').pop();
|
|
371
|
+
if (name)
|
|
372
|
+
topo.screens.push({ id: `fragment:${slug(name)}`, name, kind: 'fragment', sourceFiles: [rel(root, f)], confidence: 0.7, reasons: ['jetpack_navigation_xml'] });
|
|
373
|
+
}
|
|
374
|
+
if (navXml.length)
|
|
375
|
+
topo.router = 'jetpack-navigation';
|
|
376
|
+
}
|
|
377
|
+
const kotlin = walkFiles(root, { exts: ['.kt'], maxFiles: 2000 });
|
|
378
|
+
for (const f of kotlin) {
|
|
379
|
+
const text = readTextSafe(f);
|
|
380
|
+
if (!text || !/NavHost|composable\(/.test(text))
|
|
381
|
+
continue;
|
|
382
|
+
topo.router = topo.router === 'android' ? 'compose-navigation' : topo.router;
|
|
383
|
+
const compRe = /composable\(\s*(?:route\s*=\s*)?["']([^"']+)["']/g;
|
|
384
|
+
let m;
|
|
385
|
+
while ((m = compRe.exec(text))) {
|
|
386
|
+
const route = m[1];
|
|
387
|
+
const name = route.split('/')[0].split('?')[0] || route;
|
|
388
|
+
const id = `composable:${slug(name)}`;
|
|
389
|
+
if (!topo.screens.some((s) => s.id === id))
|
|
390
|
+
topo.screens.push({ id, name, route, kind: 'screen', sourceFiles: [rel(root, f)], confidence: 0.7, reasons: ['compose_navigation'] });
|
|
391
|
+
topo.edges.push({ to: id, kind: 'route_declaration', evidence: `${rel(root, f)}:composable`, confidence: 0.6 });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// Native iOS
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
function plistValue(text, key) {
|
|
399
|
+
const re = new RegExp(`<key>${key}</key>\\s*<(string|true|false|integer)>?([^<]*)`, 'i');
|
|
400
|
+
const m = text.match(re);
|
|
401
|
+
return m ? (m[1] === 'true' || m[1] === 'false' ? m[1] : m[2].trim()) : null;
|
|
402
|
+
}
|
|
403
|
+
function scanNativeIos(root, topo, result, swiftFiles) {
|
|
404
|
+
topo.router = topo.router ?? 'storyboard';
|
|
405
|
+
const plists = walkFiles(root, { alsoNames: ['Info.plist'], exts: [], maxFiles: 200 }).filter((f) => basename(f) === 'Info.plist' && !/\/(Tests|Pods)\//.test(f));
|
|
406
|
+
for (const p of plists) {
|
|
407
|
+
const text = readTextSafe(p);
|
|
408
|
+
if (!text)
|
|
409
|
+
continue;
|
|
410
|
+
result.appIdentity.iosBundleId = result.appIdentity.iosBundleId ?? plistValue(text, 'CFBundleIdentifier');
|
|
411
|
+
result.appIdentity.appName = result.appIdentity.appName ?? (plistValue(text, 'CFBundleDisplayName') ?? plistValue(text, 'CFBundleName'));
|
|
412
|
+
result.appIdentity.version = result.appIdentity.version ?? plistValue(text, 'CFBundleShortVersionString');
|
|
413
|
+
// URL schemes (CFBundleURLSchemes string array)
|
|
414
|
+
const schemeBlock = text.match(/<key>CFBundleURLSchemes<\/key>\s*<array>([\s\S]*?)<\/array>/i);
|
|
415
|
+
if (schemeBlock) {
|
|
416
|
+
for (const m of schemeBlock[1].matchAll(/<string>([^<]+)<\/string>/g))
|
|
417
|
+
topo.deepLinks.push(`${m[1]}://`);
|
|
418
|
+
}
|
|
419
|
+
break; // first non-test Info.plist is authoritative enough
|
|
420
|
+
}
|
|
421
|
+
// SwiftUI View structs + UIKit view controllers.
|
|
422
|
+
for (const f of swiftFiles) {
|
|
423
|
+
const text = readTextSafe(f);
|
|
424
|
+
if (!text)
|
|
425
|
+
continue;
|
|
426
|
+
const relPath = rel(root, f);
|
|
427
|
+
for (const m of text.matchAll(/struct\s+([A-Za-z0-9_]+)\s*:\s*[^{]*\bView\b/g)) {
|
|
428
|
+
const name = m[1];
|
|
429
|
+
if (/Cell|Row|Button|Style|Modifier|Preview$/.test(name))
|
|
430
|
+
continue;
|
|
431
|
+
const id = `view:${slug(name)}`;
|
|
432
|
+
if (!topo.screens.some((s) => s.id === id))
|
|
433
|
+
topo.screens.push({ id, name, kind: 'view_controller', sourceFiles: [relPath], confidence: /View$|Screen$|Page$/.test(name) ? 0.7 : 0.5, reasons: ['swiftui_view_struct'] });
|
|
434
|
+
topo.viewControllers.push(name);
|
|
435
|
+
}
|
|
436
|
+
for (const m of text.matchAll(/class\s+([A-Za-z0-9_]+)\s*:\s*[^{]*\b(UIViewController|UITableViewController|UICollectionViewController)\b/g)) {
|
|
437
|
+
const name = m[1];
|
|
438
|
+
const id = `vc:${slug(name)}`;
|
|
439
|
+
if (!topo.screens.some((s) => s.id === id))
|
|
440
|
+
topo.screens.push({ id, name, kind: 'view_controller', sourceFiles: [relPath], confidence: 0.75, reasons: ['uikit_view_controller'] });
|
|
441
|
+
topo.viewControllers.push(name);
|
|
442
|
+
}
|
|
443
|
+
// NavigationLink destinations → edges (best-effort)
|
|
444
|
+
for (const m of text.matchAll(/NavigationLink[^{]*?destination:\s*([A-Za-z0-9_]+)\s*\(/g)) {
|
|
445
|
+
topo.edges.push({ to: `view:${slug(m[1])}`, kind: 'navigation', evidence: `${relPath}:NavigationLink`, confidence: 0.5 });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// storyboards/xibs
|
|
449
|
+
const sb = walkFiles(root, { exts: ['.storyboard', '.xib'], maxFiles: 200 });
|
|
450
|
+
if (sb.length)
|
|
451
|
+
topo.parserNotes.push(`${sb.length} storyboard/xib file(s) present — scene graph not parsed (partial confidence)`);
|
|
452
|
+
}
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
// Flutter
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
456
|
+
function scanFlutter(root, topo, result, dartFiles) {
|
|
457
|
+
topo.router = topo.router ?? 'flutter-routes';
|
|
458
|
+
const pubspecText = readTextSafe(join(root, 'pubspec.yaml'));
|
|
459
|
+
if (pubspecText) {
|
|
460
|
+
try {
|
|
461
|
+
const pub = parseYaml(pubspecText);
|
|
462
|
+
result.packageName = result.packageName ?? (pub.name ?? null);
|
|
463
|
+
result.appIdentity.appName = result.appIdentity.appName ?? (pub.name ?? null);
|
|
464
|
+
result.appIdentity.version = result.appIdentity.version ?? (pub.version ? String(pub.version) : null);
|
|
465
|
+
const deps = Object.keys(pub.dependencies ?? {});
|
|
466
|
+
if (deps.includes('go_router'))
|
|
467
|
+
topo.router = 'go_router';
|
|
468
|
+
else if (deps.includes('auto_route'))
|
|
469
|
+
topo.router = 'auto_route';
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
topo.parserNotes.push('Failed to parse pubspec.yaml');
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
for (const f of dartFiles) {
|
|
476
|
+
const text = readTextSafe(f);
|
|
477
|
+
if (!text)
|
|
478
|
+
continue;
|
|
479
|
+
const relPath = rel(root, f);
|
|
480
|
+
// MaterialApp routes: { '/foo': (c) => FooPage() }
|
|
481
|
+
for (const m of text.matchAll(/["']\/([A-Za-z0-9_\-/]*)["']\s*:\s*\(/g)) {
|
|
482
|
+
const route = '/' + m[1];
|
|
483
|
+
const id = `route:${slug(m[1] || 'home')}`;
|
|
484
|
+
if (!topo.screens.some((s) => s.id === id))
|
|
485
|
+
topo.screens.push({ id, name: m[1] || 'home', route, kind: 'route', sourceFiles: [relPath], confidence: 0.75, reasons: ['flutter_named_route'] });
|
|
486
|
+
topo.flutterRoutes.push(route);
|
|
487
|
+
}
|
|
488
|
+
// GoRoute(path: '/foo')
|
|
489
|
+
for (const m of text.matchAll(/GoRoute\([^)]*path:\s*["']([^"']+)["']/g)) {
|
|
490
|
+
const route = m[1];
|
|
491
|
+
const id = `route:${slug(route.replace(/^\//, '') || 'home')}`;
|
|
492
|
+
if (!topo.screens.some((s) => s.id === id))
|
|
493
|
+
topo.screens.push({ id, name: route, route, kind: 'route', sourceFiles: [relPath], confidence: 0.8, reasons: ['go_router_route'] });
|
|
494
|
+
topo.flutterRoutes.push(route);
|
|
495
|
+
}
|
|
496
|
+
if (/onGenerateRoute/.test(text))
|
|
497
|
+
topo.parserNotes.push(`onGenerateRoute in ${relPath} — dynamic routes not fully enumerated`);
|
|
498
|
+
}
|
|
499
|
+
// pages/screens under lib/screens, lib/pages, lib/features
|
|
500
|
+
for (const f of dartFiles) {
|
|
501
|
+
const relPath = rel(root, f);
|
|
502
|
+
if (!/lib\/(screens|pages)\//.test(relPath) && !/lib\/features\/[^/]+\/(screens|pages|presentation)\//.test(relPath))
|
|
503
|
+
continue;
|
|
504
|
+
const name = basename(f).replace(/\.dart$/, '');
|
|
505
|
+
if (/^_/.test(name) || /_test$/.test(name))
|
|
506
|
+
continue;
|
|
507
|
+
const id = `page:${slug(name)}`;
|
|
508
|
+
if (!topo.screens.some((s) => s.id === id))
|
|
509
|
+
topo.screens.push({ id, name, kind: 'page', sourceFiles: [relPath], confidence: 0.55, reasons: ['flutter_page_filename'] });
|
|
510
|
+
}
|
|
511
|
+
topo.flutterRoutes = uniq(topo.flutterRoutes);
|
|
512
|
+
}
|
|
513
|
+
// ---------------------------------------------------------------------------
|
|
514
|
+
// Cross-framework: auth / onboarding / paywalls from dependencies + screens
|
|
515
|
+
// ---------------------------------------------------------------------------
|
|
516
|
+
function detectFeatureModels(root, deps, topo) {
|
|
517
|
+
const depNames = Object.keys(deps);
|
|
518
|
+
const authLibs = depNames.filter((d) => AUTH_LIB_RE.test(d));
|
|
519
|
+
const paywallLibs = depNames.filter((d) => PAYWALL_LIB_RE.test(d));
|
|
520
|
+
const authScreens = topo.screens.filter((s) => /login|signin|sign-in|auth|register|signup|sign-up|password/i.test(s.name) || /login|signin|auth/i.test(s.route ?? '')).map((s) => s.id);
|
|
521
|
+
const onboardingScreens = topo.screens.filter((s) => ONBOARDING_RE.test(s.name) || ONBOARDING_RE.test(s.route ?? '')).map((s) => s.id);
|
|
522
|
+
const paywallScreens = topo.screens.filter((s) => /paywall|subscribe|subscription|premium|upgrade|pricing|plans?/i.test(s.name)).map((s) => s.id);
|
|
523
|
+
const auth = {
|
|
524
|
+
hasAuth: authLibs.length > 0 || authScreens.length > 0,
|
|
525
|
+
signals: uniq([...authLibs, ...(authScreens.length ? ['auth_screen_detected'] : [])]),
|
|
526
|
+
libraries: authLibs,
|
|
527
|
+
screens: authScreens,
|
|
528
|
+
confidence: Math.min(1, (authLibs.length ? 0.7 : 0) + (authScreens.length ? 0.5 : 0)),
|
|
529
|
+
};
|
|
530
|
+
const onboarding = onboardingScreens.length
|
|
531
|
+
? { id: 'flow:onboarding', kind: 'onboarding', present: true, signals: ['onboarding_screen_detected'], libraries: [], screens: onboardingScreens, confidence: 0.55 }
|
|
532
|
+
: null;
|
|
533
|
+
const paywalls = paywallLibs.length || paywallScreens.length
|
|
534
|
+
? [{ id: 'flow:paywall', kind: 'paywall', present: true, signals: uniq([...paywallLibs.map((l) => `lib:${l}`), ...(paywallScreens.length ? ['paywall_screen_detected'] : [])]), libraries: paywallLibs, screens: paywallScreens, confidence: Math.min(1, (paywallLibs.length ? 0.7 : 0) + (paywallScreens.length ? 0.4 : 0)) }]
|
|
535
|
+
: [];
|
|
536
|
+
return { auth, onboarding, paywalls };
|
|
537
|
+
}
|
|
538
|
+
// ---------------------------------------------------------------------------
|
|
539
|
+
/** Run the framework-appropriate static scanners. Always returns a result; never throws. */
|
|
540
|
+
export function staticScan(root, generatedAt) {
|
|
541
|
+
const fw = detectFramework(root);
|
|
542
|
+
const topo = {
|
|
543
|
+
framework: fw,
|
|
544
|
+
router: null,
|
|
545
|
+
screens: [],
|
|
546
|
+
edges: [],
|
|
547
|
+
deepLinks: [],
|
|
548
|
+
routeConstants: [],
|
|
549
|
+
nativeActivities: [],
|
|
550
|
+
viewControllers: [],
|
|
551
|
+
flutterRoutes: [],
|
|
552
|
+
permissions: [],
|
|
553
|
+
parserNotes: [],
|
|
554
|
+
};
|
|
555
|
+
const deps = depsOf(root);
|
|
556
|
+
const pkg = readJson(join(root, 'package.json'));
|
|
557
|
+
const result = {
|
|
558
|
+
staticTopology: topo,
|
|
559
|
+
appIdentity: {},
|
|
560
|
+
auth: { hasAuth: false, signals: [], libraries: [], screens: [], confidence: 0 },
|
|
561
|
+
onboarding: null,
|
|
562
|
+
paywalls: [],
|
|
563
|
+
inputModels: [],
|
|
564
|
+
sourceFingerprint: { generatedAt, files: [] },
|
|
565
|
+
packageName: pkg?.name ?? null,
|
|
566
|
+
collectedFiles: [],
|
|
567
|
+
};
|
|
568
|
+
try {
|
|
569
|
+
if (fw === 'expo' || fw === 'bare-react-native') {
|
|
570
|
+
const jsFiles = walkFiles(root, { exts: ['.ts', '.tsx', '.js', '.jsx'] });
|
|
571
|
+
result.collectedFiles = jsFiles;
|
|
572
|
+
scanExpoRn(root, fw, topo, result, jsFiles);
|
|
573
|
+
}
|
|
574
|
+
else if (fw === 'native-android') {
|
|
575
|
+
result.collectedFiles = walkFiles(root, { exts: ['.kt', '.java', '.xml'], maxFiles: 3000 });
|
|
576
|
+
scanNativeAndroid(root, topo, result);
|
|
577
|
+
}
|
|
578
|
+
else if (fw === 'native-ios') {
|
|
579
|
+
const swift = walkFiles(root, { exts: ['.swift'], maxFiles: 3000 });
|
|
580
|
+
result.collectedFiles = swift;
|
|
581
|
+
scanNativeIos(root, topo, result, swift);
|
|
582
|
+
}
|
|
583
|
+
else if (fw === 'flutter') {
|
|
584
|
+
const dart = walkFiles(root, { exts: ['.dart'], maxFiles: 3000 });
|
|
585
|
+
result.collectedFiles = dart;
|
|
586
|
+
scanFlutter(root, topo, result, dart);
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
topo.parserNotes.push(`Framework "${fw}" — no static scanner; map will rely on runtime observation`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
catch (e) {
|
|
593
|
+
topo.parserNotes.push(`Static scan error (non-fatal): ${String(e)}`);
|
|
594
|
+
}
|
|
595
|
+
// de-dupe deep links + permissions
|
|
596
|
+
topo.deepLinks = uniq(topo.deepLinks);
|
|
597
|
+
topo.permissions = uniq(topo.permissions);
|
|
598
|
+
const models = detectFeatureModels(root, deps, topo);
|
|
599
|
+
result.auth = models.auth;
|
|
600
|
+
result.onboarding = models.onboarding;
|
|
601
|
+
result.paywalls = models.paywalls;
|
|
602
|
+
// Fingerprint structure-defining files: configs + the files that produced screens/edges.
|
|
603
|
+
const structureFiles = uniq([
|
|
604
|
+
...['app.json', 'package.json', 'pubspec.yaml', 'app.config.js', 'app.config.ts'].map((n) => join(root, n)).filter(existsSync),
|
|
605
|
+
...topo.screens.flatMap((s) => s.sourceFiles.map((rp) => join(root, rp))),
|
|
606
|
+
]);
|
|
607
|
+
result.sourceFingerprint = fingerprint(root, structureFiles, generatedAt);
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
610
|
+
/** Detect form-library usage from deps (used by feature inference). */
|
|
611
|
+
export function hasFormLibrary(root) {
|
|
612
|
+
return Object.keys(depsOf(root)).some((d) => FORM_LIB_RE.test(d));
|
|
613
|
+
}
|
|
614
|
+
//# sourceMappingURL=staticScan.js.map
|