@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,739 @@
|
|
|
1
|
+
// Stage C — monorepo workspace/package boundary detection (decides
|
|
2
|
+
// manifest-first, Louvain fallback).
|
|
3
|
+
//
|
|
4
|
+
// Declared package boundaries are the ONE structural signal in a repo that is
|
|
5
|
+
// authored, not inferred: a workspace glob in package.json / pnpm-workspace.yaml
|
|
6
|
+
// / lerna.json is the developer SAYING "this is a unit". Stage C makes those
|
|
7
|
+
// boundaries first-class in clustering — Louvain runs only WITHIN a package
|
|
8
|
+
// (small graphs, stable partitions), and the boundary between packages is
|
|
9
|
+
// deterministic from manifests rather than statistical. This module is the
|
|
10
|
+
// detection half: fs-reading, pure-output, NEVER throws (a malformed manifest
|
|
11
|
+
// must degrade to "fewer boundaries", never fail the ingest — worst case is a
|
|
12
|
+
// single-package layout, i.e. exactly the pre-Stage-C behavior).
|
|
13
|
+
//
|
|
14
|
+
// Detection rules (manifest-first, ):
|
|
15
|
+
// DECLARED — root package.json `workspaces` (array or `{packages}` object
|
|
16
|
+
// form), pnpm-workspace.yaml `packages:`, lerna.json `packages`.
|
|
17
|
+
// Globs are expanded against the package.json dirs found by the
|
|
18
|
+
// fs walk with a minimal matcher (literal, `dir/*`, `dir/**`);
|
|
19
|
+
// `!`-negations are IGNORED (rare in the wild, and ignoring a
|
|
20
|
+
// negation only yields an extra boundary — the override map's
|
|
21
|
+
// `drop` rules handle any noise it causes).
|
|
22
|
+
// INFERRED — any non-root package.json with a `"name"` field counts as a
|
|
23
|
+
// package even when not declared. This covers undeclared
|
|
24
|
+
// multi-package repos (this very repo's `worker/` is exactly
|
|
25
|
+
// that). Tradeoff: a test fixture containing a named
|
|
26
|
+
// package.json creates a noise boundary — accepted, because the
|
|
27
|
+
// override map's `drop` handles it and missing a real boundary
|
|
28
|
+
// is worse (it lets Louvain blend two packages).
|
|
29
|
+
//
|
|
30
|
+
// Membership is by LONGEST matching package root (a file under
|
|
31
|
+
// `worker/container/` belongs to that package, not `worker/`), with the repo
|
|
32
|
+
// root ('' scope) as the universal fallback.
|
|
33
|
+
//
|
|
34
|
+
// (monorepo-aware subsystem partition) extends Stage C with three
|
|
35
|
+
// PURE, DETERMINISTIC, ZERO-LLM signals computed off the same manifests:
|
|
36
|
+
// - turbo.json / nx.json recognition. Turborepo reuses the package manager's
|
|
37
|
+
// workspaces, so turbo.json adds no globs (named packages are already found
|
|
38
|
+
// INFERRED) — it's recognized only as a layout-trigger manifest. Nx declares
|
|
39
|
+
// its package dirs via `workspaceLayout` (appsDir/libsDir, default apps/libs),
|
|
40
|
+
// so nx.json contributes `<appsDir>/*` + `<libsDir>/*` DECLARED globs.
|
|
41
|
+
// - ROLE per package (app / lib / tooling) — descriptive metadata threaded onto
|
|
42
|
+
// the package's subsystem so the canvas can label the top-level box. NOT a
|
|
43
|
+
// Module kind (the kind enum stays locked).
|
|
44
|
+
// - declaredDeps: the internal package→package dependency graph, from each
|
|
45
|
+
// package.json's deps that NAME a workspace sibling (a workspace dep shadows
|
|
46
|
+
// the registry, so a name match IS the dep). The cluster layer turns these
|
|
47
|
+
// into cross-package `calls` edges (filling gaps the import graph alone
|
|
48
|
+
// missed). A tsconfig `references` / path-alias dep source is a possible
|
|
49
|
+
// follow-up — deliberately OUT of this slice.
|
|
50
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
51
|
+
import { join } from 'node:path';
|
|
52
|
+
import { parse as parseYaml } from 'yaml';
|
|
53
|
+
import { parse as parseToml } from 'smol-toml';
|
|
54
|
+
import { compileMatchers, slugify } from './overrides.js';
|
|
55
|
+
import { EXCLUDE_DIRS, PYTHON_EXCLUDE_DIRS } from '../graph/file-graph.js';
|
|
56
|
+
import { normalizePyName, requirementName } from '../graph/python-manifest.js';
|
|
57
|
+
// The manifests that can DEFINE workspace boundaries. A diff touching any of
|
|
58
|
+
// these is the container's trigger to recompute the layout (and invalidateAll()
|
|
59
|
+
// the partition cache) — everything else leaves the layout memoizable across
|
|
60
|
+
// checkpoints. pnpm only reads `pnpm-workspace.yaml` (no `.yml` variant).
|
|
61
|
+
// adds turbo.json / nx.json (monorepo layout signals).
|
|
62
|
+
const WORKSPACE_MANIFEST_BASENAMES = new Set([
|
|
63
|
+
'package.json',
|
|
64
|
+
'pnpm-workspace.yaml',
|
|
65
|
+
'lerna.json',
|
|
66
|
+
'turbo.json',
|
|
67
|
+
'nx.json',
|
|
68
|
+
// Python package/workspace manifests. A diff touching any of these
|
|
69
|
+
// moves Python package boundaries → recompute the layout, exactly like the JS
|
|
70
|
+
// manifests above.
|
|
71
|
+
'pyproject.toml',
|
|
72
|
+
'setup.py',
|
|
73
|
+
'setup.cfg',
|
|
74
|
+
]);
|
|
75
|
+
/** Does a repo-relative path name a workspace-layout-defining manifest? */
|
|
76
|
+
export function isWorkspaceManifestPath(path) {
|
|
77
|
+
const base = path.split('/').pop() ?? path;
|
|
78
|
+
return WORKSPACE_MANIFEST_BASENAMES.has(base);
|
|
79
|
+
}
|
|
80
|
+
// Walk depth cap — pathological repos (vendored trees, generated dirs that
|
|
81
|
+
// dodge EXCLUDE_DIRS) must not turn detection into a full-disk crawl. Real
|
|
82
|
+
// workspace packages live 1-3 levels deep; 8 is generous.
|
|
83
|
+
const MAX_WALK_DEPTH = 8;
|
|
84
|
+
const EXCLUDE_SET = new Set(EXCLUDE_DIRS);
|
|
85
|
+
// `.js` entry points (typically a build artifact path like `dist/index.js`)
|
|
86
|
+
// almost never exist in the SOURCE graph the extractor builds — the TS sibling
|
|
87
|
+
// next to them often does. Map each transpiled extension to the source
|
|
88
|
+
// extensions it could have come from.
|
|
89
|
+
const TS_VARIANTS = [
|
|
90
|
+
['.js', ['.ts', '.tsx']],
|
|
91
|
+
['.mjs', ['.mts']],
|
|
92
|
+
['.cjs', ['.cts']],
|
|
93
|
+
];
|
|
94
|
+
/**
|
|
95
|
+
* Detect the workspace/package layout of a checked-out repo. fs-reading;
|
|
96
|
+
* NEVER throws — any unreadable/malformed manifest source is skipped, and the
|
|
97
|
+
* worst case is a single-package (root-only) layout, which downstream treats
|
|
98
|
+
* as "no Stage-C behavior" (status quo ante).
|
|
99
|
+
*/
|
|
100
|
+
export function detectWorkspaceLayout(rootDir) {
|
|
101
|
+
// 1. Candidate package dirs: every non-root dir holding a package.json,
|
|
102
|
+
// pruning EXCLUDE_DIRS + dot-directories — MUST mirror file-graph.ts's
|
|
103
|
+
// isSourceFilePath policy, or a package boundary could claim files the
|
|
104
|
+
// graph will never contain (or vice versa). Sorted for determinism.
|
|
105
|
+
const candidates = findPackageJsonDirs(rootDir);
|
|
106
|
+
// 2. Declared workspace globs, from package.json/pnpm/lerna + nx.json's
|
|
107
|
+
// workspaceLayout. nxLayout also carries the apps/libs dir
|
|
108
|
+
// convention used for role classification below.
|
|
109
|
+
const rootManifest = readJsonObject(join(rootDir, 'package.json'));
|
|
110
|
+
const nxLayout = readNxLayout(rootDir);
|
|
111
|
+
const matchesDeclared = compileMatchers([
|
|
112
|
+
...collectDeclaredGlobs(rootDir, rootManifest, nxLayout),
|
|
113
|
+
// uv workspace member globs (`[tool.uv.workspace].members`) are the
|
|
114
|
+
// Python analogue of package.json `workspaces`.
|
|
115
|
+
...collectPythonDeclaredGlobs(rootDir),
|
|
116
|
+
]);
|
|
117
|
+
// 3. Materialize packages. The root scope is ALWAYS present (and always
|
|
118
|
+
// declared — the repo root is a boundary by definition). Slugs dedup
|
|
119
|
+
// through a used-set in root-sorted order so they're stable across runs.
|
|
120
|
+
const usedSlugs = new Set();
|
|
121
|
+
const reserveSlug = (base) => {
|
|
122
|
+
let slug = base;
|
|
123
|
+
let i = 2;
|
|
124
|
+
while (usedSlugs.has(slug))
|
|
125
|
+
slug = `${base}-${i++}`;
|
|
126
|
+
usedSlugs.add(slug);
|
|
127
|
+
return slug;
|
|
128
|
+
};
|
|
129
|
+
const packages = [];
|
|
130
|
+
// keep each package's parsed package.json for the declaredDeps
|
|
131
|
+
// second pass (it needs the full name→package map, so it can't run inline).
|
|
132
|
+
const manifestByRoot = new Map();
|
|
133
|
+
const rootName = nameOf(rootManifest);
|
|
134
|
+
manifestByRoot.set('', rootManifest);
|
|
135
|
+
packages.push({
|
|
136
|
+
root: '',
|
|
137
|
+
name: rootName,
|
|
138
|
+
slug: reserveSlug(slugFor(rootName, 'root')),
|
|
139
|
+
entryFileIds: entryCandidates('', rootManifest),
|
|
140
|
+
declared: true,
|
|
141
|
+
role: roleOf('', rootManifest, nxLayout),
|
|
142
|
+
declaredDeps: [], // root scope groups by directory, not as a package — no edges
|
|
143
|
+
});
|
|
144
|
+
for (const dir of candidates) {
|
|
145
|
+
const manifest = readJsonObject(join(rootDir, dir, 'package.json'));
|
|
146
|
+
const name = nameOf(manifest);
|
|
147
|
+
const declared = matchesDeclared(dir);
|
|
148
|
+
// A boundary needs a reason to exist: either someone DECLARED it, or the
|
|
149
|
+
// package names itself (inferred). An unnamed, undeclared package.json
|
|
150
|
+
// (e.g. `{"type":"module"}` markers, fixtures) is not a boundary.
|
|
151
|
+
if (!declared && name === null)
|
|
152
|
+
continue;
|
|
153
|
+
manifestByRoot.set(dir, manifest);
|
|
154
|
+
packages.push({
|
|
155
|
+
root: dir,
|
|
156
|
+
name,
|
|
157
|
+
slug: reserveSlug(slugFor(name, dir.split('/').pop() ?? dir)),
|
|
158
|
+
entryFileIds: entryCandidates(dir, manifest),
|
|
159
|
+
declared,
|
|
160
|
+
role: roleOf(dir, manifest, nxLayout),
|
|
161
|
+
declaredDeps: [], // filled by the second pass once nameToPackage exists
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// 3b. Python packages. Every non-root dir with a pyproject.toml /
|
|
165
|
+
// setup.py / setup.cfg that either NAMES a package (PEP 621 `[project].name`
|
|
166
|
+
// or `[tool.poetry].name`) or is a DECLARED uv workspace member. Merged into
|
|
167
|
+
// the SAME packages list so packageOf partitions Python + polyglot repos
|
|
168
|
+
// uniformly. A dir that is ALREADY a JS package (a polyglot single package
|
|
169
|
+
// with both manifests) keeps its one boundary (skip — first wins).
|
|
170
|
+
const seenRoots = new Set(packages.map((p) => p.root));
|
|
171
|
+
const pyManifestByRoot = new Map();
|
|
172
|
+
for (const dir of findPythonPackageDirs(rootDir)) {
|
|
173
|
+
if (seenRoots.has(dir))
|
|
174
|
+
continue;
|
|
175
|
+
const py = readPyproject(join(rootDir, dir));
|
|
176
|
+
const name = py ? pyPackageName(py) : null;
|
|
177
|
+
const declared = matchesDeclared(dir);
|
|
178
|
+
if (!declared && name === null)
|
|
179
|
+
continue;
|
|
180
|
+
seenRoots.add(dir);
|
|
181
|
+
if (py)
|
|
182
|
+
pyManifestByRoot.set(dir, py);
|
|
183
|
+
packages.push({
|
|
184
|
+
root: dir,
|
|
185
|
+
name,
|
|
186
|
+
slug: reserveSlug(slugFor(name, dir.split('/').pop() ?? dir)),
|
|
187
|
+
entryFileIds: pythonEntryCandidates(dir, name),
|
|
188
|
+
declared,
|
|
189
|
+
role: pythonRoleOf(dir, py),
|
|
190
|
+
declaredDeps: [], // filled by the second pass once nameToPackage exists
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// 4. Longest-prefix membership. Walking the file's own dir chain upward
|
|
194
|
+
// visits prefixes longest-first, so the first hit IS the longest match;
|
|
195
|
+
// nested packages (worker/container under worker) resolve correctly.
|
|
196
|
+
const byRoot = new Map(packages.map((p) => [p.root, p]));
|
|
197
|
+
const rootScope = packages[0];
|
|
198
|
+
const packageOf = (fileId) => {
|
|
199
|
+
let prefix = fileId;
|
|
200
|
+
for (;;) {
|
|
201
|
+
const cut = prefix.lastIndexOf('/');
|
|
202
|
+
if (cut < 0)
|
|
203
|
+
return rootScope;
|
|
204
|
+
prefix = prefix.slice(0, cut);
|
|
205
|
+
const hit = byRoot.get(prefix);
|
|
206
|
+
if (hit)
|
|
207
|
+
return hit;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const nameToPackage = new Map();
|
|
211
|
+
for (const p of packages) {
|
|
212
|
+
if (p.root === '' || p.name === null)
|
|
213
|
+
continue;
|
|
214
|
+
if (!nameToPackage.has(p.name))
|
|
215
|
+
nameToPackage.set(p.name, p);
|
|
216
|
+
}
|
|
217
|
+
// 5. cross-package dependency graph (second pass — needs the full
|
|
218
|
+
// nameToPackage map). Each non-root package's declaredDeps are the sibling
|
|
219
|
+
// roots it depends on, from its package.json deps (by sibling name).
|
|
220
|
+
const knownRoots = new Set(packages.map((p) => p.root));
|
|
221
|
+
// Roots that came from a PYTHON manifest — used to scope Python name-matching to
|
|
222
|
+
// Python siblings, so a same-named npm package can't shadow a Python dep.
|
|
223
|
+
const pyRoots = new Set(pyManifestByRoot.keys());
|
|
224
|
+
for (const pkg of packages) {
|
|
225
|
+
if (pkg.root === '')
|
|
226
|
+
continue;
|
|
227
|
+
const js = computeDeclaredDeps(pkg.root, manifestByRoot.get(pkg.root) ?? null, nameToPackage);
|
|
228
|
+
// a Python package's sibling deps — a dep NAMING a sibling (PEP 621 /
|
|
229
|
+
// poetry, normalized) or a poetry `{path=…}` dep resolving to a sibling root.
|
|
230
|
+
const py = computePythonDeclaredDeps(pkg.root, pyManifestByRoot.get(pkg.root) ?? null, nameToPackage, knownRoots, pyRoots);
|
|
231
|
+
pkg.declaredDeps = [...new Set([...js, ...py])].sort();
|
|
232
|
+
}
|
|
233
|
+
// 6. Observability (no silent caps): for a real multi-package layout, log the
|
|
234
|
+
// packages found, their roles, and any cross-package deps — so a monorepo's
|
|
235
|
+
// partition is never silently mis-derived. Every package gets a role
|
|
236
|
+
// deterministically (lib is the fallback), so there is no "unclassified"
|
|
237
|
+
// bucket; the full breakdown is logged instead.
|
|
238
|
+
if (packages.length > 1) {
|
|
239
|
+
const summary = packages
|
|
240
|
+
.filter((p) => p.root !== '')
|
|
241
|
+
.map((p) => `${p.root}=${p.role}${p.declaredDeps.length ? ` →[${p.declaredDeps.join(', ')}]` : ''}`)
|
|
242
|
+
.join(' ');
|
|
243
|
+
console.log(` [workspaces] ${packages.length - 1} package(s): ${summary}`);
|
|
244
|
+
}
|
|
245
|
+
return { packages, packageOf, nameToPackage };
|
|
246
|
+
}
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// fs walk
|
|
249
|
+
/** Sorted repo-relative posix dirs (non-root) that contain a package.json. */
|
|
250
|
+
function findPackageJsonDirs(rootDir) {
|
|
251
|
+
const out = [];
|
|
252
|
+
const walk = (rel, depth) => {
|
|
253
|
+
if (depth > MAX_WALK_DEPTH)
|
|
254
|
+
return;
|
|
255
|
+
let entries;
|
|
256
|
+
try {
|
|
257
|
+
entries = readdirSync(rel === '' ? rootDir : join(rootDir, rel), { withFileTypes: true });
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return; // unreadable dir → skip subtree, never throw
|
|
261
|
+
}
|
|
262
|
+
// Sort for cross-platform determinism (readdir order is fs-dependent).
|
|
263
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
264
|
+
for (const ent of entries) {
|
|
265
|
+
if (ent.isDirectory()) {
|
|
266
|
+
// Mirror isSourceFilePath: dot-dirs + EXCLUDE_DIRS segments are never
|
|
267
|
+
// part of the graph, so a package.json inside them is not a boundary.
|
|
268
|
+
// (Symlinked dirs fail isDirectory() and are skipped — no cycles.)
|
|
269
|
+
if (ent.name.startsWith('.') || EXCLUDE_SET.has(ent.name))
|
|
270
|
+
continue;
|
|
271
|
+
walk(rel === '' ? ent.name : `${rel}/${ent.name}`, depth + 1);
|
|
272
|
+
}
|
|
273
|
+
else if (rel !== '' && ent.isFile() && ent.name === 'package.json') {
|
|
274
|
+
out.push(rel);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
walk('', 0);
|
|
279
|
+
return out.sort();
|
|
280
|
+
}
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Declared-glob collection
|
|
283
|
+
/**
|
|
284
|
+
* Normalized declared workspace globs from root package.json `workspaces`
|
|
285
|
+
* (array or `{packages}`), pnpm-workspace.yaml `packages:`, lerna.json
|
|
286
|
+
* `packages`, and nx.json `workspaceLayout` (apps/libs dirs). Each
|
|
287
|
+
* parse failure skips that SOURCE only.
|
|
288
|
+
*/
|
|
289
|
+
function collectDeclaredGlobs(rootDir, rootManifest, nx) {
|
|
290
|
+
const globs = [];
|
|
291
|
+
const pushAll = (v) => {
|
|
292
|
+
if (!Array.isArray(v))
|
|
293
|
+
return;
|
|
294
|
+
for (const raw of v) {
|
|
295
|
+
if (typeof raw !== 'string')
|
|
296
|
+
continue;
|
|
297
|
+
let g = raw.trim().replace(/\\/g, '/');
|
|
298
|
+
if (g.startsWith('!'))
|
|
299
|
+
continue; // negations ignored (see header)
|
|
300
|
+
if (g.startsWith('./'))
|
|
301
|
+
g = g.slice(2);
|
|
302
|
+
g = g.replace(/\/+$/, '');
|
|
303
|
+
if (g)
|
|
304
|
+
globs.push(g);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
// package.json: `"workspaces": [...]` or the yarn object form
|
|
308
|
+
// `"workspaces": { "packages": [...] }`.
|
|
309
|
+
const ws = rootManifest?.workspaces;
|
|
310
|
+
if (Array.isArray(ws))
|
|
311
|
+
pushAll(ws);
|
|
312
|
+
else if (ws && typeof ws === 'object')
|
|
313
|
+
pushAll(ws.packages);
|
|
314
|
+
// pnpm-workspace.yaml — parsed with the `yaml` package (already a root dep,
|
|
315
|
+
// same parser the infra adapters use).
|
|
316
|
+
try {
|
|
317
|
+
const doc = parseYaml(readFileSync(join(rootDir, 'pnpm-workspace.yaml'), 'utf8'));
|
|
318
|
+
if (doc && typeof doc === 'object')
|
|
319
|
+
pushAll(doc.packages);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// absent or malformed → skip this source
|
|
323
|
+
}
|
|
324
|
+
// lerna.json
|
|
325
|
+
const lerna = readJsonObject(join(rootDir, 'lerna.json'));
|
|
326
|
+
if (lerna)
|
|
327
|
+
pushAll(lerna.packages);
|
|
328
|
+
// nx.json workspaceLayout — Nx declares its package homes via
|
|
329
|
+
// appsDir/libsDir (default apps/libs), not a glob list, so synthesize the
|
|
330
|
+
// conventional `<appsDir>/*` + `<libsDir>/*` boundaries. Turbo adds none
|
|
331
|
+
// (Turborepo reuses the package-manager workspaces handled above; named
|
|
332
|
+
// packages are still found via the INFERRED path).
|
|
333
|
+
if (nx.present) {
|
|
334
|
+
if (nx.appsDir)
|
|
335
|
+
globs.push(`${nx.appsDir}/*`);
|
|
336
|
+
if (nx.libsDir)
|
|
337
|
+
globs.push(`${nx.libsDir}/*`);
|
|
338
|
+
}
|
|
339
|
+
return globs;
|
|
340
|
+
}
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// Entry-point candidates
|
|
343
|
+
/**
|
|
344
|
+
* Candidate entry files for a package, best-first. Drawn from package.json
|
|
345
|
+
* `main` / `module` / `exports` ('.' entry — string, `{".": …}` subpath form,
|
|
346
|
+
* or top-level `{import|default}` conditions form), normalized repo-relative;
|
|
347
|
+
* each transpiled (.js/.mjs/.cjs) candidate also yields its TS sibling(s);
|
|
348
|
+
* the conventional index locations are always appended as fallbacks.
|
|
349
|
+
* Existence is NOT checked — the cluster layer resolves the first candidate
|
|
350
|
+
* present in its file→module map (fail-soft: none present ⇒ no remap).
|
|
351
|
+
*/
|
|
352
|
+
function entryCandidates(pkgRoot, manifest) {
|
|
353
|
+
const out = [];
|
|
354
|
+
const push = (p) => {
|
|
355
|
+
if (p && !out.includes(p))
|
|
356
|
+
out.push(p);
|
|
357
|
+
};
|
|
358
|
+
const pushWithVariants = (raw) => {
|
|
359
|
+
if (typeof raw !== 'string' || !raw.trim())
|
|
360
|
+
return;
|
|
361
|
+
const norm = normalizeEntry(pkgRoot, raw);
|
|
362
|
+
if (!norm)
|
|
363
|
+
return;
|
|
364
|
+
push(norm);
|
|
365
|
+
for (const [ext, variants] of TS_VARIANTS) {
|
|
366
|
+
if (!norm.endsWith(ext))
|
|
367
|
+
continue;
|
|
368
|
+
for (const v of variants)
|
|
369
|
+
push(norm.slice(0, -ext.length) + v);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
pushWithVariants(manifest?.main);
|
|
373
|
+
pushWithVariants(manifest?.module);
|
|
374
|
+
pushWithVariants(dotExport(manifest?.exports));
|
|
375
|
+
// Conventional fallbacks — these resolve install-free packages with no
|
|
376
|
+
// explicit entry fields (and dist-pointing manifests whose source lives at
|
|
377
|
+
// the conventional spots).
|
|
378
|
+
for (const f of ['index.ts', 'index.tsx', 'src/index.ts', 'src/index.tsx']) {
|
|
379
|
+
push(pkgRoot ? `${pkgRoot}/${f}` : f);
|
|
380
|
+
}
|
|
381
|
+
return out;
|
|
382
|
+
}
|
|
383
|
+
/** Extract the '.' export target as a string, or undefined. */
|
|
384
|
+
function dotExport(exp) {
|
|
385
|
+
let dot = exp;
|
|
386
|
+
if (dot && typeof dot === 'object' && !Array.isArray(dot)) {
|
|
387
|
+
const e = dot;
|
|
388
|
+
// Subpath-map form has a '.' key; bare-conditions form doesn't (the object
|
|
389
|
+
// itself is the '.' entry's conditions).
|
|
390
|
+
dot = '.' in e ? e['.'] : e;
|
|
391
|
+
}
|
|
392
|
+
if (dot && typeof dot === 'object' && !Array.isArray(dot)) {
|
|
393
|
+
const d = dot;
|
|
394
|
+
dot = typeof d.import === 'string' ? d.import : d.default;
|
|
395
|
+
}
|
|
396
|
+
return typeof dot === 'string' ? dot : undefined;
|
|
397
|
+
}
|
|
398
|
+
/** `./dist/index.js` (package-relative) → `worker/dist/index.js` (repo-relative). */
|
|
399
|
+
function normalizeEntry(pkgRoot, raw) {
|
|
400
|
+
let p = raw.trim().replace(/\\/g, '/');
|
|
401
|
+
if (p.startsWith('./'))
|
|
402
|
+
p = p.slice(2);
|
|
403
|
+
// Refuse anything that escapes the package (or the repo): an entry above the
|
|
404
|
+
// package root can't be a meaningful module-id anchor.
|
|
405
|
+
if (!p || p.startsWith('/') || p === '..' || p.startsWith('../') || p.includes('/../'))
|
|
406
|
+
return null;
|
|
407
|
+
return pkgRoot ? `${pkgRoot}/${p}` : p;
|
|
408
|
+
}
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
// Small helpers
|
|
411
|
+
/** Read + parse a JSON file into an object; null on ANY failure (never throws). */
|
|
412
|
+
function readJsonObject(absPath) {
|
|
413
|
+
try {
|
|
414
|
+
const parsed = JSON.parse(readFileSync(absPath, 'utf8'));
|
|
415
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
416
|
+
? parsed
|
|
417
|
+
: null;
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function nameOf(manifest) {
|
|
424
|
+
const n = manifest?.name;
|
|
425
|
+
return typeof n === 'string' && n.trim() ? n.trim() : null;
|
|
426
|
+
}
|
|
427
|
+
/** Slug from a package name's last segment (`@scope/pkg` → `pkg`), else the dir basename. */
|
|
428
|
+
function slugFor(name, fallback) {
|
|
429
|
+
const fromName = name ? slugify(name.split('/').pop() ?? name) : '';
|
|
430
|
+
return fromName || slugify(fallback) || 'pkg';
|
|
431
|
+
}
|
|
432
|
+
function readNxLayout(rootDir) {
|
|
433
|
+
const nx = readJsonObject(join(rootDir, 'nx.json'));
|
|
434
|
+
if (!nx)
|
|
435
|
+
return { appsDir: 'apps', libsDir: 'libs', present: false };
|
|
436
|
+
let appsDir = 'apps';
|
|
437
|
+
let libsDir = 'libs';
|
|
438
|
+
const wl = nx.workspaceLayout;
|
|
439
|
+
if (wl && typeof wl === 'object' && !Array.isArray(wl)) {
|
|
440
|
+
const o = wl;
|
|
441
|
+
if (typeof o.appsDir === 'string' && o.appsDir.trim())
|
|
442
|
+
appsDir = cleanDir(o.appsDir);
|
|
443
|
+
if (typeof o.libsDir === 'string' && o.libsDir.trim())
|
|
444
|
+
libsDir = cleanDir(o.libsDir);
|
|
445
|
+
}
|
|
446
|
+
return { appsDir, libsDir, present: true };
|
|
447
|
+
}
|
|
448
|
+
/** Normalize a config dir value to a clean repo-relative posix segment chain. */
|
|
449
|
+
function cleanDir(s) {
|
|
450
|
+
return s.trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
451
|
+
}
|
|
452
|
+
// Conventional tooling DIR names + package-name patterns. A package that is
|
|
453
|
+
// build/lint/config tooling is labeled `tooling` (most specific signal — checked
|
|
454
|
+
// before app/lib so a config package with a build script isn't mislabeled app).
|
|
455
|
+
const TOOLING_DIRS = new Set(['tools', 'tooling', 'config', 'configs']);
|
|
456
|
+
function isToolingName(name) {
|
|
457
|
+
const last = (name.split('/').pop() ?? name).toLowerCase();
|
|
458
|
+
return (last.includes('eslint-config') ||
|
|
459
|
+
last.includes('eslint-plugin') ||
|
|
460
|
+
last.includes('prettier-config') ||
|
|
461
|
+
last.includes('stylelint-config') ||
|
|
462
|
+
last === 'tsconfig' ||
|
|
463
|
+
last.startsWith('tsconfig-') ||
|
|
464
|
+
last.endsWith('-tsconfig') ||
|
|
465
|
+
last === 'config' ||
|
|
466
|
+
last.endsWith('-config') ||
|
|
467
|
+
last.endsWith('-preset'));
|
|
468
|
+
}
|
|
469
|
+
function hasAnyScript(manifest, names) {
|
|
470
|
+
const scripts = manifest?.scripts;
|
|
471
|
+
if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts))
|
|
472
|
+
return false;
|
|
473
|
+
const s = scripts;
|
|
474
|
+
return names.some((n) => typeof s[n] === 'string' && s[n].trim() !== '');
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Classify a package app / lib / tooling. Deterministic, pure:
|
|
478
|
+
* 1. tooling — conventional tooling dir (tools/config) or a config/eslint/
|
|
479
|
+
* tsconfig package name (most specific, checked first).
|
|
480
|
+
* 2. directory convention dominates the script heuristic: `<appsDir>/*` → app;
|
|
481
|
+
* `<libsDir>/*` (or the generic `packages`/`libs`) → lib. This keeps a
|
|
482
|
+
* `packages/ui` library a lib even when it has a build script.
|
|
483
|
+
* 3. otherwise (a non-conventional location) a runnable program — a
|
|
484
|
+
* start/dev/build/serve script, or a `bin` — is an app; everything else lib.
|
|
485
|
+
*/
|
|
486
|
+
function roleOf(pkgRoot, manifest, nx) {
|
|
487
|
+
const seg0 = pkgRoot.split('/')[0]; // '' for the root scope
|
|
488
|
+
const name = nameOf(manifest);
|
|
489
|
+
if (seg0 !== '' && TOOLING_DIRS.has(seg0))
|
|
490
|
+
return 'tooling';
|
|
491
|
+
if (name && isToolingName(name))
|
|
492
|
+
return 'tooling';
|
|
493
|
+
if (seg0 !== '' && seg0 === nx.appsDir)
|
|
494
|
+
return 'app';
|
|
495
|
+
if (seg0 === nx.libsDir || seg0 === 'packages' || seg0 === 'libs')
|
|
496
|
+
return 'lib';
|
|
497
|
+
if (hasAnyScript(manifest, ['start', 'dev', 'build', 'serve']) || manifest?.bin != null) {
|
|
498
|
+
return 'app';
|
|
499
|
+
}
|
|
500
|
+
return 'lib';
|
|
501
|
+
}
|
|
502
|
+
const DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
503
|
+
/**
|
|
504
|
+
* Sibling roots a package depends on — sorted, deduped, self/root
|
|
505
|
+
* excluded. Source: this package's package.json deps (all four dependency
|
|
506
|
+
* fields) whose KEY names a workspace sibling (a workspace dep shadows the
|
|
507
|
+
* registry, so a name match IS the dep). A tsconfig `references` / path-alias
|
|
508
|
+
* dep source is a possible follow-up, deliberately OUT of this slice.
|
|
509
|
+
*/
|
|
510
|
+
function computeDeclaredDeps(pkgRoot, manifest, nameToPackage) {
|
|
511
|
+
const deps = new Set();
|
|
512
|
+
for (const field of DEP_FIELDS) {
|
|
513
|
+
const d = manifest?.[field];
|
|
514
|
+
if (!d || typeof d !== 'object' || Array.isArray(d))
|
|
515
|
+
continue;
|
|
516
|
+
for (const depName of Object.keys(d)) {
|
|
517
|
+
const sib = nameToPackage.get(depName);
|
|
518
|
+
if (sib && sib.root !== '' && sib.root !== pkgRoot)
|
|
519
|
+
deps.add(sib.root);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return [...deps].sort();
|
|
523
|
+
}
|
|
524
|
+
// ---------------------------------------------------------------------------
|
|
525
|
+
// Python package/workspace detection (the Python analogue of the JS
|
|
526
|
+
// machinery above). All pure + fs-reading + NEVER-throws (a malformed manifest
|
|
527
|
+
// degrades to fewer boundaries). Merged into the SAME WorkspacePackage list, so
|
|
528
|
+
// the cluster layer partitions Python + polyglot repos with no change.
|
|
529
|
+
const PY_MANIFEST_FILES = new Set(['pyproject.toml', 'setup.py', 'setup.cfg']);
|
|
530
|
+
// A Python package dir must skip BOTH the JS excludes and the Python ones (venvs,
|
|
531
|
+
// caches) — a vendored `.venv/**/pyproject.toml` is never a first-party boundary.
|
|
532
|
+
const PY_EXCLUDE_SET = new Set([...EXCLUDE_DIRS, ...PYTHON_EXCLUDE_DIRS]);
|
|
533
|
+
/** Sorted non-root dirs containing a pyproject.toml / setup.py / setup.cfg. */
|
|
534
|
+
function findPythonPackageDirs(rootDir) {
|
|
535
|
+
const out = new Set();
|
|
536
|
+
const walk = (rel, depth) => {
|
|
537
|
+
if (depth > MAX_WALK_DEPTH)
|
|
538
|
+
return;
|
|
539
|
+
let entries;
|
|
540
|
+
try {
|
|
541
|
+
entries = readdirSync(rel === '' ? rootDir : join(rootDir, rel), { withFileTypes: true });
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
547
|
+
for (const ent of entries) {
|
|
548
|
+
if (ent.isDirectory()) {
|
|
549
|
+
if (ent.name.startsWith('.') || PY_EXCLUDE_SET.has(ent.name))
|
|
550
|
+
continue;
|
|
551
|
+
walk(rel === '' ? ent.name : `${rel}/${ent.name}`, depth + 1);
|
|
552
|
+
}
|
|
553
|
+
else if (rel !== '' && ent.isFile() && PY_MANIFEST_FILES.has(ent.name)) {
|
|
554
|
+
out.add(rel);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
walk('', 0);
|
|
559
|
+
return [...out].sort();
|
|
560
|
+
}
|
|
561
|
+
/** Parse a dir's pyproject.toml into an object; null on absence/any failure. */
|
|
562
|
+
function readPyproject(absDir) {
|
|
563
|
+
try {
|
|
564
|
+
const p = join(absDir, 'pyproject.toml');
|
|
565
|
+
if (!existsSync(p))
|
|
566
|
+
return null;
|
|
567
|
+
const parsed = parseToml(readFileSync(p, 'utf8'));
|
|
568
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
569
|
+
? parsed
|
|
570
|
+
: null;
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function asObject(v) {
|
|
577
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? v : undefined;
|
|
578
|
+
}
|
|
579
|
+
/** `[project].name` or `[tool.poetry].name`, or null. */
|
|
580
|
+
function pyPackageName(py) {
|
|
581
|
+
const project = asObject(py.project);
|
|
582
|
+
if (typeof project?.name === 'string' && project.name.trim())
|
|
583
|
+
return project.name.trim();
|
|
584
|
+
const poetry = asObject(asObject(py.tool)?.poetry);
|
|
585
|
+
if (typeof poetry?.name === 'string' && poetry.name.trim())
|
|
586
|
+
return poetry.name.trim();
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
/** Root pyproject `[tool.uv.workspace].members` globs (normalized). */
|
|
590
|
+
function collectPythonDeclaredGlobs(rootDir) {
|
|
591
|
+
const py = readPyproject(rootDir);
|
|
592
|
+
if (!py)
|
|
593
|
+
return [];
|
|
594
|
+
const uv = asObject(asObject(asObject(py.tool)?.uv)?.workspace);
|
|
595
|
+
const members = uv?.members;
|
|
596
|
+
if (!Array.isArray(members))
|
|
597
|
+
return [];
|
|
598
|
+
const globs = [];
|
|
599
|
+
for (const m of members) {
|
|
600
|
+
if (typeof m !== 'string')
|
|
601
|
+
continue;
|
|
602
|
+
let g = m.trim().replace(/\\/g, '/');
|
|
603
|
+
if (g.startsWith('!'))
|
|
604
|
+
continue;
|
|
605
|
+
if (g.startsWith('./'))
|
|
606
|
+
g = g.slice(2);
|
|
607
|
+
g = g.replace(/\/+$/, '');
|
|
608
|
+
if (g)
|
|
609
|
+
globs.push(g);
|
|
610
|
+
}
|
|
611
|
+
return globs;
|
|
612
|
+
}
|
|
613
|
+
/** Best-first candidate entry files for a Python package (its top-level
|
|
614
|
+
* `__init__.py`), from the dist name's import module + the dir basename. */
|
|
615
|
+
function pythonEntryCandidates(pkgRoot, name) {
|
|
616
|
+
const out = [];
|
|
617
|
+
const push = (p) => {
|
|
618
|
+
if (!out.includes(p))
|
|
619
|
+
out.push(p);
|
|
620
|
+
};
|
|
621
|
+
const rel = (f) => (pkgRoot ? `${pkgRoot}/${f}` : f);
|
|
622
|
+
const bases = [];
|
|
623
|
+
if (name)
|
|
624
|
+
bases.push(name.replace(/[-.]/g, '_').toLowerCase()); // dist name → import module
|
|
625
|
+
const dirBase = pkgRoot.split('/').pop() ?? '';
|
|
626
|
+
if (dirBase)
|
|
627
|
+
bases.push(dirBase);
|
|
628
|
+
for (const base of bases) {
|
|
629
|
+
push(rel(`src/${base}/__init__.py`));
|
|
630
|
+
push(rel(`${base}/__init__.py`));
|
|
631
|
+
}
|
|
632
|
+
push(rel('__init__.py'));
|
|
633
|
+
push(rel('main.py'));
|
|
634
|
+
return out;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Classify a Python package app / lib / tooling. Directory convention first
|
|
638
|
+
* (apps/ → app; packages|libs → lib; tools|config → tooling), then a runnable
|
|
639
|
+
* entry point (`[project.scripts]` / `[tool.poetry.scripts]`) → app, else lib.
|
|
640
|
+
*/
|
|
641
|
+
function pythonRoleOf(pkgRoot, py) {
|
|
642
|
+
const seg0 = pkgRoot.split('/')[0];
|
|
643
|
+
if (seg0 !== '' && TOOLING_DIRS.has(seg0))
|
|
644
|
+
return 'tooling';
|
|
645
|
+
if (seg0 === 'apps')
|
|
646
|
+
return 'app';
|
|
647
|
+
if (seg0 === 'packages' || seg0 === 'libs')
|
|
648
|
+
return 'lib';
|
|
649
|
+
const scripts = asObject(asObject(py?.project)?.scripts) ?? asObject(asObject(asObject(py?.tool)?.poetry)?.scripts);
|
|
650
|
+
if (scripts && Object.keys(scripts).length > 0)
|
|
651
|
+
return 'app';
|
|
652
|
+
return 'lib';
|
|
653
|
+
}
|
|
654
|
+
/** Distribution names this pyproject depends on (PEP 621 + poetry deps). */
|
|
655
|
+
function pyprojectDepNames(py) {
|
|
656
|
+
const names = [];
|
|
657
|
+
const project = asObject(py.project);
|
|
658
|
+
if (Array.isArray(project?.dependencies)) {
|
|
659
|
+
for (const d of project.dependencies) {
|
|
660
|
+
if (typeof d !== 'string')
|
|
661
|
+
continue;
|
|
662
|
+
const n = requirementName(d);
|
|
663
|
+
if (n)
|
|
664
|
+
names.push(n);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const poetryDeps = asObject(asObject(asObject(py.tool)?.poetry)?.dependencies);
|
|
668
|
+
if (poetryDeps) {
|
|
669
|
+
for (const k of Object.keys(poetryDeps))
|
|
670
|
+
if (k.toLowerCase() !== 'python')
|
|
671
|
+
names.push(k);
|
|
672
|
+
}
|
|
673
|
+
return names;
|
|
674
|
+
}
|
|
675
|
+
/** Local `{path=…}` sibling deps (poetry deps + uv sources). */
|
|
676
|
+
function pythonPathDeps(py) {
|
|
677
|
+
const paths = [];
|
|
678
|
+
const collect = (table) => {
|
|
679
|
+
if (!table)
|
|
680
|
+
return;
|
|
681
|
+
for (const v of Object.values(table)) {
|
|
682
|
+
const o = asObject(v);
|
|
683
|
+
if (typeof o?.path === 'string' && o.path.trim())
|
|
684
|
+
paths.push(o.path.trim());
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
collect(asObject(asObject(asObject(py.tool)?.poetry)?.dependencies));
|
|
688
|
+
collect(asObject(asObject(asObject(py.tool)?.uv)?.sources));
|
|
689
|
+
return paths;
|
|
690
|
+
}
|
|
691
|
+
/** Resolve a package-relative dir (with `..`) to a repo-relative root, or null
|
|
692
|
+
* if it escapes the repo. */
|
|
693
|
+
function resolveRelDir(pkgRoot, rel) {
|
|
694
|
+
const p = rel.trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
695
|
+
const segs = pkgRoot ? pkgRoot.split('/') : [];
|
|
696
|
+
for (const s of p.split('/')) {
|
|
697
|
+
if (s === '' || s === '.')
|
|
698
|
+
continue;
|
|
699
|
+
if (s === '..') {
|
|
700
|
+
if (segs.length === 0)
|
|
701
|
+
return null;
|
|
702
|
+
segs.pop();
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
segs.push(s);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return segs.join('/');
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* A Python package's sibling deps — sibling roots it depends on, from a
|
|
712
|
+
* dep NAMING a sibling (PEP 621 / poetry, PEP-503 normalized) or a `{path=…}` dep
|
|
713
|
+
* resolving to a known sibling root. Sorted, deduped, self/root excluded.
|
|
714
|
+
*/
|
|
715
|
+
function computePythonDeclaredDeps(pkgRoot, py, nameToPackage, knownRoots, pyRoots) {
|
|
716
|
+
if (!py)
|
|
717
|
+
return [];
|
|
718
|
+
const deps = new Set();
|
|
719
|
+
// Only PYTHON siblings are name-matchable (a shared-name npm package must not
|
|
720
|
+
// shadow a Python dep — the two ecosystems share the `nameToPackage` namespace).
|
|
721
|
+
const normToRoot = new Map();
|
|
722
|
+
for (const [name, pkg] of nameToPackage) {
|
|
723
|
+
if (pkg.root !== '' && pkg.root !== pkgRoot && pyRoots.has(pkg.root)) {
|
|
724
|
+
normToRoot.set(normalizePyName(name), pkg.root);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
for (const depName of pyprojectDepNames(py)) {
|
|
728
|
+
const root = normToRoot.get(normalizePyName(depName));
|
|
729
|
+
if (root)
|
|
730
|
+
deps.add(root);
|
|
731
|
+
}
|
|
732
|
+
for (const rel of pythonPathDeps(py)) {
|
|
733
|
+
const root = resolveRelDir(pkgRoot, rel);
|
|
734
|
+
if (root !== null && root !== pkgRoot && knownRoots.has(root))
|
|
735
|
+
deps.add(root);
|
|
736
|
+
}
|
|
737
|
+
return [...deps].sort();
|
|
738
|
+
}
|
|
739
|
+
//# sourceMappingURL=workspaces.js.map
|