@backthread/extractor 0.1.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 +83 -0
- package/dist/cluster/domain-grouping.d.ts +113 -0
- package/dist/cluster/domain-grouping.d.ts.map +1 -0
- package/dist/cluster/domain-grouping.js +277 -0
- package/dist/cluster/domain-grouping.js.map +1 -0
- package/dist/cluster/grouping-gate.d.ts +54 -0
- package/dist/cluster/grouping-gate.d.ts.map +1 -0
- package/dist/cluster/grouping-gate.js +0 -0
- package/dist/cluster/grouping-gate.js.map +1 -0
- package/dist/cluster/louvain.d.ts +88 -0
- package/dist/cluster/louvain.d.ts.map +1 -0
- package/dist/cluster/louvain.js +674 -0
- package/dist/cluster/louvain.js.map +1 -0
- package/dist/cluster/overrides.d.ts +19 -0
- package/dist/cluster/overrides.d.ts.map +1 -0
- package/dist/cluster/overrides.js +32 -0
- package/dist/cluster/overrides.js.map +1 -0
- package/dist/cluster/subsystem.d.ts +45 -0
- package/dist/cluster/subsystem.d.ts.map +1 -0
- package/dist/cluster/subsystem.js +299 -0
- package/dist/cluster/subsystem.js.map +1 -0
- package/dist/cluster/workspaces.d.ts +64 -0
- package/dist/cluster/workspaces.d.ts.map +1 -0
- package/dist/cluster/workspaces.js +739 -0
- package/dist/cluster/workspaces.js.map +1 -0
- package/dist/extract.d.ts +52 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +48 -0
- package/dist/extract.js.map +1 -0
- package/dist/framework/celery/celery.d.ts +17 -0
- package/dist/framework/celery/celery.d.ts.map +1 -0
- package/dist/framework/celery/celery.js +524 -0
- package/dist/framework/celery/celery.js.map +1 -0
- package/dist/framework/contribute-step.d.ts +38 -0
- package/dist/framework/contribute-step.d.ts.map +1 -0
- package/dist/framework/contribute-step.js +403 -0
- package/dist/framework/contribute-step.js.map +1 -0
- package/dist/framework/detect-step.d.ts +8 -0
- package/dist/framework/detect-step.d.ts.map +1 -0
- package/dist/framework/detect-step.js +54 -0
- package/dist/framework/detect-step.js.map +1 -0
- package/dist/framework/detect-util.d.ts +24 -0
- package/dist/framework/detect-util.d.ts.map +1 -0
- package/dist/framework/detect-util.js +59 -0
- package/dist/framework/detect-util.js.map +1 -0
- package/dist/framework/django/django.d.ts +22 -0
- package/dist/framework/django/django.d.ts.map +1 -0
- package/dist/framework/django/django.js +940 -0
- package/dist/framework/django/django.js.map +1 -0
- package/dist/framework/fastapi/fastapi.d.ts +27 -0
- package/dist/framework/fastapi/fastapi.d.ts.map +1 -0
- package/dist/framework/fastapi/fastapi.js +477 -0
- package/dist/framework/fastapi/fastapi.js.map +1 -0
- package/dist/framework/faststream/faststream.d.ts +19 -0
- package/dist/framework/faststream/faststream.d.ts.map +1 -0
- package/dist/framework/faststream/faststream.js +765 -0
- package/dist/framework/faststream/faststream.js.map +1 -0
- package/dist/framework/flask/flask.d.ts +17 -0
- package/dist/framework/flask/flask.d.ts.map +1 -0
- package/dist/framework/flask/flask.js +507 -0
- package/dist/framework/flask/flask.js.map +1 -0
- package/dist/framework/graphql/graphql.d.ts +17 -0
- package/dist/framework/graphql/graphql.d.ts.map +1 -0
- package/dist/framework/graphql/graphql.js +708 -0
- package/dist/framework/graphql/graphql.js.map +1 -0
- package/dist/framework/grpc/grpc.d.ts +22 -0
- package/dist/framework/grpc/grpc.d.ts.map +1 -0
- package/dist/framework/grpc/grpc.js +618 -0
- package/dist/framework/grpc/grpc.js.map +1 -0
- package/dist/framework/litestar/litestar.d.ts +17 -0
- package/dist/framework/litestar/litestar.d.ts.map +1 -0
- package/dist/framework/litestar/litestar.js +536 -0
- package/dist/framework/litestar/litestar.js.map +1 -0
- package/dist/framework/nest/nest.d.ts +19 -0
- package/dist/framework/nest/nest.d.ts.map +1 -0
- package/dist/framework/nest/nest.js +613 -0
- package/dist/framework/nest/nest.js.map +1 -0
- package/dist/framework/next/next.d.ts +22 -0
- package/dist/framework/next/next.d.ts.map +1 -0
- package/dist/framework/next/next.js +449 -0
- package/dist/framework/next/next.js.map +1 -0
- package/dist/framework/node/node.d.ts +24 -0
- package/dist/framework/node/node.d.ts.map +1 -0
- package/dist/framework/node/node.js +1009 -0
- package/dist/framework/node/node.js.map +1 -0
- package/dist/framework/orchestrator/orchestrator.d.ts +34 -0
- package/dist/framework/orchestrator/orchestrator.d.ts.map +1 -0
- package/dist/framework/orchestrator/orchestrator.js +691 -0
- package/dist/framework/orchestrator/orchestrator.js.map +1 -0
- package/dist/framework/orm/orm.d.ts +61 -0
- package/dist/framework/orm/orm.d.ts.map +1 -0
- package/dist/framework/orm/orm.js +620 -0
- package/dist/framework/orm/orm.js.map +1 -0
- package/dist/framework/python/analyze.d.ts +38 -0
- package/dist/framework/python/analyze.d.ts.map +1 -0
- package/dist/framework/python/analyze.js +98 -0
- package/dist/framework/python/analyze.js.map +1 -0
- package/dist/framework/python/py-ast.d.ts +130 -0
- package/dist/framework/python/py-ast.d.ts.map +1 -0
- package/dist/framework/python/py-ast.js +344 -0
- package/dist/framework/python/py-ast.js.map +1 -0
- package/dist/framework/python-orm/python-orm.d.ts +19 -0
- package/dist/framework/python-orm/python-orm.d.ts.map +1 -0
- package/dist/framework/python-orm/python-orm.js +516 -0
- package/dist/framework/python-orm/python-orm.js.map +1 -0
- package/dist/framework/react-native/react-native.d.ts +35 -0
- package/dist/framework/react-native/react-native.d.ts.map +1 -0
- package/dist/framework/react-native/react-native.js +1181 -0
- package/dist/framework/react-native/react-native.js.map +1 -0
- package/dist/framework/register.d.ts +2 -0
- package/dist/framework/register.d.ts.map +1 -0
- package/dist/framework/register.js +59 -0
- package/dist/framework/register.js.map +1 -0
- package/dist/framework/registry.d.ts +31 -0
- package/dist/framework/registry.d.ts.map +1 -0
- package/dist/framework/registry.js +70 -0
- package/dist/framework/registry.js.map +1 -0
- package/dist/framework/routing/expo-router.d.ts +31 -0
- package/dist/framework/routing/expo-router.d.ts.map +1 -0
- package/dist/framework/routing/expo-router.js +356 -0
- package/dist/framework/routing/expo-router.js.map +1 -0
- package/dist/framework/routing/index.d.ts +4 -0
- package/dist/framework/routing/index.d.ts.map +1 -0
- package/dist/framework/routing/index.js +16 -0
- package/dist/framework/routing/index.js.map +1 -0
- package/dist/framework/routing/next-router.d.ts +45 -0
- package/dist/framework/routing/next-router.d.ts.map +1 -0
- package/dist/framework/routing/next-router.js +378 -0
- package/dist/framework/routing/next-router.js.map +1 -0
- package/dist/framework/routing/route-tree.d.ts +73 -0
- package/dist/framework/routing/route-tree.d.ts.map +1 -0
- package/dist/framework/routing/route-tree.js +120 -0
- package/dist/framework/routing/route-tree.js.map +1 -0
- package/dist/framework/types.d.ts +230 -0
- package/dist/framework/types.d.ts.map +1 -0
- package/dist/framework/types.js +25 -0
- package/dist/framework/types.js.map +1 -0
- package/dist/graph/cross-language.d.ts +11 -0
- package/dist/graph/cross-language.d.ts.map +1 -0
- package/dist/graph/cross-language.js +123 -0
- package/dist/graph/cross-language.js.map +1 -0
- package/dist/graph/extract.d.ts +13 -0
- package/dist/graph/extract.d.ts.map +1 -0
- package/dist/graph/extract.js +79 -0
- package/dist/graph/extract.js.map +1 -0
- package/dist/graph/file-graph.d.ts +186 -0
- package/dist/graph/file-graph.d.ts.map +1 -0
- package/dist/graph/file-graph.js +427 -0
- package/dist/graph/file-graph.js.map +1 -0
- package/dist/graph/http-callsites.d.ts +14 -0
- package/dist/graph/http-callsites.d.ts.map +1 -0
- package/dist/graph/http-callsites.js +107 -0
- package/dist/graph/http-callsites.js.map +1 -0
- package/dist/graph/incremental.d.ts +122 -0
- package/dist/graph/incremental.d.ts.map +1 -0
- package/dist/graph/incremental.js +448 -0
- package/dist/graph/incremental.js.map +1 -0
- package/dist/graph/language.d.ts +31 -0
- package/dist/graph/language.d.ts.map +1 -0
- package/dist/graph/language.js +146 -0
- package/dist/graph/language.js.map +1 -0
- package/dist/graph/noise-filter.d.ts +46 -0
- package/dist/graph/noise-filter.d.ts.map +1 -0
- package/dist/graph/noise-filter.js +199 -0
- package/dist/graph/noise-filter.js.map +1 -0
- package/dist/graph/python-adapter.d.ts +36 -0
- package/dist/graph/python-adapter.d.ts.map +1 -0
- package/dist/graph/python-adapter.js +402 -0
- package/dist/graph/python-adapter.js.map +1 -0
- package/dist/graph/python-manifest.d.ts +22 -0
- package/dist/graph/python-manifest.d.ts.map +1 -0
- package/dist/graph/python-manifest.js +137 -0
- package/dist/graph/python-manifest.js.map +1 -0
- package/dist/graph/python-stdlib.d.ts +2 -0
- package/dist/graph/python-stdlib.d.ts.map +1 -0
- package/dist/graph/python-stdlib.js +53 -0
- package/dist/graph/python-stdlib.js.map +1 -0
- package/dist/graph/ts-morph-adapter.d.ts +65 -0
- package/dist/graph/ts-morph-adapter.d.ts.map +1 -0
- package/dist/graph/ts-morph-adapter.js +265 -0
- package/dist/graph/ts-morph-adapter.js.map +1 -0
- package/dist/graph/types.d.ts +37 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/types.js +50 -0
- package/dist/graph/types.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/infra/aws/aws-parse.d.ts +39 -0
- package/dist/infra/aws/aws-parse.d.ts.map +1 -0
- package/dist/infra/aws/aws-parse.js +149 -0
- package/dist/infra/aws/aws-parse.js.map +1 -0
- package/dist/infra/aws/aws.d.ts +6 -0
- package/dist/infra/aws/aws.d.ts.map +1 -0
- package/dist/infra/aws/aws.js +425 -0
- package/dist/infra/aws/aws.js.map +1 -0
- package/dist/infra/azure/azure-parse.d.ts +73 -0
- package/dist/infra/azure/azure-parse.d.ts.map +1 -0
- package/dist/infra/azure/azure-parse.js +178 -0
- package/dist/infra/azure/azure-parse.js.map +1 -0
- package/dist/infra/azure/azure.d.ts +12 -0
- package/dist/infra/azure/azure.d.ts.map +1 -0
- package/dist/infra/azure/azure.js +673 -0
- package/dist/infra/azure/azure.js.map +1 -0
- package/dist/infra/cloudflare/cloudflare.d.ts +50 -0
- package/dist/infra/cloudflare/cloudflare.d.ts.map +1 -0
- package/dist/infra/cloudflare/cloudflare.js +382 -0
- package/dist/infra/cloudflare/cloudflare.js.map +1 -0
- package/dist/infra/cloudflare/wrangler-parse.d.ts +6 -0
- package/dist/infra/cloudflare/wrangler-parse.d.ts.map +1 -0
- package/dist/infra/cloudflare/wrangler-parse.js +278 -0
- package/dist/infra/cloudflare/wrangler-parse.js.map +1 -0
- package/dist/infra/compose/compose-parse.d.ts +24 -0
- package/dist/infra/compose/compose-parse.d.ts.map +1 -0
- package/dist/infra/compose/compose-parse.js +77 -0
- package/dist/infra/compose/compose-parse.js.map +1 -0
- package/dist/infra/compose/compose.d.ts +12 -0
- package/dist/infra/compose/compose.d.ts.map +1 -0
- package/dist/infra/compose/compose.js +201 -0
- package/dist/infra/compose/compose.js.map +1 -0
- package/dist/infra/convex/convex-parse.d.ts +28 -0
- package/dist/infra/convex/convex-parse.d.ts.map +1 -0
- package/dist/infra/convex/convex-parse.js +74 -0
- package/dist/infra/convex/convex-parse.js.map +1 -0
- package/dist/infra/convex/convex.d.ts +18 -0
- package/dist/infra/convex/convex.d.ts.map +1 -0
- package/dist/infra/convex/convex.js +199 -0
- package/dist/infra/convex/convex.js.map +1 -0
- package/dist/infra/digitalocean/digitalocean.d.ts +33 -0
- package/dist/infra/digitalocean/digitalocean.d.ts.map +1 -0
- package/dist/infra/digitalocean/digitalocean.js +204 -0
- package/dist/infra/digitalocean/digitalocean.js.map +1 -0
- package/dist/infra/firebase/firebase.d.ts +43 -0
- package/dist/infra/firebase/firebase.d.ts.map +1 -0
- package/dist/infra/firebase/firebase.js +271 -0
- package/dist/infra/firebase/firebase.js.map +1 -0
- package/dist/infra/fly/fly-parse.d.ts +52 -0
- package/dist/infra/fly/fly-parse.d.ts.map +1 -0
- package/dist/infra/fly/fly-parse.js +114 -0
- package/dist/infra/fly/fly-parse.js.map +1 -0
- package/dist/infra/fly/fly.d.ts +15 -0
- package/dist/infra/fly/fly.d.ts.map +1 -0
- package/dist/infra/fly/fly.js +265 -0
- package/dist/infra/fly/fly.js.map +1 -0
- package/dist/infra/gcp/gcp-parse.d.ts +24 -0
- package/dist/infra/gcp/gcp-parse.d.ts.map +1 -0
- package/dist/infra/gcp/gcp-parse.js +123 -0
- package/dist/infra/gcp/gcp-parse.js.map +1 -0
- package/dist/infra/gcp/gcp.d.ts +10 -0
- package/dist/infra/gcp/gcp.d.ts.map +1 -0
- package/dist/infra/gcp/gcp.js +502 -0
- package/dist/infra/gcp/gcp.js.map +1 -0
- package/dist/infra/heroku/heroku-parse.d.ts +33 -0
- package/dist/infra/heroku/heroku-parse.d.ts.map +1 -0
- package/dist/infra/heroku/heroku-parse.js +63 -0
- package/dist/infra/heroku/heroku-parse.js.map +1 -0
- package/dist/infra/heroku/heroku.d.ts +24 -0
- package/dist/infra/heroku/heroku.d.ts.map +1 -0
- package/dist/infra/heroku/heroku.js +253 -0
- package/dist/infra/heroku/heroku.js.map +1 -0
- package/dist/infra/image-resolve.d.ts +56 -0
- package/dist/infra/image-resolve.d.ts.map +1 -0
- package/dist/infra/image-resolve.js +229 -0
- package/dist/infra/image-resolve.js.map +1 -0
- package/dist/infra/infra-step.d.ts +39 -0
- package/dist/infra/infra-step.d.ts.map +1 -0
- package/dist/infra/infra-step.js +57 -0
- package/dist/infra/infra-step.js.map +1 -0
- package/dist/infra/k8s/index.d.ts +78 -0
- package/dist/infra/k8s/index.d.ts.map +1 -0
- package/dist/infra/k8s/index.js +251 -0
- package/dist/infra/k8s/index.js.map +1 -0
- package/dist/infra/kamal/kamal-parse.d.ts +23 -0
- package/dist/infra/kamal/kamal-parse.d.ts.map +1 -0
- package/dist/infra/kamal/kamal-parse.js +66 -0
- package/dist/infra/kamal/kamal-parse.js.map +1 -0
- package/dist/infra/kamal/kamal.d.ts +17 -0
- package/dist/infra/kamal/kamal.d.ts.map +1 -0
- package/dist/infra/kamal/kamal.js +162 -0
- package/dist/infra/kamal/kamal.js.map +1 -0
- package/dist/infra/netlify/netlify.d.ts +33 -0
- package/dist/infra/netlify/netlify.d.ts.map +1 -0
- package/dist/infra/netlify/netlify.js +247 -0
- package/dist/infra/netlify/netlify.js.map +1 -0
- package/dist/infra/opentofu/opentofu.d.ts +3 -0
- package/dist/infra/opentofu/opentofu.d.ts.map +1 -0
- package/dist/infra/opentofu/opentofu.js +157 -0
- package/dist/infra/opentofu/opentofu.js.map +1 -0
- package/dist/infra/pulumi/pulumi-parse.d.ts +60 -0
- package/dist/infra/pulumi/pulumi-parse.d.ts.map +1 -0
- package/dist/infra/pulumi/pulumi-parse.js +155 -0
- package/dist/infra/pulumi/pulumi-parse.js.map +1 -0
- package/dist/infra/pulumi/pulumi.d.ts +23 -0
- package/dist/infra/pulumi/pulumi.d.ts.map +1 -0
- package/dist/infra/pulumi/pulumi.js +260 -0
- package/dist/infra/pulumi/pulumi.js.map +1 -0
- package/dist/infra/railway/railway-parse.d.ts +54 -0
- package/dist/infra/railway/railway-parse.d.ts.map +1 -0
- package/dist/infra/railway/railway-parse.js +200 -0
- package/dist/infra/railway/railway-parse.js.map +1 -0
- package/dist/infra/railway/railway.d.ts +19 -0
- package/dist/infra/railway/railway.d.ts.map +1 -0
- package/dist/infra/railway/railway.js +397 -0
- package/dist/infra/railway/railway.js.map +1 -0
- package/dist/infra/register.d.ts +2 -0
- package/dist/infra/register.d.ts.map +1 -0
- package/dist/infra/register.js +81 -0
- package/dist/infra/register.js.map +1 -0
- package/dist/infra/registry.d.ts +64 -0
- package/dist/infra/registry.d.ts.map +1 -0
- package/dist/infra/registry.js +205 -0
- package/dist/infra/registry.js.map +1 -0
- package/dist/infra/relevance.d.ts +16 -0
- package/dist/infra/relevance.d.ts.map +1 -0
- package/dist/infra/relevance.js +132 -0
- package/dist/infra/relevance.js.map +1 -0
- package/dist/infra/render/render-parse.d.ts +76 -0
- package/dist/infra/render/render-parse.d.ts.map +1 -0
- package/dist/infra/render/render-parse.js +155 -0
- package/dist/infra/render/render-parse.js.map +1 -0
- package/dist/infra/render/render.d.ts +11 -0
- package/dist/infra/render/render.d.ts.map +1 -0
- package/dist/infra/render/render.js +303 -0
- package/dist/infra/render/render.js.map +1 -0
- package/dist/infra/sst/sst-parse.d.ts +25 -0
- package/dist/infra/sst/sst-parse.d.ts.map +1 -0
- package/dist/infra/sst/sst-parse.js +45 -0
- package/dist/infra/sst/sst-parse.js.map +1 -0
- package/dist/infra/sst/sst.d.ts +19 -0
- package/dist/infra/sst/sst.d.ts.map +1 -0
- package/dist/infra/sst/sst.js +236 -0
- package/dist/infra/sst/sst.js.map +1 -0
- package/dist/infra/supabase/supabase-parse.d.ts +34 -0
- package/dist/infra/supabase/supabase-parse.d.ts.map +1 -0
- package/dist/infra/supabase/supabase-parse.js +113 -0
- package/dist/infra/supabase/supabase-parse.js.map +1 -0
- package/dist/infra/supabase/supabase.d.ts +15 -0
- package/dist/infra/supabase/supabase.d.ts.map +1 -0
- package/dist/infra/supabase/supabase.js +198 -0
- package/dist/infra/supabase/supabase.js.map +1 -0
- package/dist/infra/terraform/hcl-parse.d.ts +34 -0
- package/dist/infra/terraform/hcl-parse.d.ts.map +1 -0
- package/dist/infra/terraform/hcl-parse.js +203 -0
- package/dist/infra/terraform/hcl-parse.js.map +1 -0
- package/dist/infra/terraform/terraform.d.ts +14 -0
- package/dist/infra/terraform/terraform.d.ts.map +1 -0
- package/dist/infra/terraform/terraform.js +264 -0
- package/dist/infra/terraform/terraform.js.map +1 -0
- package/dist/infra/types.d.ts +151 -0
- package/dist/infra/types.d.ts.map +1 -0
- package/dist/infra/types.js +14 -0
- package/dist/infra/types.js.map +1 -0
- package/dist/infra/vercel/vercel-parse.d.ts +70 -0
- package/dist/infra/vercel/vercel-parse.d.ts.map +1 -0
- package/dist/infra/vercel/vercel-parse.js +194 -0
- package/dist/infra/vercel/vercel-parse.js.map +1 -0
- package/dist/infra/vercel/vercel.d.ts +30 -0
- package/dist/infra/vercel/vercel.d.ts.map +1 -0
- package/dist/infra/vercel/vercel.js +523 -0
- package/dist/infra/vercel/vercel.js.map +1 -0
- package/dist/infra/walk.d.ts +36 -0
- package/dist/infra/walk.d.ts.map +1 -0
- package/dist/infra/walk.js +85 -0
- package/dist/infra/walk.js.map +1 -0
- package/dist/safety.d.ts +13 -0
- package/dist/safety.d.ts.map +1 -0
- package/dist/safety.js +67 -0
- package/dist/safety.js.map +1 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +111 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +35 -0
- package/dist/version.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,1181 @@
|
|
|
1
|
+
// (Slice 1) — the React Native / Expo FrameworkAdapter.
|
|
2
|
+
//
|
|
3
|
+
// FIRST implementation of the FrameworkAdapter contract, and `detect()` ONLY
|
|
4
|
+
// this slice: it decides "is this an RN/Expo app, and is it Expo or bare RN?"
|
|
5
|
+
// from package.json deps + config-file existence — never source content
|
|
6
|
+
// (never-store-source). The graph contributions (navigation graph, JS↔native
|
|
7
|
+
// bridge boundary, screen roles) land in . reverses the
|
|
8
|
+
// "connect now, group later" deferral and adds the `groupingPrior` hook:
|
|
9
|
+
// screens registered in one navigator (Stack/Tab/Drawer) become one feature
|
|
10
|
+
// subsystem (a feature-folder fallback covers route files with no navigator —
|
|
11
|
+
// e.g. Expo Router pages). `classificationsNeeded` stays absent (grouping is
|
|
12
|
+
// fully deterministic — name/path-derived, zero LLM).
|
|
13
|
+
//
|
|
14
|
+
// Detection signals (manifest + config existence only):
|
|
15
|
+
// * deps: `react-native` and/or `expo`
|
|
16
|
+
// * configs: app.json · app.config.{js,ts,cjs,mjs} · metro.config.{js,ts,cjs}
|
|
17
|
+
// · react-native.config.{js,ts}
|
|
18
|
+
// * native: ios/ + android/ dirs
|
|
19
|
+
//
|
|
20
|
+
// The scoring is a PURE function (scoreReactNative) so it unit-tests without a
|
|
21
|
+
// real repo dir; the adapter just gathers the fs signals and calls it — the
|
|
22
|
+
// same pure-builder / fs-adapter split the infra adapters use.
|
|
23
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
24
|
+
import { join, posix, relative } from 'node:path';
|
|
25
|
+
import { SyntaxKind, } from 'ts-morph';
|
|
26
|
+
import { addAllSourceFiles, buildExtractionProject, readTsconfigCompilerOptions, toId, } from '../../graph/ts-morph-adapter.js';
|
|
27
|
+
import { SOURCE_EXTENSIONS } from '../../graph/file-graph.js';
|
|
28
|
+
import { extractExpoRouterTree } from '../routing/index.js';
|
|
29
|
+
// Config-file name sets — existence-only checks (we never read their content).
|
|
30
|
+
const APP_CONFIG_NAMES = ['app.config.js', 'app.config.ts', 'app.config.cjs', 'app.config.mjs'];
|
|
31
|
+
const METRO_CONFIG_NAMES = ['metro.config.js', 'metro.config.ts', 'metro.config.cjs'];
|
|
32
|
+
const RN_CONFIG_NAMES = ['react-native.config.js', 'react-native.config.ts'];
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Signal gathering (fs) — kept thin so the scoring stays pure + testable.
|
|
35
|
+
/** The merged dependency map from a package.json (deps + devDeps); {} on any error. */
|
|
36
|
+
function readDeps(baseDir) {
|
|
37
|
+
try {
|
|
38
|
+
const pkg = JSON.parse(readFileSync(join(baseDir, 'package.json'), 'utf8'));
|
|
39
|
+
return { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function isDir(p) {
|
|
46
|
+
try {
|
|
47
|
+
return statSync(p).isDirectory();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Gather the signal set for a single root dir. */
|
|
54
|
+
export function gatherReactNativeSignals(baseDir) {
|
|
55
|
+
const deps = readDeps(baseDir);
|
|
56
|
+
return {
|
|
57
|
+
hasReactNativeDep: 'react-native' in deps,
|
|
58
|
+
hasExpoDep: 'expo' in deps,
|
|
59
|
+
hasAppJson: existsSync(join(baseDir, 'app.json')),
|
|
60
|
+
hasAppConfig: APP_CONFIG_NAMES.some((n) => existsSync(join(baseDir, n))),
|
|
61
|
+
hasMetroConfig: METRO_CONFIG_NAMES.some((n) => existsSync(join(baseDir, n))),
|
|
62
|
+
hasRnConfig: RN_CONFIG_NAMES.some((n) => existsSync(join(baseDir, n))),
|
|
63
|
+
hasIosDir: isDir(join(baseDir, 'ios')),
|
|
64
|
+
hasAndroidDir: isDir(join(baseDir, 'android')),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Pure scoring.
|
|
69
|
+
/**
|
|
70
|
+
* Decide RN/Expo from the signal set. Returns a DetectMatch, or null if the
|
|
71
|
+
* signals are too weak/ambiguous to claim the stack (so the generic-TS
|
|
72
|
+
* fallthrough stays intact).
|
|
73
|
+
*
|
|
74
|
+
* A SUFFICIENT (match-on-its-own) signal is one of:
|
|
75
|
+
* * a `react-native` or `expo` dep — the authoritative signal
|
|
76
|
+
* * metro.config.* or react-native.config.* — RN-toolchain-specific
|
|
77
|
+
* * BOTH ios/ and android/ native dirs — a built RN project
|
|
78
|
+
*
|
|
79
|
+
* app.json / app.config / a single native dir are SUPPORTING signals (they
|
|
80
|
+
* raise confidence) but never sufficient alone — app.json in particular is too
|
|
81
|
+
* generic to claim a stack on its own.
|
|
82
|
+
*
|
|
83
|
+
* Variant: `expo` dep (or an `app.config.*` dynamic config) ⇒ 'expo', else
|
|
84
|
+
* 'bare'. Recorded in metadata + reflected in confidence.
|
|
85
|
+
*/
|
|
86
|
+
export function scoreReactNative(s, rootPath = '') {
|
|
87
|
+
const hasDep = s.hasReactNativeDep || s.hasExpoDep;
|
|
88
|
+
const hasRnToolingConfig = s.hasMetroConfig || s.hasRnConfig;
|
|
89
|
+
const hasBothNative = s.hasIosDir && s.hasAndroidDir;
|
|
90
|
+
// Gate: need at least one SUFFICIENT signal. Supporting-only ⇒ no match.
|
|
91
|
+
if (!hasDep && !hasRnToolingConfig && !hasBothNative)
|
|
92
|
+
return null;
|
|
93
|
+
let confidence = 0;
|
|
94
|
+
if (s.hasReactNativeDep)
|
|
95
|
+
confidence += 0.5;
|
|
96
|
+
if (s.hasExpoDep)
|
|
97
|
+
confidence += 0.5;
|
|
98
|
+
if (s.hasMetroConfig)
|
|
99
|
+
confidence += 0.2;
|
|
100
|
+
if (s.hasRnConfig)
|
|
101
|
+
confidence += 0.2;
|
|
102
|
+
if (hasBothNative)
|
|
103
|
+
confidence += 0.3;
|
|
104
|
+
else if (s.hasIosDir || s.hasAndroidDir)
|
|
105
|
+
confidence += 0.1;
|
|
106
|
+
if (s.hasAppConfig)
|
|
107
|
+
confidence += 0.1;
|
|
108
|
+
if (s.hasAppJson)
|
|
109
|
+
confidence += 0.05;
|
|
110
|
+
confidence = Math.min(1, Number(confidence.toFixed(2)));
|
|
111
|
+
const variant = s.hasExpoDep || s.hasAppConfig ? 'expo' : 'bare';
|
|
112
|
+
return {
|
|
113
|
+
adapter: 'react-native',
|
|
114
|
+
confidence,
|
|
115
|
+
rootPath,
|
|
116
|
+
metadata: {
|
|
117
|
+
variant,
|
|
118
|
+
signals: {
|
|
119
|
+
reactNativeDep: s.hasReactNativeDep,
|
|
120
|
+
expoDep: s.hasExpoDep,
|
|
121
|
+
appJson: s.hasAppJson,
|
|
122
|
+
appConfig: s.hasAppConfig,
|
|
123
|
+
metroConfig: s.hasMetroConfig,
|
|
124
|
+
rnConfig: s.hasRnConfig,
|
|
125
|
+
iosDir: s.hasIosDir,
|
|
126
|
+
androidDir: s.hasAndroidDir,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const ROLE_PRIORITY = {
|
|
132
|
+
endpoint: 9, // Expo Router API route — a server boundary; the only `gateway` here
|
|
133
|
+
navigator: 8, // React Navigation navigator (structural container)
|
|
134
|
+
layout: 7, // Expo Router layout (structural container)
|
|
135
|
+
nativeModule: 6, // JS↔native bridge
|
|
136
|
+
page: 5, // Expo Router page — a concrete routed screen
|
|
137
|
+
screen: 4, // React Navigation screen
|
|
138
|
+
route: 3, // Expo Router generic routable (Expo never emits it; kept for the type)
|
|
139
|
+
hook: 2,
|
|
140
|
+
component: 1,
|
|
141
|
+
};
|
|
142
|
+
const ROLE_KIND = {
|
|
143
|
+
endpoint: 'gateway',
|
|
144
|
+
navigator: 'frontend',
|
|
145
|
+
layout: 'frontend',
|
|
146
|
+
nativeModule: 'service',
|
|
147
|
+
page: 'frontend',
|
|
148
|
+
screen: 'frontend',
|
|
149
|
+
route: 'frontend',
|
|
150
|
+
hook: 'frontend',
|
|
151
|
+
component: 'frontend',
|
|
152
|
+
};
|
|
153
|
+
const NAVIGATOR_FACTORIES = new Set([
|
|
154
|
+
'createStackNavigator',
|
|
155
|
+
'createNativeStackNavigator',
|
|
156
|
+
'createBottomTabNavigator',
|
|
157
|
+
'createMaterialTopTabNavigator',
|
|
158
|
+
'createMaterialBottomTabNavigator',
|
|
159
|
+
'createDrawerNavigator',
|
|
160
|
+
]);
|
|
161
|
+
// `navigation.<m>('Screen')` — methods whose first string arg is a screen name.
|
|
162
|
+
// `navigate`/`replace` are nav-unambiguous → an unresolved string is LOGGED.
|
|
163
|
+
// `push`/`popTo`/`jumpTo`/`navigateDeprecated` collide with Array/other APIs
|
|
164
|
+
// (`arr.push('x')`), so they only emit on a REGISTRY HIT and miss silently —
|
|
165
|
+
// a non-screen string there is not a dropped nav target, it's not a nav call.
|
|
166
|
+
const NAV_LOG_METHODS = new Set(['navigate', 'replace']);
|
|
167
|
+
const NAV_SOFT_METHODS = new Set(['push', 'popTo', 'jumpTo', 'navigateDeprecated']);
|
|
168
|
+
const NATIVE_GLOBALS = new Set(['NativeModules', 'TurboModuleRegistry']);
|
|
169
|
+
const NATIVE_FUNCS = new Set(['requireNativeComponent', 'requireNativeModule']);
|
|
170
|
+
const SOURCE_EXT_SET = new Set(SOURCE_EXTENSIONS);
|
|
171
|
+
// Analysis is memoized on the FrameworkContext OBJECT (not repoDir): the step
|
|
172
|
+
// passes the SAME ctx to syntheticEdges + roleTags so they share one parse,
|
|
173
|
+
// while the merge walk's per-checkpoint ctx (same clone.dir path, different tree)
|
|
174
|
+
// gets a fresh analysis — no cross-tree staleness.
|
|
175
|
+
const ANALYSIS_CACHE = new WeakMap();
|
|
176
|
+
function roleTag(role) {
|
|
177
|
+
return { role, kind: ROLE_KIND[role], priority: ROLE_PRIORITY[role] };
|
|
178
|
+
}
|
|
179
|
+
// Keep the highest-priority candidate role per file (deterministic, lexical
|
|
180
|
+
// tiebreak) — a screen that also touches NativeModules reads as `nativeModule`.
|
|
181
|
+
function addRole(map, fileId, role) {
|
|
182
|
+
const cur = map.get(fileId);
|
|
183
|
+
if (cur === undefined ||
|
|
184
|
+
ROLE_PRIORITY[role] > ROLE_PRIORITY[cur] ||
|
|
185
|
+
(ROLE_PRIORITY[role] === ROLE_PRIORITY[cur] && role < cur)) {
|
|
186
|
+
map.set(fileId, role);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// localImportedName → resolved repo-relative file id, for a single source file.
|
|
190
|
+
// When ts-morph resolves the specifier we trust it; when it doesn't (a tsconfig
|
|
191
|
+
// path alias the in-memory Project mis-anchors), we fall back to our
|
|
192
|
+
// own alias resolver so `#/`-aliased screen imports still bind to real files.
|
|
193
|
+
function buildImportNameMap(sf, repoDir, aliasResolve) {
|
|
194
|
+
const map = new Map();
|
|
195
|
+
for (const decl of sf.getImportDeclarations()) {
|
|
196
|
+
const resolved = decl.getModuleSpecifierSourceFile();
|
|
197
|
+
let fileId;
|
|
198
|
+
if (resolved) {
|
|
199
|
+
fileId = toId(repoDir, resolved.getFilePath());
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const spec = decl.getModuleSpecifierValue();
|
|
203
|
+
fileId = spec ? aliasResolve(spec) : undefined;
|
|
204
|
+
}
|
|
205
|
+
if (!fileId)
|
|
206
|
+
continue;
|
|
207
|
+
const def = decl.getDefaultImport();
|
|
208
|
+
if (def)
|
|
209
|
+
map.set(def.getText(), fileId);
|
|
210
|
+
const ns = decl.getNamespaceImport();
|
|
211
|
+
if (ns)
|
|
212
|
+
map.set(ns.getText(), fileId);
|
|
213
|
+
for (const ni of decl.getNamedImports()) {
|
|
214
|
+
map.set((ni.getAliasNode() ?? ni.getNameNode()).getText(), fileId);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return map;
|
|
218
|
+
}
|
|
219
|
+
// Resolve a bare base path (extensionless) to a real source-file id: the path
|
|
220
|
+
// itself, then each source extension, then an index file, in the FIXED
|
|
221
|
+
// SOURCE_EXTENSIONS order so the pick is snapshot-stable.
|
|
222
|
+
function resolveAgainstIdSet(base, idSet) {
|
|
223
|
+
if (idSet.has(base))
|
|
224
|
+
return base;
|
|
225
|
+
for (const ext of SOURCE_EXTENSIONS) {
|
|
226
|
+
const cand = `${base}.${ext}`;
|
|
227
|
+
if (idSet.has(cand))
|
|
228
|
+
return cand;
|
|
229
|
+
}
|
|
230
|
+
for (const ext of SOURCE_EXTENSIONS) {
|
|
231
|
+
const cand = `${base}/index.${ext}`;
|
|
232
|
+
if (idSet.has(cand))
|
|
233
|
+
return cand;
|
|
234
|
+
}
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
// Build the alias patterns from the repo's tsconfig/jsconfig `paths`. The
|
|
238
|
+
// tsconfig READ (candidate search + JSONC parse: comments + trailing commas, no
|
|
239
|
+
// `extends` follow) is now the SHARED graph/ts-morph-adapter helper
|
|
240
|
+
// centralized it so this adapter and the structural extractor can't drift on
|
|
241
|
+
// what parses (the no-baseUrl + trailing-comma shape that previously dropped
|
|
242
|
+
// every alias). This adapter then keeps its OWN pattern model + id-set resolver
|
|
243
|
+
// (makeAliasResolver / makeSpecifierResolver), because RN must resolve arbitrary
|
|
244
|
+
// string specifiers — including the lazy `require('#/…')` / `import('#/…')`
|
|
245
|
+
// screen forms — against the file-id set, which ts-morph's static-declaration
|
|
246
|
+
// resolution structurally cannot do.
|
|
247
|
+
//
|
|
248
|
+
// Note: deferring to the shared reader adopts its "FIRST config declaring
|
|
249
|
+
// baseUrl||paths wins" rule (the config the TS compiler loads); the old local
|
|
250
|
+
// reader skipped a baseUrl-only config to keep hunting for one with `paths`.
|
|
251
|
+
// Negligible/more-correct in practice (paths + baseUrl live in the same root
|
|
252
|
+
// tsconfig); fixtures + real RN repos are unaffected. The '*' wildcard + leading
|
|
253
|
+
// './' are normalized away so resolution stays a plain join.
|
|
254
|
+
function readAliasPatterns(repoDir) {
|
|
255
|
+
const paths = readTsconfigCompilerOptions(repoDir)?.paths;
|
|
256
|
+
if (!paths || typeof paths !== 'object')
|
|
257
|
+
return [];
|
|
258
|
+
const out = [];
|
|
259
|
+
for (const [key, val] of Object.entries(paths)) {
|
|
260
|
+
if (!Array.isArray(val))
|
|
261
|
+
continue;
|
|
262
|
+
const wildcard = key.endsWith('/*');
|
|
263
|
+
const prefix = wildcard ? key.slice(0, -1) : key; // '#/*' → '#/'
|
|
264
|
+
const targets = val
|
|
265
|
+
.filter((t) => typeof t === 'string')
|
|
266
|
+
.map((t) => {
|
|
267
|
+
let s = t.replace(/^\.\//, '').replace(/^\//, '');
|
|
268
|
+
if (wildcard)
|
|
269
|
+
s = s.replace(/\*$/, '').replace(/\/$/, ''); // './src/*' → 'src'
|
|
270
|
+
return s;
|
|
271
|
+
});
|
|
272
|
+
if (targets.length > 0)
|
|
273
|
+
out.push({ prefix, wildcard, targets });
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
// A non-relative specifier → a real file id via the tsconfig path aliases, or
|
|
278
|
+
// undefined. Tries each matching pattern's targets in declared order against the
|
|
279
|
+
// id set (with extension/index resolution) — fully deterministic.
|
|
280
|
+
function makeAliasResolver(patterns, idSet) {
|
|
281
|
+
return (spec) => {
|
|
282
|
+
for (const { prefix, wildcard, targets } of patterns) {
|
|
283
|
+
if (wildcard) {
|
|
284
|
+
if (!spec.startsWith(prefix))
|
|
285
|
+
continue;
|
|
286
|
+
const rest = spec.slice(prefix.length);
|
|
287
|
+
for (const base of targets) {
|
|
288
|
+
const full = base ? `${base}/${rest}` : rest;
|
|
289
|
+
const r = resolveAgainstIdSet(full, idSet);
|
|
290
|
+
if (r)
|
|
291
|
+
return r;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
if (spec !== prefix)
|
|
296
|
+
continue;
|
|
297
|
+
for (const base of targets) {
|
|
298
|
+
const r = resolveAgainstIdSet(base, idSet);
|
|
299
|
+
if (r)
|
|
300
|
+
return r;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return undefined;
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
// Deterministic specifier resolver over the known source-file id set, for the
|
|
308
|
+
// lazy `require('…')` / `import('…')` forms: RELATIVE specifiers resolve against
|
|
309
|
+
// the importing file's dir; a non-relative (bare/aliased) specifier falls to the
|
|
310
|
+
// tsconfig alias resolver. Snapshot-stable by construction.
|
|
311
|
+
function makeSpecifierResolver(idSet, aliasResolve) {
|
|
312
|
+
return (fromFileId, spec) => {
|
|
313
|
+
if (!spec.startsWith('.'))
|
|
314
|
+
return aliasResolve(spec);
|
|
315
|
+
const slash = fromFileId.lastIndexOf('/');
|
|
316
|
+
const dir = slash >= 0 ? fromFileId.slice(0, slash) : '';
|
|
317
|
+
return resolveAgainstIdSet(posix.join(dir, spec), idSet);
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
// Structural accessors — ts-morph exposes getExpression()/getInitializer()/
|
|
321
|
+
// getLiteralValue() on many concrete node types but not on the base Node, and we
|
|
322
|
+
// reach them only after a getKind() gate, so a guarded optional-call is both safe
|
|
323
|
+
// and avoids a forest of per-kind casts.
|
|
324
|
+
function innerExpression(node) {
|
|
325
|
+
return node.getExpression?.();
|
|
326
|
+
}
|
|
327
|
+
function nodeInitializer(node) {
|
|
328
|
+
return node.getInitializer?.();
|
|
329
|
+
}
|
|
330
|
+
function literalValue(node) {
|
|
331
|
+
return node.getLiteralValue?.();
|
|
332
|
+
}
|
|
333
|
+
// Statically resolve a string from an expression node — a string literal, an
|
|
334
|
+
// `as`/parenthesis/non-null-wrapped literal (the RUNTIME value; a type cast like
|
|
335
|
+
// `'X' as 'Y'` is type-only), or a const / enum member / object-literal-const
|
|
336
|
+
// property whose value is itself a static string. Truly computed → undefined.
|
|
337
|
+
function staticStringValue(node, depth = 0) {
|
|
338
|
+
if (depth > 8)
|
|
339
|
+
return undefined;
|
|
340
|
+
switch (node.getKind()) {
|
|
341
|
+
case SyntaxKind.StringLiteral:
|
|
342
|
+
case SyntaxKind.NoSubstitutionTemplateLiteral:
|
|
343
|
+
return literalValue(node);
|
|
344
|
+
case SyntaxKind.ParenthesizedExpression:
|
|
345
|
+
case SyntaxKind.AsExpression:
|
|
346
|
+
case SyntaxKind.SatisfiesExpression:
|
|
347
|
+
case SyntaxKind.NonNullExpression:
|
|
348
|
+
case SyntaxKind.TypeAssertionExpression: {
|
|
349
|
+
const inner = innerExpression(node);
|
|
350
|
+
return inner ? staticStringValue(inner, depth + 1) : undefined;
|
|
351
|
+
}
|
|
352
|
+
case SyntaxKind.Identifier:
|
|
353
|
+
case SyntaxKind.PropertyAccessExpression:
|
|
354
|
+
return resolveConstString(node, depth);
|
|
355
|
+
default:
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Resolve an Identifier / PropertyAccess referring to a const string, enum
|
|
360
|
+
// member, or const object property to its string value (symbol-based; degrades
|
|
361
|
+
// to undefined without resolution rather than throwing).
|
|
362
|
+
function resolveConstString(node, depth) {
|
|
363
|
+
let sym;
|
|
364
|
+
try {
|
|
365
|
+
sym = node.getSymbol();
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
const decl = sym?.getValueDeclaration() ?? sym?.getDeclarations()?.[0];
|
|
371
|
+
if (!decl)
|
|
372
|
+
return undefined;
|
|
373
|
+
switch (decl.getKind()) {
|
|
374
|
+
case SyntaxKind.VariableDeclaration:
|
|
375
|
+
case SyntaxKind.EnumMember:
|
|
376
|
+
case SyntaxKind.PropertyAssignment: {
|
|
377
|
+
const init = nodeInitializer(decl);
|
|
378
|
+
return init ? staticStringValue(init, depth + 1) : undefined;
|
|
379
|
+
}
|
|
380
|
+
default:
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Resolve a screen-registration `name` to its runtime string value, or undefined
|
|
385
|
+
// when it's truly computed/dynamic. Handles a JSX string attr (`name="X"`) and a
|
|
386
|
+
// statically-resolvable JSX expression (`name={'X' as 'Y'}`, `name={ROUTE}`, …).
|
|
387
|
+
function resolveScreenName(nameAttr) {
|
|
388
|
+
const init = nameAttr.getInitializer();
|
|
389
|
+
if (!init)
|
|
390
|
+
return undefined;
|
|
391
|
+
if (init.getKind() === SyntaxKind.StringLiteral) {
|
|
392
|
+
return init.getLiteralValue();
|
|
393
|
+
}
|
|
394
|
+
if (init.getKind() === SyntaxKind.JsxExpression) {
|
|
395
|
+
const expr = init.getExpression();
|
|
396
|
+
return expr ? staticStringValue(expr) : undefined;
|
|
397
|
+
}
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
// Resolve a component-producing expression used in a screen registration to its
|
|
401
|
+
// file id. Handles BOTH eager `component={X}` and the lazy `getComponent` forms
|
|
402
|
+
//: an imported identifier, a namespace member, or a `require('./x')` /
|
|
403
|
+
// `import('./x')` (incl. a `.Named` access on either). `as`/paren/non-null
|
|
404
|
+
// wrappers are unwrapped. An imported symbol → its source file; a locally-defined
|
|
405
|
+
// component (or nested navigator) → the registering file. Anything else (an
|
|
406
|
+
// inline `() => <Foo/>`, a require we can't resolve install-free) → undefined,
|
|
407
|
+
// so the caller LOGS + skips rather than mis-register the screen.
|
|
408
|
+
function resolveComponentRef(expr, imports, selfFileId, resolveSpecifier, depth = 0) {
|
|
409
|
+
if (depth > 8)
|
|
410
|
+
return undefined;
|
|
411
|
+
switch (expr.getKind()) {
|
|
412
|
+
case SyntaxKind.ParenthesizedExpression:
|
|
413
|
+
case SyntaxKind.AsExpression:
|
|
414
|
+
case SyntaxKind.SatisfiesExpression:
|
|
415
|
+
case SyntaxKind.NonNullExpression:
|
|
416
|
+
case SyntaxKind.TypeAssertionExpression: {
|
|
417
|
+
const inner = innerExpression(expr);
|
|
418
|
+
return inner ? resolveComponentRef(inner, imports, selfFileId, resolveSpecifier, depth + 1) : undefined;
|
|
419
|
+
}
|
|
420
|
+
case SyntaxKind.Identifier:
|
|
421
|
+
return imports.get(expr.getText()) ?? selfFileId;
|
|
422
|
+
case SyntaxKind.PropertyAccessExpression: {
|
|
423
|
+
const base = expr.getExpression();
|
|
424
|
+
// `require('./x').Named` / `import('./x').Named` → resolve the call.
|
|
425
|
+
if (base.getKind() === SyntaxKind.CallExpression) {
|
|
426
|
+
return resolveComponentRef(base, imports, selfFileId, resolveSpecifier, depth + 1);
|
|
427
|
+
}
|
|
428
|
+
// `Ns.Component` → resolve the root identifier (imported, else self file).
|
|
429
|
+
if (base.getKind() === SyntaxKind.Identifier) {
|
|
430
|
+
return imports.get(base.getText()) ?? selfFileId;
|
|
431
|
+
}
|
|
432
|
+
return resolveComponentRef(base, imports, selfFileId, resolveSpecifier, depth + 1);
|
|
433
|
+
}
|
|
434
|
+
case SyntaxKind.CallExpression: {
|
|
435
|
+
const call = expr;
|
|
436
|
+
const callee = call.getExpression();
|
|
437
|
+
const isRequire = callee.getKind() === SyntaxKind.Identifier && callee.getText() === 'require';
|
|
438
|
+
const isImport = callee.getKind() === SyntaxKind.ImportKeyword || callee.getText() === 'import';
|
|
439
|
+
if (!isRequire && !isImport)
|
|
440
|
+
return undefined;
|
|
441
|
+
const arg = call.getArguments()[0];
|
|
442
|
+
if (!arg || arg.getKind() !== SyntaxKind.StringLiteral)
|
|
443
|
+
return undefined;
|
|
444
|
+
return resolveSpecifier(arg.getLiteralValue());
|
|
445
|
+
}
|
|
446
|
+
default:
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// From a `getComponent={() => …}` initializer (an arrow / function expression),
|
|
451
|
+
// return the single expression it RETURNS — a concise arrow body, or the first
|
|
452
|
+
// top-level `return`'s argument in a block body. Undefined otherwise.
|
|
453
|
+
function getComponentReturnExpr(fn) {
|
|
454
|
+
const k = fn.getKind();
|
|
455
|
+
if (k !== SyntaxKind.ArrowFunction && k !== SyntaxKind.FunctionExpression)
|
|
456
|
+
return undefined;
|
|
457
|
+
const body = fn.getBody();
|
|
458
|
+
if (!body)
|
|
459
|
+
return undefined;
|
|
460
|
+
if (body.getKind() !== SyntaxKind.Block)
|
|
461
|
+
return body; // concise arrow body
|
|
462
|
+
for (const stmt of body.getStatements()) {
|
|
463
|
+
if (stmt.getKind() === SyntaxKind.ReturnStatement) {
|
|
464
|
+
return stmt.getExpression();
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
// The static-config screen property KEY → screen name: a plain identifier/string
|
|
470
|
+
// key, or a computed `[ROUTE]` key resolved to its static string. Dynamic → undefined.
|
|
471
|
+
function staticPropName(pa) {
|
|
472
|
+
const nameNode = pa.getNameNode();
|
|
473
|
+
if (nameNode.getKind() === SyntaxKind.ComputedPropertyName) {
|
|
474
|
+
const e = innerExpression(nameNode);
|
|
475
|
+
return e ? staticStringValue(e) : undefined;
|
|
476
|
+
}
|
|
477
|
+
if (nameNode.getKind() === SyntaxKind.StringLiteral) {
|
|
478
|
+
return nameNode.getLiteralValue();
|
|
479
|
+
}
|
|
480
|
+
return nameNode.getText();
|
|
481
|
+
}
|
|
482
|
+
// A static-config screen VALUE → component file id. The value is either the
|
|
483
|
+
// component reference directly (`Home: HomeScreen`) or a screen-config object
|
|
484
|
+
// (`Home: { screen: X }` / `{ component: X }` / `{ getComponent: () => … }`).
|
|
485
|
+
function resolveConfigScreenComponent(init, imports, selfFileId, resolveSpecifier) {
|
|
486
|
+
if (init.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
487
|
+
const obj = init;
|
|
488
|
+
for (const key of ['screen', 'component', 'getComponent']) {
|
|
489
|
+
const p = obj.getProperty(key);
|
|
490
|
+
if (!p || p.getKind() !== SyntaxKind.PropertyAssignment)
|
|
491
|
+
continue;
|
|
492
|
+
const v = p.getInitializer();
|
|
493
|
+
if (!v)
|
|
494
|
+
continue;
|
|
495
|
+
if (key === 'getComponent') {
|
|
496
|
+
const ret = getComponentReturnExpr(v);
|
|
497
|
+
return ret ? resolveComponentRef(ret, imports, selfFileId, resolveSpecifier) : undefined;
|
|
498
|
+
}
|
|
499
|
+
return resolveComponentRef(v, imports, selfFileId, resolveSpecifier);
|
|
500
|
+
}
|
|
501
|
+
return undefined;
|
|
502
|
+
}
|
|
503
|
+
return resolveComponentRef(init, imports, selfFileId, resolveSpecifier);
|
|
504
|
+
}
|
|
505
|
+
// Collect screen registrations from one file: JSX `<X.Screen name component|
|
|
506
|
+
// getComponent>` plus the static `createXNavigator({ screens })` config.
|
|
507
|
+
// `resolveSpecifier` is the per-file (bound to this file) relative require/import
|
|
508
|
+
// resolver. Diagnostics accumulate into `diag` for one aggregated log.
|
|
509
|
+
function collectScreens(sf, imports, selfFileId, registry, screenFiles, resolveSpecifier, diag, membership) {
|
|
510
|
+
let isNavigator = false;
|
|
511
|
+
const register = (screenName, componentFile) => {
|
|
512
|
+
if (!registry.has(screenName))
|
|
513
|
+
registry.set(screenName, componentFile);
|
|
514
|
+
screenFiles.add(componentFile);
|
|
515
|
+
// record this screen's component file as a member of THIS file's
|
|
516
|
+
// navigator(s), so the grouping prior can group a navigator's screens into
|
|
517
|
+
// one feature subsystem (independent of the global first-wins registry).
|
|
518
|
+
membership.add(componentFile);
|
|
519
|
+
};
|
|
520
|
+
// The component file from a screen element's `component` / `getComponent` attr.
|
|
521
|
+
// Returns a file id (resolved), null (an attr was present but unresolvable →
|
|
522
|
+
// caller logs), or undefined (no component-binding attr at all).
|
|
523
|
+
const componentFileOf = (el) => {
|
|
524
|
+
const comp = el.getAttribute('component');
|
|
525
|
+
if (comp && comp.getKind() === SyntaxKind.JsxAttribute) {
|
|
526
|
+
const init = comp.getInitializer();
|
|
527
|
+
if (init && init.getKind() === SyntaxKind.JsxExpression) {
|
|
528
|
+
const e = init.getExpression();
|
|
529
|
+
if (e)
|
|
530
|
+
return resolveComponentRef(e, imports, selfFileId, resolveSpecifier) ?? null;
|
|
531
|
+
}
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
const getComp = el.getAttribute('getComponent');
|
|
535
|
+
if (getComp && getComp.getKind() === SyntaxKind.JsxAttribute) {
|
|
536
|
+
const init = getComp.getInitializer();
|
|
537
|
+
if (init && init.getKind() === SyntaxKind.JsxExpression) {
|
|
538
|
+
const fn = init.getExpression();
|
|
539
|
+
const ret = fn ? getComponentReturnExpr(fn) : undefined;
|
|
540
|
+
if (ret)
|
|
541
|
+
return resolveComponentRef(ret, imports, selfFileId, resolveSpecifier) ?? null;
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
return undefined;
|
|
546
|
+
};
|
|
547
|
+
const jsxEls = [
|
|
548
|
+
...sf.getDescendantsOfKind(SyntaxKind.JsxOpeningElement),
|
|
549
|
+
...sf.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement),
|
|
550
|
+
];
|
|
551
|
+
for (const el of jsxEls) {
|
|
552
|
+
const tag = el.getTagNameNode().getText();
|
|
553
|
+
if (tag.endsWith('.Navigator') || tag === 'Navigator')
|
|
554
|
+
isNavigator = true;
|
|
555
|
+
if (!(tag.endsWith('.Screen') || tag === 'Screen'))
|
|
556
|
+
continue;
|
|
557
|
+
isNavigator = true;
|
|
558
|
+
const nameAttr = el.getAttribute('name');
|
|
559
|
+
if (!nameAttr || nameAttr.getKind() !== SyntaxKind.JsxAttribute)
|
|
560
|
+
continue;
|
|
561
|
+
const screenName = resolveScreenName(nameAttr);
|
|
562
|
+
if (screenName === undefined) {
|
|
563
|
+
diag.dynamicNames++;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const componentFile = componentFileOf(el);
|
|
567
|
+
if (componentFile === undefined)
|
|
568
|
+
continue; // no component/getComponent binding
|
|
569
|
+
if (componentFile === null) {
|
|
570
|
+
diag.unresolvedComponents.add(screenName);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
register(screenName, componentFile);
|
|
574
|
+
}
|
|
575
|
+
// Static config API: createXNavigator({ screens: { Home: HomeScreen, … } }).
|
|
576
|
+
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
577
|
+
if (!NAVIGATOR_FACTORIES.has(call.getExpression().getText()))
|
|
578
|
+
continue;
|
|
579
|
+
isNavigator = true;
|
|
580
|
+
const arg = call.getArguments()[0];
|
|
581
|
+
if (!arg || arg.getKind() !== SyntaxKind.ObjectLiteralExpression)
|
|
582
|
+
continue;
|
|
583
|
+
const screensProp = arg.getProperty('screens');
|
|
584
|
+
if (!screensProp || screensProp.getKind() !== SyntaxKind.PropertyAssignment)
|
|
585
|
+
continue;
|
|
586
|
+
const screensInit = screensProp.getInitializer();
|
|
587
|
+
if (!screensInit || screensInit.getKind() !== SyntaxKind.ObjectLiteralExpression)
|
|
588
|
+
continue;
|
|
589
|
+
for (const prop of screensInit.getProperties()) {
|
|
590
|
+
if (prop.getKind() !== SyntaxKind.PropertyAssignment)
|
|
591
|
+
continue;
|
|
592
|
+
const pa = prop;
|
|
593
|
+
const screenName = staticPropName(pa);
|
|
594
|
+
if (screenName === undefined) {
|
|
595
|
+
diag.dynamicNames++;
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
const init = pa.getInitializer();
|
|
599
|
+
if (!init)
|
|
600
|
+
continue;
|
|
601
|
+
const componentFile = resolveConfigScreenComponent(init, imports, selfFileId, resolveSpecifier);
|
|
602
|
+
if (componentFile === undefined) {
|
|
603
|
+
diag.unresolvedComponents.add(screenName);
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
register(screenName, componentFile);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return isNavigator;
|
|
610
|
+
}
|
|
611
|
+
// Does this file touch the JS↔native bridge (NativeModules / TurboModules /
|
|
612
|
+
// requireNative*)? Specific patterns only, to avoid false positives.
|
|
613
|
+
function usesNative(sf) {
|
|
614
|
+
for (const pae of sf.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)) {
|
|
615
|
+
const base = pae.getExpression();
|
|
616
|
+
if (base.getKind() === SyntaxKind.Identifier && NATIVE_GLOBALS.has(base.getText()))
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
for (const eae of sf.getDescendantsOfKind(SyntaxKind.ElementAccessExpression)) {
|
|
620
|
+
const base = eae.getExpression();
|
|
621
|
+
if (base.getKind() === SyntaxKind.Identifier && NATIVE_GLOBALS.has(base.getText()))
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
for (const vd of sf.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
625
|
+
const init = vd.getInitializer();
|
|
626
|
+
if (init && init.getKind() === SyntaxKind.Identifier && NATIVE_GLOBALS.has(init.getText()))
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
630
|
+
if (NATIVE_FUNCS.has(call.getExpression().getText()))
|
|
631
|
+
return true;
|
|
632
|
+
}
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
function hasJsx(sf) {
|
|
636
|
+
return (sf.getFirstDescendantByKind(SyntaxKind.JsxElement) !== undefined ||
|
|
637
|
+
sf.getFirstDescendantByKind(SyntaxKind.JsxSelfClosingElement) !== undefined ||
|
|
638
|
+
sf.getFirstDescendantByKind(SyntaxKind.JsxFragment) !== undefined);
|
|
639
|
+
}
|
|
640
|
+
// basename without extension: src/hooks/useAuth.ts → 'useAuth'.
|
|
641
|
+
function baseName(fileId) {
|
|
642
|
+
const last = fileId.split('/').pop() ?? fileId;
|
|
643
|
+
return last.replace(/\.[^.]+$/, '');
|
|
644
|
+
}
|
|
645
|
+
function inScope(fileId, rootPath) {
|
|
646
|
+
if (rootPath === '')
|
|
647
|
+
return true;
|
|
648
|
+
return fileId === rootPath || fileId.startsWith(`${rootPath}/`);
|
|
649
|
+
}
|
|
650
|
+
// ---------------------------------------------------------------------------
|
|
651
|
+
// feature grouping prior (the deferred "group later").
|
|
652
|
+
//
|
|
653
|
+
// PRIMARY signal: navigator membership — screens registered in one navigator
|
|
654
|
+
// (Stack/Tab/Drawer) belong to ONE feature subsystem, regardless of where their
|
|
655
|
+
// files sit on disk (the headline value: screens grouped by feature, not by
|
|
656
|
+
// directory). The group id derives from the NAVIGATOR FILE name (deterministic +
|
|
657
|
+
// meaningful — RootNavigator → 'root', AuthStack → 'auth'), NEVER an index.
|
|
658
|
+
//
|
|
659
|
+
// that primary signal OVER-COLLAPSES at real scale: production RN apps
|
|
660
|
+
// (Bluesky) register EVERY screen in a SINGLE `Navigation.tsx` (often via tab
|
|
661
|
+
// sub-navigators in one file), so navigator membership yields ONE group for the
|
|
662
|
+
// whole app. The fix is an adaptive per-navigator mode:
|
|
663
|
+
// * COHESIVE navigator (its screens share ONE feature directory) → keep them
|
|
664
|
+
// as one group named by the navigator (the AuthStack case, the fixtures).
|
|
665
|
+
// * MEGA / incoherent navigator (its screens span MANY feature directories) →
|
|
666
|
+
// the navigator is a routing HUB, not a feature; SPLIT its screens to their
|
|
667
|
+
// own feature folders (Bluesky's src/screens/<Feature>/… + src/view/screens/
|
|
668
|
+
// <Feature>.tsx), so a 78-screen app becomes ~dozens of legible features.
|
|
669
|
+
// The cohesion test is the distinct directory-feature count over the navigator's
|
|
670
|
+
// member screens (filename-as-feature is excluded from cohesion so same-directory
|
|
671
|
+
// screens read as cohesive). Logged per mode — no silent cap.
|
|
672
|
+
//
|
|
673
|
+
// FALLBACK: a feature folder for screen/route-family files in NO navigator — the
|
|
674
|
+
// Expo Router case (file-based routing has no navigator). The feature is the
|
|
675
|
+
// first meaningful directory segment under a container root (a route group
|
|
676
|
+
// `(tabs)` → 'tabs'). A root-level file with no determinable feature is LOGGED,
|
|
677
|
+
// never silently dropped (no filename fallback here — that's the SPLIT path).
|
|
678
|
+
//
|
|
679
|
+
// The contribute-step namespaces these as `<adapter>:<id>` (here
|
|
680
|
+
// `react-native:<id>`) and overrides the claimed modules' subsystem, beating the
|
|
681
|
+
// directory heuristic. Fully deterministic (name/path-derived) → no LLM, so no
|
|
682
|
+
// `classificationsNeeded`.
|
|
683
|
+
// Roles whose files are candidates for the feature-folder fallback (screens that
|
|
684
|
+
// somehow escaped navigator membership + the Expo Router route family).
|
|
685
|
+
const FALLBACK_ROLES = new Set(['screen', 'page', 'layout', 'route', 'endpoint']);
|
|
686
|
+
// Directory segments that are CONTAINERS, not features — stripped (repeatedly,
|
|
687
|
+
// outermost-first) before the first real feature segment is read. Covers the
|
|
688
|
+
// common RN/Expo layout roots (src · app · view(s) · screen(s) · scene(s) ·
|
|
689
|
+
// feature(s) · page(s) · modules), so `src/view/screens/Home.tsx` → 'Home' and
|
|
690
|
+
// `src/screens/Messages/ChatList.tsx` → 'Messages'.
|
|
691
|
+
const CONTAINER_DIRS = new Set([
|
|
692
|
+
'src',
|
|
693
|
+
'app',
|
|
694
|
+
'view',
|
|
695
|
+
'views',
|
|
696
|
+
'screen',
|
|
697
|
+
'screens',
|
|
698
|
+
'scene',
|
|
699
|
+
'scenes',
|
|
700
|
+
'feature',
|
|
701
|
+
'features',
|
|
702
|
+
'page',
|
|
703
|
+
'pages',
|
|
704
|
+
'modules',
|
|
705
|
+
]);
|
|
706
|
+
// Trailing navigator-y suffixes stripped from a navigator file's basename to get
|
|
707
|
+
// a clean feature name (only when something remains).
|
|
708
|
+
const NAV_NAME_SUFFIX = /(Navigators?|Navigation|Nav|Stack|Tabs?|Drawer|Routers?|Routes|Screens)$/;
|
|
709
|
+
// Basenames too generic to name a navigator group — fall back to the parent dir.
|
|
710
|
+
const GENERIC_NAV_BASENAMES = new Set([
|
|
711
|
+
'index',
|
|
712
|
+
'navigation',
|
|
713
|
+
'navigator',
|
|
714
|
+
'navigators',
|
|
715
|
+
'routes',
|
|
716
|
+
'router',
|
|
717
|
+
'app',
|
|
718
|
+
]);
|
|
719
|
+
// Deterministic slug (camelCase → kebab, drop non-alnum). Mirrors the Nest/infra
|
|
720
|
+
// id discipline so the group id is stable across snapshots.
|
|
721
|
+
function slugify(s) {
|
|
722
|
+
return s
|
|
723
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
724
|
+
.toLowerCase()
|
|
725
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
726
|
+
.replace(/^-+|-+$/g, '');
|
|
727
|
+
}
|
|
728
|
+
// Humanize a raw name into a subsystem label (split camel/kebab/snake, title-case).
|
|
729
|
+
function humanizeName(s) {
|
|
730
|
+
const words = s
|
|
731
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
732
|
+
.replace(/[_\-.]+/g, ' ')
|
|
733
|
+
.trim()
|
|
734
|
+
.split(/\s+/)
|
|
735
|
+
.filter(Boolean);
|
|
736
|
+
if (words.length === 0)
|
|
737
|
+
return s;
|
|
738
|
+
return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
739
|
+
}
|
|
740
|
+
// The last directory segment of a file id (the parent dir name); '' at root.
|
|
741
|
+
function parentDirName(fileId) {
|
|
742
|
+
const segs = fileId.split('/');
|
|
743
|
+
return segs.length >= 2 ? segs[segs.length - 2] : '';
|
|
744
|
+
}
|
|
745
|
+
// A navigator file's feature name: its basename with a navigator-y suffix
|
|
746
|
+
// stripped; a generic basename (index/navigation/…) falls back to the parent dir.
|
|
747
|
+
function navigatorGroupName(fileId) {
|
|
748
|
+
let base = baseName(fileId);
|
|
749
|
+
if (GENERIC_NAV_BASENAMES.has(base.toLowerCase())) {
|
|
750
|
+
base = parentDirName(fileId) || base;
|
|
751
|
+
}
|
|
752
|
+
const stripped = base.replace(NAV_NAME_SUFFIX, '');
|
|
753
|
+
return stripped.length > 0 ? stripped : base;
|
|
754
|
+
}
|
|
755
|
+
// A screen/route file's feature name. Strips leading CONTAINER segments, then:
|
|
756
|
+
// * a remaining directory segment is the feature (a route group `(tabs)` →
|
|
757
|
+
// 'tabs');
|
|
758
|
+
// * if NOTHING remains (the file sits directly in a container, e.g.
|
|
759
|
+
// `src/view/screens/Home.tsx`) → `allowFilename` decides: the SPLIT path
|
|
760
|
+
// uses the file basename ('Home'); the Expo fallback path returns undefined
|
|
761
|
+
// (a root-level route — logged as ungroupable, never silently dropped).
|
|
762
|
+
function deriveFeature(fileId, allowFilename) {
|
|
763
|
+
const segs = fileId.split('/');
|
|
764
|
+
const file = segs.pop() ?? fileId;
|
|
765
|
+
let i = 0;
|
|
766
|
+
while (i < segs.length && CONTAINER_DIRS.has(segs[i].toLowerCase()))
|
|
767
|
+
i++;
|
|
768
|
+
const first = segs[i];
|
|
769
|
+
if (first) {
|
|
770
|
+
const stripped = first.replace(/^\((.*)\)$/, '$1'); // route group → bare name
|
|
771
|
+
if (stripped.length > 0)
|
|
772
|
+
return stripped;
|
|
773
|
+
}
|
|
774
|
+
if (!allowFilename)
|
|
775
|
+
return undefined;
|
|
776
|
+
const fn = baseName(file);
|
|
777
|
+
return fn.length > 0 ? fn : undefined;
|
|
778
|
+
}
|
|
779
|
+
// The directory-only feature key used for the COHESION test (no filename
|
|
780
|
+
// fallback): files in the same container directory share a key, so same-folder
|
|
781
|
+
// screens read as one cohesive feature. A file with no sub-feature directory
|
|
782
|
+
// keys on its container dir path, distinguishing e.g. src/view/screens from
|
|
783
|
+
// src/legacy/screens.
|
|
784
|
+
function featureDirKey(fileId) {
|
|
785
|
+
const dir = deriveFeature(fileId, false);
|
|
786
|
+
if (dir)
|
|
787
|
+
return dir.toLowerCase();
|
|
788
|
+
const slash = fileId.lastIndexOf('/');
|
|
789
|
+
return `dir:${slash >= 0 ? fileId.slice(0, slash) : ''}`;
|
|
790
|
+
}
|
|
791
|
+
// Build the grouping prior: per-navigator COHESIVE (one group) vs MEGA (split by
|
|
792
|
+
// feature folder), then a feature-folder fallback for non-navigator screen/route
|
|
793
|
+
// files. Deterministic by construction: sorted inputs, set-union accumulation,
|
|
794
|
+
// sorted output, lexical label tiebreak — so two runs are byte-identical.
|
|
795
|
+
function buildGroupingPrior(navMembership, roles) {
|
|
796
|
+
const acc = new Map();
|
|
797
|
+
const add = (id, label, files) => {
|
|
798
|
+
let g = acc.get(id);
|
|
799
|
+
if (!g) {
|
|
800
|
+
g = { label, fileIds: new Set() };
|
|
801
|
+
acc.set(id, g);
|
|
802
|
+
}
|
|
803
|
+
else if (label < g.label) {
|
|
804
|
+
g.label = label; // deterministic on the (rare) same-id/different-label case
|
|
805
|
+
}
|
|
806
|
+
for (const f of files)
|
|
807
|
+
g.fileIds.add(f);
|
|
808
|
+
};
|
|
809
|
+
const claimed = new Set();
|
|
810
|
+
const ungroupable = [];
|
|
811
|
+
let keptNavs = 0;
|
|
812
|
+
let splitNavs = 0;
|
|
813
|
+
// Assign one screen-ish file to its own feature folder (filename-as-feature
|
|
814
|
+
// when it sits directly in a container) — the SPLIT path + the mega-navigator's
|
|
815
|
+
// own file. A file with no determinable feature is logged, never dropped.
|
|
816
|
+
const splitFile = (f) => {
|
|
817
|
+
if (claimed.has(f))
|
|
818
|
+
return;
|
|
819
|
+
const feat = deriveFeature(f, true);
|
|
820
|
+
if (!feat) {
|
|
821
|
+
ungroupable.push(f);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
const id = slugify(feat) || 'feature';
|
|
825
|
+
add(id, humanizeName(feat) || id, [f]);
|
|
826
|
+
claimed.add(f);
|
|
827
|
+
};
|
|
828
|
+
// Per-navigator (sorted; first-claim wins so a screen in two navigators is
|
|
829
|
+
// deterministic). A navigator with NO statically-resolvable screens has nothing
|
|
830
|
+
// to group → skip (keeps the directory heuristic, not a noisy singleton).
|
|
831
|
+
for (const navFile of [...navMembership.keys()].sort()) {
|
|
832
|
+
const members = navMembership.get(navFile);
|
|
833
|
+
if (members.size === 0)
|
|
834
|
+
continue;
|
|
835
|
+
const memberList = [...members].sort();
|
|
836
|
+
const dirKeys = new Set(memberList.map(featureDirKey));
|
|
837
|
+
if (dirKeys.size <= 1) {
|
|
838
|
+
// COHESIVE — the navigator's screens are one feature; keep them together
|
|
839
|
+
// under the navigator name (defeats the directory heuristic — the headline
|
|
840
|
+
// value). Only add files not already claimed by an earlier navigator.
|
|
841
|
+
keptNavs++;
|
|
842
|
+
const name = navigatorGroupName(navFile);
|
|
843
|
+
const id = slugify(name) || 'navigator';
|
|
844
|
+
const fresh = [navFile, ...memberList].filter((f) => !claimed.has(f));
|
|
845
|
+
if (fresh.length > 0) {
|
|
846
|
+
add(id, humanizeName(name) || id, fresh);
|
|
847
|
+
for (const f of fresh)
|
|
848
|
+
claimed.add(f);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
// MEGA / incoherent — a routing hub spanning many features; distribute its
|
|
853
|
+
// screens (and the navigator file itself) to their own feature folders.
|
|
854
|
+
splitNavs++;
|
|
855
|
+
splitFile(navFile);
|
|
856
|
+
for (const m of memberList)
|
|
857
|
+
splitFile(m);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
// Fallback — screen/route-family files in no navigator (Expo Router pages, or
|
|
861
|
+
// an orphan screen), grouped by feature folder. Root-level files with no
|
|
862
|
+
// determinable feature are logged (no silent cap; no filename fallback here).
|
|
863
|
+
for (const fileId of [...roles.keys()].sort()) {
|
|
864
|
+
if (!FALLBACK_ROLES.has(roles.get(fileId).role))
|
|
865
|
+
continue;
|
|
866
|
+
if (claimed.has(fileId))
|
|
867
|
+
continue;
|
|
868
|
+
const feature = deriveFeature(fileId, false);
|
|
869
|
+
if (!feature) {
|
|
870
|
+
ungroupable.push(fileId);
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
const id = slugify(feature) || 'feature';
|
|
874
|
+
add(id, humanizeName(feature) || id, [fileId]);
|
|
875
|
+
claimed.add(fileId);
|
|
876
|
+
}
|
|
877
|
+
const groups = [...acc.entries()]
|
|
878
|
+
.map(([id, g]) => ({ id, label: g.label, fileIds: [...g.fileIds].sort() }))
|
|
879
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
880
|
+
return { groups, ungroupable, keptNavs, splitNavs };
|
|
881
|
+
}
|
|
882
|
+
function analyzeExpoRouter(repoDir, rootPath) {
|
|
883
|
+
const tree = extractExpoRouterTree({ repoDir, rootPath });
|
|
884
|
+
if (!tree)
|
|
885
|
+
return null;
|
|
886
|
+
// Route roles. parentFileId + routePath carried as metadata (NOT edges).
|
|
887
|
+
const roles = new Map();
|
|
888
|
+
for (const node of tree.routes) {
|
|
889
|
+
roles.set(node.fileId, {
|
|
890
|
+
role: node.role,
|
|
891
|
+
kind: ROLE_KIND[node.role],
|
|
892
|
+
priority: ROLE_PRIORITY[node.role],
|
|
893
|
+
metadata: {
|
|
894
|
+
framework: 'react-native',
|
|
895
|
+
convention: tree.convention,
|
|
896
|
+
routePath: node.routePath,
|
|
897
|
+
...(node.parentFileId !== undefined ? { parentFileId: node.parentFileId } : {}),
|
|
898
|
+
},
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
// Nav edges → `calls`. The step resolves file ids → modules, drops self-edges,
|
|
902
|
+
// and dedupes these AGAINST the React-Navigation edges (overlap collapses).
|
|
903
|
+
const edges = tree.navEdges.map((e) => ({
|
|
904
|
+
source: e.fromFileId,
|
|
905
|
+
target: e.toFileId,
|
|
906
|
+
kind: 'calls',
|
|
907
|
+
metadata: { framework: 'react-native', convention: tree.convention, relation: 'navigation' },
|
|
908
|
+
}));
|
|
909
|
+
return { edges, roles };
|
|
910
|
+
}
|
|
911
|
+
// Mirror of contribute-step.ts beats(): higher priority wins; lexical role
|
|
912
|
+
// tiebreak keeps the pick deterministic regardless of merge/iteration order.
|
|
913
|
+
function roleBeats(incoming, incumbent) {
|
|
914
|
+
const a = incoming.priority ?? 0;
|
|
915
|
+
const b = incumbent.priority ?? 0;
|
|
916
|
+
if (a !== b)
|
|
917
|
+
return a > b;
|
|
918
|
+
return incoming.role < incumbent.role;
|
|
919
|
+
}
|
|
920
|
+
// Fold one role map into another, collapsing a per-file collision (a file that
|
|
921
|
+
// matched both a React-Navigation role AND an Expo route role) by roleBeats and
|
|
922
|
+
// recording it for a single aggregated log — no silent pick (constraint 7).
|
|
923
|
+
function mergeRole(into, fileId, tag, collisions) {
|
|
924
|
+
const cur = into.get(fileId);
|
|
925
|
+
if (cur === undefined) {
|
|
926
|
+
into.set(fileId, tag);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (cur.role !== tag.role) {
|
|
930
|
+
let set = collisions.get(fileId);
|
|
931
|
+
if (!set) {
|
|
932
|
+
set = new Set([cur.role]);
|
|
933
|
+
collisions.set(fileId, set);
|
|
934
|
+
}
|
|
935
|
+
set.add(tag.role);
|
|
936
|
+
}
|
|
937
|
+
if (roleBeats(tag, cur))
|
|
938
|
+
into.set(fileId, tag);
|
|
939
|
+
}
|
|
940
|
+
function analyzeReactNative(ctx) {
|
|
941
|
+
const { repoDir, rootPath, graph } = ctx;
|
|
942
|
+
const project = buildExtractionProject(repoDir);
|
|
943
|
+
addAllSourceFiles(project, repoDir);
|
|
944
|
+
const sourceFiles = project.getSourceFiles();
|
|
945
|
+
const fileById = new Map();
|
|
946
|
+
for (const sf of sourceFiles)
|
|
947
|
+
fileById.set(toId(repoDir, sf.getFilePath()), sf);
|
|
948
|
+
// Pass 1 — global screen registry + navigator/screen role signals.
|
|
949
|
+
const registry = new Map(); // screen name → component file id
|
|
950
|
+
const screenFiles = new Set();
|
|
951
|
+
const navigatorFiles = new Set();
|
|
952
|
+
// navigator file id → the set of screen component files it registers
|
|
953
|
+
// (the navigator-membership grouping signal).
|
|
954
|
+
const navMembership = new Map();
|
|
955
|
+
const importMaps = new Map();
|
|
956
|
+
const idSet = new Set(fileById.keys());
|
|
957
|
+
const aliasResolve = makeAliasResolver(readAliasPatterns(repoDir), idSet);
|
|
958
|
+
const resolveSpecifier = makeSpecifierResolver(idSet, aliasResolve);
|
|
959
|
+
const screenDiag = { dynamicNames: 0, unresolvedComponents: new Set() };
|
|
960
|
+
for (const [fileId, sf] of fileById) {
|
|
961
|
+
if (!inScope(fileId, rootPath))
|
|
962
|
+
continue;
|
|
963
|
+
const imports = buildImportNameMap(sf, repoDir, aliasResolve);
|
|
964
|
+
importMaps.set(fileId, imports);
|
|
965
|
+
const membership = new Set();
|
|
966
|
+
const navigator = collectScreens(sf, imports, fileId, registry, screenFiles, (spec) => resolveSpecifier(fileId, spec), screenDiag, membership);
|
|
967
|
+
if (navigator) {
|
|
968
|
+
navigatorFiles.add(fileId);
|
|
969
|
+
navMembership.set(fileId, membership);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
// Pass 2 — nav edges from navigate()/push() targets + native boundary files.
|
|
973
|
+
const navEdges = [];
|
|
974
|
+
const seenNav = new Set();
|
|
975
|
+
const nativeFiles = new Set();
|
|
976
|
+
let dynamicNavTargets = 0;
|
|
977
|
+
const unresolvedNavTargets = new Set();
|
|
978
|
+
for (const [fileId, sf] of fileById) {
|
|
979
|
+
if (!inScope(fileId, rootPath))
|
|
980
|
+
continue;
|
|
981
|
+
if (usesNative(sf))
|
|
982
|
+
nativeFiles.add(fileId);
|
|
983
|
+
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
984
|
+
const callee = call.getExpression();
|
|
985
|
+
if (callee.getKind() !== SyntaxKind.PropertyAccessExpression)
|
|
986
|
+
continue;
|
|
987
|
+
const method = callee.getName();
|
|
988
|
+
const isLog = NAV_LOG_METHODS.has(method);
|
|
989
|
+
const isSoft = NAV_SOFT_METHODS.has(method);
|
|
990
|
+
if (!isLog && !isSoft)
|
|
991
|
+
continue;
|
|
992
|
+
const arg = call.getArguments()[0];
|
|
993
|
+
if (!arg)
|
|
994
|
+
continue;
|
|
995
|
+
if (arg.getKind() !== SyntaxKind.StringLiteral) {
|
|
996
|
+
// Dynamic target — only count it for the nav-unambiguous methods, so
|
|
997
|
+
// `arr.push(x)` doesn't masquerade as a dropped navigation.
|
|
998
|
+
if (isLog)
|
|
999
|
+
dynamicNavTargets++;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
const target = arg.getLiteralValue();
|
|
1003
|
+
const targetFile = registry.get(target);
|
|
1004
|
+
if (!targetFile) {
|
|
1005
|
+
if (isLog)
|
|
1006
|
+
unresolvedNavTargets.add(target);
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
const key = `${fileId}→${targetFile}`;
|
|
1010
|
+
if (seenNav.has(key))
|
|
1011
|
+
continue;
|
|
1012
|
+
seenNav.add(key);
|
|
1013
|
+
navEdges.push({ source: fileId, target: targetFile, kind: 'calls', metadata: { framework: 'react-native', relation: 'navigation', screen: target } });
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
// Pass 3 — JS↔native bridge edges: importers (from the structural graph) of a
|
|
1017
|
+
// native-boundary file → that file. Native code itself is off-graph (we never
|
|
1018
|
+
// parse ios/android source); the boundary FILE is the JS side of the bridge.
|
|
1019
|
+
const bridgeEdges = [];
|
|
1020
|
+
const seenBridge = new Set();
|
|
1021
|
+
if (nativeFiles.size > 0) {
|
|
1022
|
+
for (const e of graph.edges) {
|
|
1023
|
+
if (e.external)
|
|
1024
|
+
continue;
|
|
1025
|
+
if (!nativeFiles.has(e.to))
|
|
1026
|
+
continue;
|
|
1027
|
+
if (e.from === e.to)
|
|
1028
|
+
continue;
|
|
1029
|
+
const key = `${e.from}→${e.to}`;
|
|
1030
|
+
if (seenBridge.has(key))
|
|
1031
|
+
continue;
|
|
1032
|
+
seenBridge.add(key);
|
|
1033
|
+
bridgeEdges.push({ source: e.from, target: e.to, kind: 'calls', metadata: { framework: 'react-native', relation: 'native-bridge' } });
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
// Roles — combine all signals; highest priority per file wins.
|
|
1037
|
+
const roleByFile = new Map();
|
|
1038
|
+
for (const f of navigatorFiles)
|
|
1039
|
+
addRole(roleByFile, f, 'navigator');
|
|
1040
|
+
for (const f of nativeFiles)
|
|
1041
|
+
addRole(roleByFile, f, 'nativeModule');
|
|
1042
|
+
for (const f of screenFiles)
|
|
1043
|
+
addRole(roleByFile, f, 'screen');
|
|
1044
|
+
for (const [fileId, sf] of fileById) {
|
|
1045
|
+
if (!inScope(fileId, rootPath))
|
|
1046
|
+
continue;
|
|
1047
|
+
if (/^use[A-Z0-9]/.test(baseName(fileId)))
|
|
1048
|
+
addRole(roleByFile, fileId, 'hook');
|
|
1049
|
+
if (hasJsx(sf))
|
|
1050
|
+
addRole(roleByFile, fileId, 'component');
|
|
1051
|
+
}
|
|
1052
|
+
const roles = new Map();
|
|
1053
|
+
for (const [fileId, role] of roleByFile)
|
|
1054
|
+
roles.set(fileId, roleTag(role));
|
|
1055
|
+
// Slice B — Expo Router routes. DETECTION-GATED: no `app/`|`src/app/` route dir
|
|
1056
|
+
// ⇒ analyzeExpoRouter returns null ⇒ no route roles/edges ⇒ the Slice-A output
|
|
1057
|
+
// (and a non-Expo repo) is byte-identical. When present, fold the route roles
|
|
1058
|
+
// into the SAME role map — one file can carry a React-Navigation role (Slice A)
|
|
1059
|
+
// AND an Expo route role (Slice B); collapse by priority, record the collision
|
|
1060
|
+
// — and collect the route nav edges into their own bucket (the step dedupes
|
|
1061
|
+
// them against the React-Navigation edges + resolves both to modules).
|
|
1062
|
+
const routeEdges = [];
|
|
1063
|
+
const roleCollisions = new Map();
|
|
1064
|
+
const expo = analyzeExpoRouter(repoDir, rootPath);
|
|
1065
|
+
if (expo) {
|
|
1066
|
+
for (const [fileId, tag] of expo.roles)
|
|
1067
|
+
mergeRole(roles, fileId, tag, roleCollisions);
|
|
1068
|
+
routeEdges.push(...expo.edges);
|
|
1069
|
+
}
|
|
1070
|
+
// Registration summary (a positive signal for validation) + the "no silent
|
|
1071
|
+
// caps" registration diagnostics: screens whose name/component couldn't be
|
|
1072
|
+
// statically resolved (lazy registration recovered, the rest logged).
|
|
1073
|
+
if (navigatorFiles.size > 0 || registry.size > 0) {
|
|
1074
|
+
console.log(` [react-native] registered ${registry.size} screen(s) across ${navigatorFiles.size} navigator file(s)`);
|
|
1075
|
+
}
|
|
1076
|
+
if (screenDiag.dynamicNames > 0 || screenDiag.unresolvedComponents.size > 0) {
|
|
1077
|
+
const parts = [];
|
|
1078
|
+
if (screenDiag.dynamicNames > 0)
|
|
1079
|
+
parts.push(`${screenDiag.dynamicNames} dynamic screen name(s)`);
|
|
1080
|
+
if (screenDiag.unresolvedComponents.size > 0) {
|
|
1081
|
+
parts.push(`${screenDiag.unresolvedComponents.size} screen(s) with an unresolvable component: ${[...screenDiag.unresolvedComponents].sort().join(', ')}`);
|
|
1082
|
+
}
|
|
1083
|
+
console.log(` [react-native] registration skipped ${parts.join(' · ')} (logged, not silently dropped)`);
|
|
1084
|
+
}
|
|
1085
|
+
// No silent caps ( constraint 7): log dropped/unresolved targets once.
|
|
1086
|
+
if (dynamicNavTargets > 0 || unresolvedNavTargets.size > 0) {
|
|
1087
|
+
const parts = [];
|
|
1088
|
+
if (dynamicNavTargets > 0)
|
|
1089
|
+
parts.push(`${dynamicNavTargets} dynamic target(s)`);
|
|
1090
|
+
if (unresolvedNavTargets.size > 0) {
|
|
1091
|
+
parts.push(`${unresolvedNavTargets.size} unregistered target(s): ${[...unresolvedNavTargets].sort().join(', ')}`);
|
|
1092
|
+
}
|
|
1093
|
+
console.log(` [react-native] skipped ${parts.join(' · ')} (logged, not silently dropped)`);
|
|
1094
|
+
}
|
|
1095
|
+
// No silent caps: a file that matched BOTH a React-Navigation role AND an Expo
|
|
1096
|
+
// route role had them collapsed by priority — report each (roles seen + winner),
|
|
1097
|
+
// never a silent pick. (The contribute-step logs the analogous per-MODULE
|
|
1098
|
+
// collapse after file→module resolution; this is the per-FILE collapse.)
|
|
1099
|
+
if (roleCollisions.size > 0) {
|
|
1100
|
+
const sample = [...roleCollisions.entries()]
|
|
1101
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
1102
|
+
.slice(0, 10)
|
|
1103
|
+
.map(([fileId, set]) => `${fileId} {${[...set].sort().join(', ')}} → kept '${roles.get(fileId)?.role}'`);
|
|
1104
|
+
console.log(` [react-native] ${roleCollisions.size} file(s) matched both a React-Navigation and an Expo Router role (collapsed by priority): ${sample.join(' · ')}` +
|
|
1105
|
+
(roleCollisions.size > sample.length ? ' …' : ''));
|
|
1106
|
+
}
|
|
1107
|
+
// the feature grouping prior (navigator membership + feature-folder
|
|
1108
|
+
// fallback). Built from the final role map (so the fallback sees every role).
|
|
1109
|
+
const { groups, ungroupable, keptNavs, splitNavs } = buildGroupingPrior(navMembership, roles);
|
|
1110
|
+
if (groups.length > 0) {
|
|
1111
|
+
console.log(` [react-native] grouped into ${groups.length} feature subsystem(s) from ${navMembership.size} navigator file(s) ` +
|
|
1112
|
+
`(${keptNavs} cohesive navigator group(s) + ${splitNavs} mega-navigator(s) split by feature folder + feature-folder fallback)`);
|
|
1113
|
+
}
|
|
1114
|
+
if (ungroupable.length > 0) {
|
|
1115
|
+
const sample = [...ungroupable].sort().slice(0, 10);
|
|
1116
|
+
console.log(` [react-native] ${ungroupable.length} screen/route file(s) had no determinable feature folder (not grouped): ${sample.join(', ')}` +
|
|
1117
|
+
(ungroupable.length > sample.length ? ' …' : ''));
|
|
1118
|
+
}
|
|
1119
|
+
// Deterministic ordering — module-id resolution + dedupe downstream rely on a
|
|
1120
|
+
// stable input order; sort by endpoints.
|
|
1121
|
+
const byEndpoints = (a, b) => a.source < b.source ? -1 : a.source > b.source ? 1 : a.target < b.target ? -1 : a.target > b.target ? 1 : 0;
|
|
1122
|
+
navEdges.sort(byEndpoints);
|
|
1123
|
+
bridgeEdges.sort(byEndpoints);
|
|
1124
|
+
routeEdges.sort(byEndpoints);
|
|
1125
|
+
return { navEdges, bridgeEdges, routeEdges, roles, groups };
|
|
1126
|
+
}
|
|
1127
|
+
function getAnalysis(ctx) {
|
|
1128
|
+
let a = ANALYSIS_CACHE.get(ctx);
|
|
1129
|
+
if (!a) {
|
|
1130
|
+
a = analyzeReactNative(ctx);
|
|
1131
|
+
ANALYSIS_CACHE.set(ctx, a);
|
|
1132
|
+
}
|
|
1133
|
+
return a;
|
|
1134
|
+
}
|
|
1135
|
+
// ---------------------------------------------------------------------------
|
|
1136
|
+
// The adapter.
|
|
1137
|
+
export const reactNativeAdapter = {
|
|
1138
|
+
name: 'react-native',
|
|
1139
|
+
async detect(ctx) {
|
|
1140
|
+
// Thin single-root pass: scan the workspace package when given, else the
|
|
1141
|
+
// repo root (per-package fan-out is ). rootPath is the repo-relative
|
|
1142
|
+
// posix path of that base ('' for the repo root).
|
|
1143
|
+
const base = ctx.packageDir ?? ctx.repoDir;
|
|
1144
|
+
const rootPath = ctx.packageDir
|
|
1145
|
+
? relative(ctx.repoDir, ctx.packageDir).split('\\').join('/')
|
|
1146
|
+
: '';
|
|
1147
|
+
return scoreReactNative(gatherReactNativeSignals(base), rootPath);
|
|
1148
|
+
},
|
|
1149
|
+
// the feature grouping prior: navigator membership (primary) +
|
|
1150
|
+
// feature-folder fallback. The contribute-step namespaces each group
|
|
1151
|
+
// `react-native:<id>` and overrides the claimed modules' subsystem, beating the
|
|
1152
|
+
// directory heuristic (screens grouped by feature, not by folder). Deterministic
|
|
1153
|
+
// (name/path-derived) → no classificationsNeeded (the deferred-LLM channel stays
|
|
1154
|
+
// empty, preserving the zero-LLM property).
|
|
1155
|
+
async groupingPrior(ctx) {
|
|
1156
|
+
return { groups: getAnalysis(ctx).groups };
|
|
1157
|
+
},
|
|
1158
|
+
// the navigation graph + JS↔native boundary (file-id space; the
|
|
1159
|
+
// contribution step resolves to modules). kind 'calls' only.
|
|
1160
|
+
async syntheticEdges(ctx) {
|
|
1161
|
+
const a = getAnalysis(ctx);
|
|
1162
|
+
// React-Navigation nav + JS↔native bridge (Slice A) + Expo Router route nav
|
|
1163
|
+
// (Slice B). All `calls`, file-id endpoints; the step dedupes the union.
|
|
1164
|
+
return [...a.navEdges, ...a.bridgeEdges, ...a.routeEdges];
|
|
1165
|
+
},
|
|
1166
|
+
async roleTags(ctx) {
|
|
1167
|
+
return getAnalysis(ctx).roles;
|
|
1168
|
+
},
|
|
1169
|
+
// The hooks READ SOURCE — React-Navigation navigator config + navigate() calls
|
|
1170
|
+
// + NativeModules usage (Slice A), AND the Expo Router `app/` route files +
|
|
1171
|
+
// their <Link href>/router.push() nav targets (Slice B) — so declare the
|
|
1172
|
+
// source paths the diff-driven hosted walk must treat as framework-relevant.
|
|
1173
|
+
// Every Expo route file is a source file (.ts/.tsx/.js/.jsx, incl. `+api.ts`),
|
|
1174
|
+
// so the source-extension test already covers them. Never-store-source holds:
|
|
1175
|
+
// read server-side, persist only derived edges/roles.
|
|
1176
|
+
scansSourcePath(path) {
|
|
1177
|
+
const ext = path.split('.').pop();
|
|
1178
|
+
return ext !== undefined && SOURCE_EXT_SET.has(ext);
|
|
1179
|
+
},
|
|
1180
|
+
};
|
|
1181
|
+
//# sourceMappingURL=react-native.js.map
|