codesift-mcp 0.3.0 → 0.4.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/README.md +143 -20
- package/dist/cache/hono-cache.d.ts +50 -0
- package/dist/cache/hono-cache.d.ts.map +1 -0
- package/dist/cache/hono-cache.js +132 -0
- package/dist/cache/hono-cache.js.map +1 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +17 -2
- package/dist/cli/setup.js.map +1 -1
- package/dist/formatters-shortening.d.ts +13 -0
- package/dist/formatters-shortening.d.ts.map +1 -1
- package/dist/formatters-shortening.js +131 -0
- package/dist/formatters-shortening.js.map +1 -1
- package/dist/formatters.d.ts +38 -0
- package/dist/formatters.d.ts.map +1 -1
- package/dist/formatters.js +498 -0
- package/dist/formatters.js.map +1 -1
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.d.ts.map +1 -1
- package/dist/instructions.js +27 -26
- package/dist/instructions.js.map +1 -1
- package/dist/lsp/lsp-servers.d.ts.map +1 -1
- package/dist/lsp/lsp-servers.js +5 -0
- package/dist/lsp/lsp-servers.js.map +1 -1
- package/dist/lsp/lsp-tools.d.ts.map +1 -1
- package/dist/lsp/lsp-tools.js +1 -0
- package/dist/lsp/lsp-tools.js.map +1 -1
- package/dist/parser/astro-template.d.ts +47 -0
- package/dist/parser/astro-template.d.ts.map +1 -0
- package/dist/parser/astro-template.js +171 -0
- package/dist/parser/astro-template.js.map +1 -0
- package/dist/parser/extractors/_shared.d.ts +4 -0
- package/dist/parser/extractors/_shared.d.ts.map +1 -1
- package/dist/parser/extractors/_shared.js +8 -0
- package/dist/parser/extractors/_shared.js.map +1 -1
- package/dist/parser/extractors/astro.d.ts +4 -5
- package/dist/parser/extractors/astro.d.ts.map +1 -1
- package/dist/parser/extractors/astro.js +102 -26
- package/dist/parser/extractors/astro.js.map +1 -1
- package/dist/parser/extractors/gradle-kts.d.ts +4 -0
- package/dist/parser/extractors/gradle-kts.d.ts.map +1 -0
- package/dist/parser/extractors/gradle-kts.js +246 -0
- package/dist/parser/extractors/gradle-kts.js.map +1 -0
- package/dist/parser/extractors/hono-inline-analyzer.d.ts +34 -0
- package/dist/parser/extractors/hono-inline-analyzer.d.ts.map +1 -0
- package/dist/parser/extractors/hono-inline-analyzer.js +465 -0
- package/dist/parser/extractors/hono-inline-analyzer.js.map +1 -0
- package/dist/parser/extractors/hono-model.d.ts +196 -0
- package/dist/parser/extractors/hono-model.d.ts.map +1 -0
- package/dist/parser/extractors/hono-model.js +10 -0
- package/dist/parser/extractors/hono-model.js.map +1 -0
- package/dist/parser/extractors/hono.d.ts +118 -0
- package/dist/parser/extractors/hono.d.ts.map +1 -0
- package/dist/parser/extractors/hono.js +1527 -0
- package/dist/parser/extractors/hono.js.map +1 -0
- package/dist/parser/extractors/kotlin.d.ts +4 -0
- package/dist/parser/extractors/kotlin.d.ts.map +1 -0
- package/dist/parser/extractors/kotlin.js +521 -0
- package/dist/parser/extractors/kotlin.js.map +1 -0
- package/dist/parser/extractors/php.d.ts +22 -0
- package/dist/parser/extractors/php.d.ts.map +1 -0
- package/dist/parser/extractors/php.js +326 -0
- package/dist/parser/extractors/php.js.map +1 -0
- package/dist/parser/extractors/python.d.ts.map +1 -1
- package/dist/parser/extractors/python.js +234 -11
- package/dist/parser/extractors/python.js.map +1 -1
- package/dist/parser/extractors/sql.d.ts +33 -0
- package/dist/parser/extractors/sql.d.ts.map +1 -0
- package/dist/parser/extractors/sql.js +506 -0
- package/dist/parser/extractors/sql.js.map +1 -0
- package/dist/parser/extractors/typescript.d.ts.map +1 -1
- package/dist/parser/extractors/typescript.js +166 -3
- package/dist/parser/extractors/typescript.js.map +1 -1
- package/dist/parser/languages/tree-sitter-javascript.wasm +0 -0
- package/dist/parser/languages/tree-sitter-kotlin.wasm +0 -0
- package/dist/parser/languages/tree-sitter-php.wasm +0 -0
- package/dist/parser/languages/tree-sitter-php_only.wasm +0 -0
- package/dist/parser/languages/tree-sitter-python.wasm +0 -0
- package/dist/parser/parser-manager.d.ts +32 -0
- package/dist/parser/parser-manager.d.ts.map +1 -1
- package/dist/parser/parser-manager.js +82 -3
- package/dist/parser/parser-manager.js.map +1 -1
- package/dist/parser/symbol-extractor.d.ts.map +1 -1
- package/dist/parser/symbol-extractor.js +16 -0
- package/dist/parser/symbol-extractor.js.map +1 -1
- package/dist/register-tools.d.ts +37 -1
- package/dist/register-tools.d.ts.map +1 -1
- package/dist/register-tools.js +2657 -191
- package/dist/register-tools.js.map +1 -1
- package/dist/search/reranker.js +1 -1
- package/dist/search/reranker.js.map +1 -1
- package/dist/server-helpers.d.ts.map +1 -1
- package/dist/server-helpers.js +11 -0
- package/dist/server-helpers.js.map +1 -1
- package/dist/server.js +28 -1
- package/dist/server.js.map +1 -1
- package/dist/storage/index-store.d.ts +15 -1
- package/dist/storage/index-store.d.ts.map +1 -1
- package/dist/storage/index-store.js +27 -1
- package/dist/storage/index-store.js.map +1 -1
- package/dist/storage/session-state.d.ts +1 -1
- package/dist/storage/session-state.d.ts.map +1 -1
- package/dist/storage/session-state.js +6 -4
- package/dist/storage/session-state.js.map +1 -1
- package/dist/tools/agent-config-tools.d.ts +24 -0
- package/dist/tools/agent-config-tools.d.ts.map +1 -0
- package/dist/tools/agent-config-tools.js +119 -0
- package/dist/tools/agent-config-tools.js.map +1 -0
- package/dist/tools/architecture-tools.d.ts +23 -0
- package/dist/tools/architecture-tools.d.ts.map +1 -0
- package/dist/tools/architecture-tools.js +140 -0
- package/dist/tools/architecture-tools.js.map +1 -0
- package/dist/tools/astro-config.d.ts +33 -0
- package/dist/tools/astro-config.d.ts.map +1 -0
- package/dist/tools/astro-config.js +260 -0
- package/dist/tools/astro-config.js.map +1 -0
- package/dist/tools/astro-islands.d.ts +61 -0
- package/dist/tools/astro-islands.d.ts.map +1 -0
- package/dist/tools/astro-islands.js +240 -0
- package/dist/tools/astro-islands.js.map +1 -0
- package/dist/tools/astro-routes.d.ts +49 -0
- package/dist/tools/astro-routes.d.ts.map +1 -0
- package/dist/tools/astro-routes.js +119 -0
- package/dist/tools/astro-routes.js.map +1 -0
- package/dist/tools/audit-tools.d.ts +38 -0
- package/dist/tools/audit-tools.d.ts.map +1 -0
- package/dist/tools/audit-tools.js +248 -0
- package/dist/tools/audit-tools.js.map +1 -0
- package/dist/tools/celery-tools.d.ts +38 -0
- package/dist/tools/celery-tools.d.ts.map +1 -0
- package/dist/tools/celery-tools.js +154 -0
- package/dist/tools/celery-tools.js.map +1 -0
- package/dist/tools/clone-tools.js +1 -1
- package/dist/tools/clone-tools.js.map +1 -1
- package/dist/tools/complexity-tools.d.ts +4 -0
- package/dist/tools/complexity-tools.d.ts.map +1 -1
- package/dist/tools/complexity-tools.js +78 -4
- package/dist/tools/complexity-tools.js.map +1 -1
- package/dist/tools/compose-tools.d.ts +60 -0
- package/dist/tools/compose-tools.d.ts.map +1 -0
- package/dist/tools/compose-tools.js +203 -0
- package/dist/tools/compose-tools.js.map +1 -0
- package/dist/tools/coupling-tools.d.ts +50 -0
- package/dist/tools/coupling-tools.d.ts.map +1 -0
- package/dist/tools/coupling-tools.js +262 -0
- package/dist/tools/coupling-tools.js.map +1 -0
- package/dist/tools/dependency-audit-tools.d.ts +65 -0
- package/dist/tools/dependency-audit-tools.d.ts.map +1 -0
- package/dist/tools/dependency-audit-tools.js +553 -0
- package/dist/tools/dependency-audit-tools.js.map +1 -0
- package/dist/tools/django-settings.d.ts +22 -0
- package/dist/tools/django-settings.d.ts.map +1 -0
- package/dist/tools/django-settings.js +301 -0
- package/dist/tools/django-settings.js.map +1 -0
- package/dist/tools/frequency-tools.js +1 -1
- package/dist/tools/frequency-tools.js.map +1 -1
- package/dist/tools/graph-tools.d.ts +8 -2
- package/dist/tools/graph-tools.d.ts.map +1 -1
- package/dist/tools/graph-tools.js +44 -3
- package/dist/tools/graph-tools.js.map +1 -1
- package/dist/tools/hilt-tools.d.ts +55 -0
- package/dist/tools/hilt-tools.d.ts.map +1 -0
- package/dist/tools/hilt-tools.js +258 -0
- package/dist/tools/hilt-tools.js.map +1 -0
- package/dist/tools/hono-analyze-app.d.ts +48 -0
- package/dist/tools/hono-analyze-app.d.ts.map +1 -0
- package/dist/tools/hono-analyze-app.js +102 -0
- package/dist/tools/hono-analyze-app.js.map +1 -0
- package/dist/tools/hono-api-contract.d.ts +22 -0
- package/dist/tools/hono-api-contract.d.ts.map +1 -0
- package/dist/tools/hono-api-contract.js +80 -0
- package/dist/tools/hono-api-contract.js.map +1 -0
- package/dist/tools/hono-conditional-middleware.d.ts +27 -0
- package/dist/tools/hono-conditional-middleware.d.ts.map +1 -0
- package/dist/tools/hono-conditional-middleware.js +62 -0
- package/dist/tools/hono-conditional-middleware.js.map +1 -0
- package/dist/tools/hono-context-flow.d.ts +24 -0
- package/dist/tools/hono-context-flow.d.ts.map +1 -0
- package/dist/tools/hono-context-flow.js +78 -0
- package/dist/tools/hono-context-flow.js.map +1 -0
- package/dist/tools/hono-dead-routes.d.ts +26 -0
- package/dist/tools/hono-dead-routes.d.ts.map +1 -0
- package/dist/tools/hono-dead-routes.js +109 -0
- package/dist/tools/hono-dead-routes.js.map +1 -0
- package/dist/tools/hono-env-regression.d.ts +29 -0
- package/dist/tools/hono-env-regression.d.ts.map +1 -0
- package/dist/tools/hono-env-regression.js +157 -0
- package/dist/tools/hono-env-regression.js.map +1 -0
- package/dist/tools/hono-inline-analyze.d.ts +31 -0
- package/dist/tools/hono-inline-analyze.d.ts.map +1 -0
- package/dist/tools/hono-inline-analyze.js +67 -0
- package/dist/tools/hono-inline-analyze.js.map +1 -0
- package/dist/tools/hono-middleware-chain.d.ts +22 -0
- package/dist/tools/hono-middleware-chain.d.ts.map +1 -0
- package/dist/tools/hono-middleware-chain.js +84 -0
- package/dist/tools/hono-middleware-chain.js.map +1 -0
- package/dist/tools/hono-modules.d.ts +22 -0
- package/dist/tools/hono-modules.d.ts.map +1 -0
- package/dist/tools/hono-modules.js +126 -0
- package/dist/tools/hono-modules.js.map +1 -0
- package/dist/tools/hono-response-types.d.ts +37 -0
- package/dist/tools/hono-response-types.d.ts.map +1 -0
- package/dist/tools/hono-response-types.js +84 -0
- package/dist/tools/hono-response-types.js.map +1 -0
- package/dist/tools/hono-rpc-types.d.ts +21 -0
- package/dist/tools/hono-rpc-types.d.ts.map +1 -0
- package/dist/tools/hono-rpc-types.js +57 -0
- package/dist/tools/hono-rpc-types.js.map +1 -0
- package/dist/tools/hono-security.d.ts +21 -0
- package/dist/tools/hono-security.d.ts.map +1 -0
- package/dist/tools/hono-security.js +98 -0
- package/dist/tools/hono-security.js.map +1 -0
- package/dist/tools/hono-visualize.d.ts +13 -0
- package/dist/tools/hono-visualize.d.ts.map +1 -0
- package/dist/tools/hono-visualize.js +72 -0
- package/dist/tools/hono-visualize.js.map +1 -0
- package/dist/tools/hotspot-tools.d.ts.map +1 -1
- package/dist/tools/hotspot-tools.js +9 -7
- package/dist/tools/hotspot-tools.js.map +1 -1
- package/dist/tools/index-tools.d.ts +17 -0
- package/dist/tools/index-tools.d.ts.map +1 -1
- package/dist/tools/index-tools.js +210 -10
- package/dist/tools/index-tools.js.map +1 -1
- package/dist/tools/kotlin-tools.d.ts +142 -0
- package/dist/tools/kotlin-tools.d.ts.map +1 -0
- package/dist/tools/kotlin-tools.js +572 -0
- package/dist/tools/kotlin-tools.js.map +1 -0
- package/dist/tools/legacy-hono-conventions.d.ts +14 -0
- package/dist/tools/legacy-hono-conventions.d.ts.map +1 -0
- package/dist/tools/legacy-hono-conventions.js +152 -0
- package/dist/tools/legacy-hono-conventions.js.map +1 -0
- package/dist/tools/migration-lint-tools.d.ts +26 -0
- package/dist/tools/migration-lint-tools.d.ts.map +1 -0
- package/dist/tools/migration-lint-tools.js +247 -0
- package/dist/tools/migration-lint-tools.js.map +1 -0
- package/dist/tools/model-tools.d.ts +30 -0
- package/dist/tools/model-tools.d.ts.map +1 -0
- package/dist/tools/model-tools.js +145 -0
- package/dist/tools/model-tools.js.map +1 -0
- package/dist/tools/nest-ext-tools.d.ts +92 -0
- package/dist/tools/nest-ext-tools.d.ts.map +1 -0
- package/dist/tools/nest-ext-tools.js +359 -0
- package/dist/tools/nest-ext-tools.js.map +1 -0
- package/dist/tools/nest-tools.d.ts +171 -0
- package/dist/tools/nest-tools.d.ts.map +1 -0
- package/dist/tools/nest-tools.js +1042 -0
- package/dist/tools/nest-tools.js.map +1 -0
- package/dist/tools/nextjs-api-contract-readers.d.ts +14 -0
- package/dist/tools/nextjs-api-contract-readers.d.ts.map +1 -0
- package/dist/tools/nextjs-api-contract-readers.js +204 -0
- package/dist/tools/nextjs-api-contract-readers.js.map +1 -0
- package/dist/tools/nextjs-api-contract-tools.d.ts +57 -0
- package/dist/tools/nextjs-api-contract-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-api-contract-tools.js +144 -0
- package/dist/tools/nextjs-api-contract-tools.js.map +1 -0
- package/dist/tools/nextjs-boundary-tools.d.ts +39 -0
- package/dist/tools/nextjs-boundary-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-boundary-tools.js +152 -0
- package/dist/tools/nextjs-boundary-tools.js.map +1 -0
- package/dist/tools/nextjs-component-tools.d.ts +121 -0
- package/dist/tools/nextjs-component-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-component-tools.js +460 -0
- package/dist/tools/nextjs-component-tools.js.map +1 -0
- package/dist/tools/nextjs-data-flow-tools.d.ts +42 -0
- package/dist/tools/nextjs-data-flow-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-data-flow-tools.js +158 -0
- package/dist/tools/nextjs-data-flow-tools.js.map +1 -0
- package/dist/tools/nextjs-framework-audit-tools.d.ts +37 -0
- package/dist/tools/nextjs-framework-audit-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-framework-audit-tools.js +211 -0
- package/dist/tools/nextjs-framework-audit-tools.js.map +1 -0
- package/dist/tools/nextjs-link-tools.d.ts +41 -0
- package/dist/tools/nextjs-link-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-link-tools.js +157 -0
- package/dist/tools/nextjs-link-tools.js.map +1 -0
- package/dist/tools/nextjs-metadata-tools.d.ts +74 -0
- package/dist/tools/nextjs-metadata-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-metadata-tools.js +252 -0
- package/dist/tools/nextjs-metadata-tools.js.map +1 -0
- package/dist/tools/nextjs-middleware-coverage-tools.d.ts +41 -0
- package/dist/tools/nextjs-middleware-coverage-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-middleware-coverage-tools.js +88 -0
- package/dist/tools/nextjs-middleware-coverage-tools.js.map +1 -0
- package/dist/tools/nextjs-route-tools.d.ts +100 -0
- package/dist/tools/nextjs-route-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-route-tools.js +493 -0
- package/dist/tools/nextjs-route-tools.js.map +1 -0
- package/dist/tools/nextjs-security-readers.d.ts +22 -0
- package/dist/tools/nextjs-security-readers.d.ts.map +1 -0
- package/dist/tools/nextjs-security-readers.js +318 -0
- package/dist/tools/nextjs-security-readers.js.map +1 -0
- package/dist/tools/nextjs-security-scoring.d.ts +15 -0
- package/dist/tools/nextjs-security-scoring.d.ts.map +1 -0
- package/dist/tools/nextjs-security-scoring.js +65 -0
- package/dist/tools/nextjs-security-scoring.js.map +1 -0
- package/dist/tools/nextjs-security-tools.d.ts +75 -0
- package/dist/tools/nextjs-security-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-security-tools.js +153 -0
- package/dist/tools/nextjs-security-tools.js.map +1 -0
- package/dist/tools/nextjs-tools.d.ts +15 -0
- package/dist/tools/nextjs-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-tools.js +15 -0
- package/dist/tools/nextjs-tools.js.map +1 -0
- package/dist/tools/outline-tools.d.ts.map +1 -1
- package/dist/tools/outline-tools.js +20 -0
- package/dist/tools/outline-tools.js.map +1 -1
- package/dist/tools/pattern-tools.d.ts +8 -0
- package/dist/tools/pattern-tools.d.ts.map +1 -1
- package/dist/tools/pattern-tools.js +561 -3
- package/dist/tools/pattern-tools.js.map +1 -1
- package/dist/tools/perf-tools.d.ts +32 -0
- package/dist/tools/perf-tools.d.ts.map +1 -0
- package/dist/tools/perf-tools.js +227 -0
- package/dist/tools/perf-tools.js.map +1 -0
- package/dist/tools/php-tools.d.ts +176 -0
- package/dist/tools/php-tools.d.ts.map +1 -0
- package/dist/tools/php-tools.js +543 -0
- package/dist/tools/php-tools.js.map +1 -0
- package/dist/tools/prisma-schema-tools.d.ts +44 -0
- package/dist/tools/prisma-schema-tools.d.ts.map +1 -0
- package/dist/tools/prisma-schema-tools.js +358 -0
- package/dist/tools/prisma-schema-tools.js.map +1 -0
- package/dist/tools/project-tools.d.ts +115 -6
- package/dist/tools/project-tools.d.ts.map +1 -1
- package/dist/tools/project-tools.js +594 -217
- package/dist/tools/project-tools.js.map +1 -1
- package/dist/tools/pyproject-tools.d.ts +23 -0
- package/dist/tools/pyproject-tools.d.ts.map +1 -0
- package/dist/tools/pyproject-tools.js +133 -0
- package/dist/tools/pyproject-tools.js.map +1 -0
- package/dist/tools/pytest-tools.d.ts +20 -0
- package/dist/tools/pytest-tools.d.ts.map +1 -0
- package/dist/tools/pytest-tools.js +106 -0
- package/dist/tools/pytest-tools.js.map +1 -0
- package/dist/tools/python-callers.d.ts +28 -0
- package/dist/tools/python-callers.d.ts.map +1 -0
- package/dist/tools/python-callers.js +110 -0
- package/dist/tools/python-callers.js.map +1 -0
- package/dist/tools/python-circular-imports.d.ts +19 -0
- package/dist/tools/python-circular-imports.d.ts.map +1 -0
- package/dist/tools/python-circular-imports.js +126 -0
- package/dist/tools/python-circular-imports.js.map +1 -0
- package/dist/tools/python-deps-analyzer.d.ts +46 -0
- package/dist/tools/python-deps-analyzer.d.ts.map +1 -0
- package/dist/tools/python-deps-analyzer.js +227 -0
- package/dist/tools/python-deps-analyzer.js.map +1 -0
- package/dist/tools/query-tools.d.ts +23 -0
- package/dist/tools/query-tools.d.ts.map +1 -0
- package/dist/tools/query-tools.js +256 -0
- package/dist/tools/query-tools.js.map +1 -0
- package/dist/tools/react-tools.d.ts +218 -0
- package/dist/tools/react-tools.d.ts.map +1 -0
- package/dist/tools/react-tools.js +714 -0
- package/dist/tools/react-tools.js.map +1 -0
- package/dist/tools/report-tools.js +47 -0
- package/dist/tools/report-tools.js.map +1 -1
- package/dist/tools/review-diff-tools.d.ts +2 -6
- package/dist/tools/review-diff-tools.d.ts.map +1 -1
- package/dist/tools/review-diff-tools.js +51 -66
- package/dist/tools/review-diff-tools.js.map +1 -1
- package/dist/tools/room-tools.d.ts +36 -0
- package/dist/tools/room-tools.d.ts.map +1 -0
- package/dist/tools/room-tools.js +147 -0
- package/dist/tools/room-tools.js.map +1 -0
- package/dist/tools/route-tools.d.ts +27 -1
- package/dist/tools/route-tools.d.ts.map +1 -1
- package/dist/tools/route-tools.js +744 -18
- package/dist/tools/route-tools.js.map +1 -1
- package/dist/tools/ruff-tools.d.ts +32 -0
- package/dist/tools/ruff-tools.d.ts.map +1 -0
- package/dist/tools/ruff-tools.js +114 -0
- package/dist/tools/ruff-tools.js.map +1 -0
- package/dist/tools/search-ranker.d.ts.map +1 -1
- package/dist/tools/search-ranker.js +7 -0
- package/dist/tools/search-ranker.js.map +1 -1
- package/dist/tools/serialization-tools.d.ts +24 -0
- package/dist/tools/serialization-tools.d.ts.map +1 -0
- package/dist/tools/serialization-tools.js +156 -0
- package/dist/tools/serialization-tools.js.map +1 -0
- package/dist/tools/sql-tools.d.ts +234 -0
- package/dist/tools/sql-tools.d.ts.map +1 -0
- package/dist/tools/sql-tools.js +1037 -0
- package/dist/tools/sql-tools.js.map +1 -0
- package/dist/tools/status-tools.d.ts +10 -0
- package/dist/tools/status-tools.d.ts.map +1 -0
- package/dist/tools/status-tools.js +32 -0
- package/dist/tools/status-tools.js.map +1 -0
- package/dist/tools/symbol-tools.d.ts +19 -0
- package/dist/tools/symbol-tools.d.ts.map +1 -1
- package/dist/tools/symbol-tools.js +78 -4
- package/dist/tools/symbol-tools.js.map +1 -1
- package/dist/tools/test-impact-tools.d.ts +29 -0
- package/dist/tools/test-impact-tools.d.ts.map +1 -0
- package/dist/tools/test-impact-tools.js +156 -0
- package/dist/tools/test-impact-tools.js.map +1 -0
- package/dist/tools/typecheck-tools.d.ts +39 -0
- package/dist/tools/typecheck-tools.d.ts.map +1 -0
- package/dist/tools/typecheck-tools.js +191 -0
- package/dist/tools/typecheck-tools.js.map +1 -0
- package/dist/tools/wiring-tools.d.ts +19 -0
- package/dist/tools/wiring-tools.d.ts.map +1 -0
- package/dist/tools/wiring-tools.js +147 -0
- package/dist/tools/wiring-tools.js.map +1 -0
- package/dist/types.d.ts +9 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/framework-detect.d.ts +18 -2
- package/dist/utils/framework-detect.d.ts.map +1 -1
- package/dist/utils/framework-detect.js +150 -3
- package/dist/utils/framework-detect.js.map +1 -1
- package/dist/utils/import-graph.d.ts +36 -0
- package/dist/utils/import-graph.d.ts.map +1 -1
- package/dist/utils/import-graph.js +212 -9
- package/dist/utils/import-graph.js.map +1 -1
- package/dist/utils/language-detect.d.ts +21 -0
- package/dist/utils/language-detect.d.ts.map +1 -0
- package/dist/utils/language-detect.js +183 -0
- package/dist/utils/language-detect.js.map +1 -0
- package/dist/utils/nextjs-ast-readers.d.ts +44 -0
- package/dist/utils/nextjs-ast-readers.d.ts.map +1 -0
- package/dist/utils/nextjs-ast-readers.js +341 -0
- package/dist/utils/nextjs-ast-readers.js.map +1 -0
- package/dist/utils/nextjs-audit-cache.d.ts +51 -0
- package/dist/utils/nextjs-audit-cache.d.ts.map +1 -0
- package/dist/utils/nextjs-audit-cache.js +116 -0
- package/dist/utils/nextjs-audit-cache.js.map +1 -0
- package/dist/utils/nextjs-metadata-readers.d.ts +65 -0
- package/dist/utils/nextjs-metadata-readers.d.ts.map +1 -0
- package/dist/utils/nextjs-metadata-readers.js +447 -0
- package/dist/utils/nextjs-metadata-readers.js.map +1 -0
- package/dist/utils/nextjs.d.ts +42 -0
- package/dist/utils/nextjs.d.ts.map +1 -0
- package/dist/utils/nextjs.js +284 -0
- package/dist/utils/nextjs.js.map +1 -0
- package/dist/utils/python-import-resolver.d.ts +42 -0
- package/dist/utils/python-import-resolver.d.ts.map +1 -0
- package/dist/utils/python-import-resolver.js +101 -0
- package/dist/utils/python-import-resolver.js.map +1 -0
- package/dist/utils/python-imports.d.ts +28 -0
- package/dist/utils/python-imports.d.ts.map +1 -0
- package/dist/utils/python-imports.js +117 -0
- package/dist/utils/python-imports.js.map +1 -0
- package/dist/utils/react-alias.d.ts +15 -0
- package/dist/utils/react-alias.d.ts.map +1 -0
- package/dist/utils/react-alias.js +31 -0
- package/dist/utils/react-alias.js.map +1 -0
- package/dist/utils/test-file.d.ts.map +1 -1
- package/dist/utils/test-file.js +7 -0
- package/dist/utils/test-file.js.map +1 -1
- package/dist/utils/walk.d.ts +22 -0
- package/dist/utils/walk.d.ts.map +1 -1
- package/dist/utils/walk.js +70 -2
- package/dist/utils/walk.js.map +1 -1
- package/package.json +3 -2
- package/rules/codesift.md +33 -5
- package/rules/codesift.mdc +33 -5
- package/rules/codex.md +33 -5
- package/rules/gemini.md +33 -5
- package/src/parser/languages/tree-sitter-javascript.wasm +0 -0
- package/src/parser/languages/tree-sitter-kotlin.wasm +0 -0
- package/src/parser/languages/tree-sitter-php.wasm +0 -0
- package/src/parser/languages/tree-sitter-php_only.wasm +0 -0
- package/src/parser/languages/tree-sitter-python.wasm +0 -0
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTTP route tracing — given a URL path, find handler → service → DB calls.
|
|
3
|
-
* Supports NestJS decorators, Next.js App Router, and
|
|
3
|
+
* Supports NestJS decorators, Next.js App Router, Express, Yii2 conventions, and Laravel routes.
|
|
4
4
|
*/
|
|
5
5
|
import { getCodeIndex } from "./index-tools.js";
|
|
6
6
|
import { buildAdjacencyIndex, buildCallTree, stripSource } from "./graph-tools.js";
|
|
7
|
+
import { findAstroHandlers } from "./astro-routes.js";
|
|
8
|
+
import { deriveUrlPath, computeLayoutChain, traceMiddleware, scanDirective } from "../utils/nextjs.js";
|
|
9
|
+
import { join } from "node:path";
|
|
7
10
|
const DB_PATTERNS = [
|
|
8
11
|
/prisma\.\w+\.(findMany|findFirst|findUnique|create|update|delete|upsert|count|aggregate|groupBy)/,
|
|
9
12
|
/\.\$(transaction|queryRaw|executeRaw)/,
|
|
10
13
|
/getRepository|\.query\(|\.execute\(/,
|
|
11
14
|
/knex\.|\.raw\(/,
|
|
15
|
+
// PHP / Yii2 ActiveRecord
|
|
16
|
+
/->find\(\)|->findOne\(|->findAll\(|->findBySql\(/,
|
|
17
|
+
/->createCommand\(|Yii::\$app->db/,
|
|
18
|
+
/::find\(\)->where\(|->andWhere\(|->orWhere\(/,
|
|
19
|
+
// Kotlin — Exposed ORM, Spring Data, Ktor
|
|
20
|
+
/transaction\s*\{[\s\S]*?\.(select|insert|update|delete)/,
|
|
21
|
+
/\.(findById|findAll|save|deleteById|findBy\w+)\s*\(/,
|
|
22
|
+
/\bSchemaUtils\.(create|drop)/,
|
|
23
|
+
// Python — Django ORM, SQLAlchemy
|
|
24
|
+
/\.objects\.(get|filter|all|exclude|create|update|delete|aggregate|annotate|values|values_list|count|exists|first|last|bulk_create|bulk_update|get_or_create|update_or_create)\s*\(/,
|
|
25
|
+
/\.query\.(filter|filter_by|get|all|first|one|one_or_none|join|outerjoin|subquery)\s*\(/,
|
|
26
|
+
/session\.(add|delete|commit|rollback|flush|execute|query)\s*\(/,
|
|
27
|
+
/\.select_related\(|\.prefetch_related\(/,
|
|
12
28
|
];
|
|
13
29
|
/**
|
|
14
30
|
* Match a URL path pattern against a route definition.
|
|
15
|
-
* Handles :param, [param], [...param], [[...param]]
|
|
31
|
+
* Handles :param, [param], [...param], [[...param]], <type:name> (Flask/Django),
|
|
32
|
+
* {name} (FastAPI) as wildcards.
|
|
16
33
|
*/
|
|
17
|
-
function matchPath(routePath, searchPath) {
|
|
34
|
+
export function matchPath(routePath, searchPath) {
|
|
18
35
|
const normalize = (p) => p.replace(/^\/|\/$/g, "").toLowerCase();
|
|
19
36
|
const routeParts = normalize(routePath).split("/");
|
|
20
37
|
const searchParts = normalize(searchPath).split("/");
|
|
@@ -23,8 +40,8 @@ function matchPath(routePath, searchPath) {
|
|
|
23
40
|
for (let i = 0; i < routeParts.length; i++) {
|
|
24
41
|
const rp = routeParts[i];
|
|
25
42
|
const sp = searchParts[i];
|
|
26
|
-
// Dynamic segments: :id, [id], [...slug], [[...slug]]
|
|
27
|
-
if (rp.startsWith(":") || rp.startsWith("["))
|
|
43
|
+
// Dynamic segments: :id, [id], [...slug], [[...slug]], <type:name>, {name}
|
|
44
|
+
if (rp.startsWith(":") || rp.startsWith("[") || rp.startsWith("<") || rp.startsWith("{"))
|
|
28
45
|
continue;
|
|
29
46
|
if (rp !== sp)
|
|
30
47
|
return false;
|
|
@@ -35,25 +52,27 @@ function matchPath(routePath, searchPath) {
|
|
|
35
52
|
* Find NestJS route handlers via @Controller + @Get/@Post/etc. decorators.
|
|
36
53
|
* Reads raw file content because tree-sitter symbol source may not include decorators.
|
|
37
54
|
*/
|
|
38
|
-
async function findNestJSHandlers(index, searchPath) {
|
|
55
|
+
export async function findNestJSHandlers(index, searchPath) {
|
|
39
56
|
const handlers = [];
|
|
40
57
|
const methods = ["Get", "Post", "Put", "Delete", "Patch"];
|
|
41
58
|
// Find controller files
|
|
42
59
|
const controllerFiles = index.files.filter((f) => f.path.endsWith(".controller.ts") || f.path.endsWith(".controller.js"));
|
|
43
60
|
const { readFile } = await import("node:fs/promises");
|
|
44
|
-
const { join } = await import("node:path");
|
|
61
|
+
const { join: joinPath } = await import("node:path");
|
|
45
62
|
for (const file of controllerFiles) {
|
|
46
63
|
let source;
|
|
47
64
|
try {
|
|
48
|
-
source = await readFile(
|
|
65
|
+
source = await readFile(joinPath(index.root, file.path), "utf-8");
|
|
49
66
|
}
|
|
50
67
|
catch {
|
|
51
68
|
continue;
|
|
52
69
|
}
|
|
53
|
-
// Extract controller prefix
|
|
54
|
-
const
|
|
55
|
-
const
|
|
70
|
+
// Extract controller prefix — supports both @Controller('prefix') and @Controller()
|
|
71
|
+
const ctrlMatchStr = /@Controller\s*\(\s*['"`]([^'"`]*)['"`]/.exec(source);
|
|
72
|
+
const ctrlMatchEmpty = !ctrlMatchStr ? /@Controller\s*\(\s*\)/.exec(source) : null;
|
|
73
|
+
const controllerPrefix = ctrlMatchStr?.[1] ?? (ctrlMatchEmpty ? "" : "");
|
|
56
74
|
for (const method of methods) {
|
|
75
|
+
// Pass 1: string-literal paths — @Get('path')
|
|
57
76
|
const re = new RegExp(`@${method}\\s*\\(\\s*['"\`]([^'"\`]*)['"\`]\\s*\\)\\s*\\n\\s*(?:async\\s+)?(\\w+)`, "g");
|
|
58
77
|
let match;
|
|
59
78
|
while ((match = re.exec(source)) !== null) {
|
|
@@ -70,6 +89,24 @@ async function findNestJSHandlers(index, searchPath) {
|
|
|
70
89
|
});
|
|
71
90
|
}
|
|
72
91
|
}
|
|
92
|
+
// Pass 2: empty decorator — @Get() with no path argument
|
|
93
|
+
const reEmpty = new RegExp(`@${method}\\s*\\(\\s*\\)\\s*\\n\\s*(?:async\\s+)?(\\w+)`, "g");
|
|
94
|
+
let emptyMatch;
|
|
95
|
+
while ((emptyMatch = reEmpty.exec(source)) !== null) {
|
|
96
|
+
const funcName = emptyMatch[1] ?? "";
|
|
97
|
+
const fullPath = `/${controllerPrefix}`.replace(/\/+/g, "/") || "/";
|
|
98
|
+
if (matchPath(fullPath, searchPath)) {
|
|
99
|
+
if (handlers.some((h) => h.file === file.path && h.symbol.name === funcName && h.method === method.toUpperCase()))
|
|
100
|
+
continue;
|
|
101
|
+
const sym = index.symbols.find((s) => s.file === file.path && s.name === funcName);
|
|
102
|
+
handlers.push({
|
|
103
|
+
symbol: sym ? stripSource(sym) : { id: `${file.path}:${funcName}`, name: funcName, kind: "method", file: file.path, start_line: 1, end_line: 1 },
|
|
104
|
+
file: file.path,
|
|
105
|
+
method: method.toUpperCase(),
|
|
106
|
+
framework: "nestjs",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
73
110
|
}
|
|
74
111
|
}
|
|
75
112
|
return handlers;
|
|
@@ -81,14 +118,15 @@ function findNextJSHandlers(index, searchPath) {
|
|
|
81
118
|
const handlers = [];
|
|
82
119
|
const normalized = searchPath.replace(/^\/|\/$/g, "");
|
|
83
120
|
for (const file of index.files) {
|
|
84
|
-
// Match app/api/...route.ts or app/...route.ts
|
|
85
|
-
if (
|
|
121
|
+
// Match app/api/...route.{ts,tsx,js,jsx} or app/...route.{ts,tsx,js,jsx}
|
|
122
|
+
if (!/\/route\.[jt]sx?$/.test(file.path))
|
|
86
123
|
continue;
|
|
87
124
|
// Extract route path from file path: app/api/users/[id]/route.ts → /api/users/[id]
|
|
88
|
-
const routeMatch = file.path.match(/app\/(.*?)\/route
|
|
125
|
+
const routeMatch = file.path.match(/app\/(.*?)\/route\.[jt]sx?$/);
|
|
89
126
|
if (!routeMatch)
|
|
90
127
|
continue;
|
|
91
|
-
|
|
128
|
+
// Strip route groups: (auth)/login → login
|
|
129
|
+
const filePath = routeMatch[1].replace(/\([^)]+\)\/?/g, "");
|
|
92
130
|
if (matchPath(filePath, normalized)) {
|
|
93
131
|
// Find exported handler functions (GET, POST, etc.)
|
|
94
132
|
const fileSymbols = index.symbols.filter((s) => s.file === file.path && /^(GET|POST|PUT|DELETE|PATCH)$/.test(s.name));
|
|
@@ -98,6 +136,7 @@ function findNextJSHandlers(index, searchPath) {
|
|
|
98
136
|
file: sym.file,
|
|
99
137
|
method: sym.name,
|
|
100
138
|
framework: "nextjs",
|
|
139
|
+
router: "app",
|
|
101
140
|
});
|
|
102
141
|
}
|
|
103
142
|
// If no named exports found, add the file itself
|
|
@@ -106,21 +145,162 @@ function findNextJSHandlers(index, searchPath) {
|
|
|
106
145
|
symbol: { id: file.path, name: "route", kind: "function", file: file.path, start_line: 1, end_line: 1 },
|
|
107
146
|
file: file.path,
|
|
108
147
|
framework: "nextjs",
|
|
148
|
+
router: "app",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return handlers;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Find Pages Router API route handlers via default exports in pages/api/.
|
|
157
|
+
* @internal exported for unit testing
|
|
158
|
+
*/
|
|
159
|
+
function findPagesRouterHandlers(index, searchPath) {
|
|
160
|
+
const handlers = [];
|
|
161
|
+
// Require Next.js project signal to disambiguate from Astro's src/pages/api/*.
|
|
162
|
+
// Accept next.config.* at root/src OR an App Router convention file.
|
|
163
|
+
const hasNextSignal = index.files.some((f) => /^(src\/)?next\.config\.[mc]?[jt]sx?$/.test(f.path) ||
|
|
164
|
+
/(^|\/)app\/.*\/(page|layout|route)\.[jt]sx?$/.test(f.path));
|
|
165
|
+
if (!hasNextSignal)
|
|
166
|
+
return handlers;
|
|
167
|
+
for (const file of index.files) {
|
|
168
|
+
// Only match files under pages/api/ (not src/pages/api/ which is Astro convention)
|
|
169
|
+
if (!/^(\.\/)?pages\/api\//.test(file.path))
|
|
170
|
+
continue;
|
|
171
|
+
// Derive URL path from file path
|
|
172
|
+
const urlPath = deriveUrlPath(file.path, "pages");
|
|
173
|
+
const normalizedSearch = searchPath.replace(/^\/|\/$/g, "");
|
|
174
|
+
const normalizedUrl = urlPath.replace(/^\/|\/$/g, "");
|
|
175
|
+
if (normalizedUrl !== normalizedSearch)
|
|
176
|
+
continue;
|
|
177
|
+
// Find default export or named handler in the file
|
|
178
|
+
const fileSymbols = index.symbols.filter((s) => s.file === file.path);
|
|
179
|
+
// Look for default export
|
|
180
|
+
const defaultExport = fileSymbols.find((s) => s.name === "default" || s.name === "handler");
|
|
181
|
+
if (defaultExport) {
|
|
182
|
+
handlers.push({
|
|
183
|
+
symbol: stripSource(defaultExport),
|
|
184
|
+
file: file.path,
|
|
185
|
+
framework: "nextjs",
|
|
186
|
+
router: "pages",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
else if (fileSymbols.length > 0) {
|
|
190
|
+
// Try variable indirection: find any exported function
|
|
191
|
+
const exported = fileSymbols.find((s) => s.kind === "function" || s.kind === "variable");
|
|
192
|
+
if (exported) {
|
|
193
|
+
handlers.push({
|
|
194
|
+
symbol: stripSource(exported),
|
|
195
|
+
file: file.path,
|
|
196
|
+
framework: "nextjs",
|
|
197
|
+
router: "pages",
|
|
109
198
|
});
|
|
110
199
|
}
|
|
111
200
|
}
|
|
201
|
+
// Fallback: at least mark the file as having a handler
|
|
202
|
+
if (handlers.filter((h) => h.file === file.path).length === 0) {
|
|
203
|
+
handlers.push({
|
|
204
|
+
symbol: {
|
|
205
|
+
id: file.path, name: "handler", kind: "function",
|
|
206
|
+
file: file.path, start_line: 1, end_line: 1,
|
|
207
|
+
},
|
|
208
|
+
file: file.path,
|
|
209
|
+
framework: "nextjs",
|
|
210
|
+
router: "pages",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
112
213
|
}
|
|
113
214
|
return handlers;
|
|
114
215
|
}
|
|
115
216
|
/**
|
|
116
217
|
* Find Express-style route handlers via router.get/app.post patterns.
|
|
218
|
+
* Restricted to JS/TS files to avoid false positives from Python test
|
|
219
|
+
* code like `client.get('/')` or Ruby/PHP method calls.
|
|
220
|
+
*/
|
|
221
|
+
/**
|
|
222
|
+
* Find Hono route handlers by consulting the HonoExtractor-produced model.
|
|
223
|
+
* Uses HonoCache when available, falls back to on-demand parse.
|
|
224
|
+
*
|
|
225
|
+
* The model has pre-resolved paths (basePath + mount prefix + route path),
|
|
226
|
+
* so path matching is a direct matchPath() call against each route.
|
|
117
227
|
*/
|
|
228
|
+
async function findHonoHandlers(repo, index, searchPath) {
|
|
229
|
+
// Only run if Hono is detected
|
|
230
|
+
const { detectFrameworks } = await import("../utils/framework-detect.js");
|
|
231
|
+
const frameworks = detectFrameworks(index);
|
|
232
|
+
if (!frameworks.has("hono"))
|
|
233
|
+
return [];
|
|
234
|
+
// Resolve entry file: prefer orchestrator, else first file containing `new Hono()`
|
|
235
|
+
const entryFile = resolveHonoEntryFile(index);
|
|
236
|
+
if (!entryFile)
|
|
237
|
+
return [];
|
|
238
|
+
// Get model (via cache)
|
|
239
|
+
let model;
|
|
240
|
+
try {
|
|
241
|
+
const { honoCache } = await import("../cache/hono-cache.js");
|
|
242
|
+
const { HonoExtractor } = await import("../parser/extractors/hono.js");
|
|
243
|
+
const extractor = new HonoExtractor();
|
|
244
|
+
model = await honoCache.get(repo, entryFile, extractor);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
const handlers = [];
|
|
250
|
+
for (const route of model.routes) {
|
|
251
|
+
if (!matchPath(route.path, searchPath))
|
|
252
|
+
continue;
|
|
253
|
+
// Find the handler symbol in the index by file + name + line
|
|
254
|
+
let handlerSym = index.symbols.find((s) => s.file === route.handler.file.replace(index.root + "/", "") &&
|
|
255
|
+
s.name === route.handler.name &&
|
|
256
|
+
Math.abs(s.start_line - route.handler.line) <= 2);
|
|
257
|
+
// Fallback: synthetic stub for inline handlers
|
|
258
|
+
if (!handlerSym) {
|
|
259
|
+
handlerSym = {
|
|
260
|
+
id: `hono:${route.file}:${route.line}`,
|
|
261
|
+
repo,
|
|
262
|
+
name: route.handler.name,
|
|
263
|
+
kind: "function",
|
|
264
|
+
file: route.handler.file.replace(index.root + "/", ""),
|
|
265
|
+
start_line: route.handler.line,
|
|
266
|
+
end_line: route.handler.line,
|
|
267
|
+
start_byte: 0,
|
|
268
|
+
end_byte: 0,
|
|
269
|
+
source: "",
|
|
270
|
+
tokens: [route.handler.name],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
handlers.push({
|
|
274
|
+
symbol: stripSource(handlerSym),
|
|
275
|
+
file: handlerSym.file,
|
|
276
|
+
method: route.method,
|
|
277
|
+
framework: "hono",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return handlers;
|
|
281
|
+
}
|
|
282
|
+
/** Find the Hono entry file for a repo — ORCHESTRATOR or first new Hono() file. */
|
|
283
|
+
function resolveHonoEntryFile(index) {
|
|
284
|
+
for (const sym of index.symbols) {
|
|
285
|
+
if (sym.source && /new\s+(?:Hono|OpenAPIHono)\s*(?:<[^>]*>)?\s*\(/.test(sym.source)) {
|
|
286
|
+
// Return absolute path
|
|
287
|
+
return join(index.root, sym.file);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
118
292
|
function findExpressHandlers(index, searchPath) {
|
|
119
293
|
const handlers = [];
|
|
120
294
|
const methods = ["get", "post", "put", "delete", "patch"];
|
|
121
295
|
for (const sym of index.symbols) {
|
|
122
296
|
if (!sym.source)
|
|
123
297
|
continue;
|
|
298
|
+
// Only scan JS/TS files — .get()/.post() is ambiguous across languages
|
|
299
|
+
if (!/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(sym.file))
|
|
300
|
+
continue;
|
|
301
|
+
// Skip test files to avoid matching test harness client calls
|
|
302
|
+
if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(sym.file))
|
|
303
|
+
continue;
|
|
124
304
|
for (const method of methods) {
|
|
125
305
|
const re = new RegExp(`\\.(${method})\\s*\\(\\s*['"\`]([^'"\`]+)['"\`]`);
|
|
126
306
|
const match = re.exec(sym.source);
|
|
@@ -142,6 +322,37 @@ function findExpressHandlers(index, searchPath) {
|
|
|
142
322
|
/**
|
|
143
323
|
* Detect DB operations in a symbol's call chain.
|
|
144
324
|
*/
|
|
325
|
+
/**
|
|
326
|
+
* Detect server actions in the call chain by checking for "use server" directive
|
|
327
|
+
* at the file level (not function-body level).
|
|
328
|
+
*/
|
|
329
|
+
async function findServerActions(repoRoot, calleeSymbols, callChain) {
|
|
330
|
+
const actions = [];
|
|
331
|
+
const checkedFiles = new Map();
|
|
332
|
+
for (const sym of calleeSymbols) {
|
|
333
|
+
const absPath = join(repoRoot, sym.file);
|
|
334
|
+
let hasDirective;
|
|
335
|
+
if (checkedFiles.has(sym.file)) {
|
|
336
|
+
hasDirective = checkedFiles.get(sym.file);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
const directive = await scanDirective(absPath);
|
|
340
|
+
hasDirective = directive === "use server";
|
|
341
|
+
checkedFiles.set(sym.file, hasDirective);
|
|
342
|
+
}
|
|
343
|
+
if (hasDirective) {
|
|
344
|
+
// Find who called this symbol
|
|
345
|
+
const callerIdx = callChain.findIndex((c) => c.file === sym.file && c.name === sym.name);
|
|
346
|
+
const calledFrom = callerIdx > 0 ? callChain[callerIdx - 1]?.name : undefined;
|
|
347
|
+
actions.push({
|
|
348
|
+
name: sym.name,
|
|
349
|
+
file: sym.file,
|
|
350
|
+
called_from: calledFrom,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return actions;
|
|
355
|
+
}
|
|
145
356
|
function findDbCalls(symbols) {
|
|
146
357
|
const calls = [];
|
|
147
358
|
for (const sym of symbols) {
|
|
@@ -184,8 +395,9 @@ function appendDbCalls(lines, dbCalls, node, actor) {
|
|
|
184
395
|
}
|
|
185
396
|
/**
|
|
186
397
|
* Render a RouteTraceResult as a Mermaid sequence diagram.
|
|
398
|
+
* @internal exported for unit testing
|
|
187
399
|
*/
|
|
188
|
-
function routeToMermaid(result) {
|
|
400
|
+
export function routeToMermaid(result) {
|
|
189
401
|
if (result.handlers.length === 0) {
|
|
190
402
|
return "sequenceDiagram\n Note over Client: No handler found for " + result.path;
|
|
191
403
|
}
|
|
@@ -193,7 +405,26 @@ function routeToMermaid(result) {
|
|
|
193
405
|
const handler = result.handlers[0];
|
|
194
406
|
const method = handler.method ?? "REQUEST";
|
|
195
407
|
const aliases = new Map();
|
|
196
|
-
|
|
408
|
+
// Add Middleware participant if middleware applies
|
|
409
|
+
if (result.middleware?.applies) {
|
|
410
|
+
lines.push(` participant Middleware`);
|
|
411
|
+
lines.push(` Client->>+Middleware: ${method} ${result.path}`);
|
|
412
|
+
lines.push(` Middleware->>+Controller: continue`);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
lines.push(` Client->>+Controller: ${method} ${result.path}`);
|
|
416
|
+
}
|
|
417
|
+
// Add Layout chain rendering
|
|
418
|
+
if (result.layout_chain && result.layout_chain.length > 0) {
|
|
419
|
+
let prev = "Controller";
|
|
420
|
+
for (let i = 0; i < result.layout_chain.length; i++) {
|
|
421
|
+
const layoutName = `Layout${i + 1}`;
|
|
422
|
+
const layoutFile = result.layout_chain[i];
|
|
423
|
+
lines.push(` participant ${layoutName}`);
|
|
424
|
+
lines.push(` ${prev}->>+${layoutName}: render (${layoutFile})`);
|
|
425
|
+
prev = layoutName;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
197
428
|
const root = result.call_chain[0];
|
|
198
429
|
if (root) {
|
|
199
430
|
appendDbCalls(lines, result.db_calls, root, "Controller");
|
|
@@ -223,9 +454,463 @@ function routeToMermaid(result) {
|
|
|
223
454
|
closeUntilDepth(nextDepth);
|
|
224
455
|
}
|
|
225
456
|
closeUntilDepth(0);
|
|
226
|
-
|
|
457
|
+
// Close layout chain
|
|
458
|
+
if (result.layout_chain && result.layout_chain.length > 0) {
|
|
459
|
+
for (let i = result.layout_chain.length - 1; i >= 0; i--) {
|
|
460
|
+
const layoutName = `Layout${i + 1}`;
|
|
461
|
+
const returnTo = i > 0 ? `Layout${i}` : "Controller";
|
|
462
|
+
lines.push(` ${layoutName}-->>-${returnTo}: rendered`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (result.middleware?.applies) {
|
|
466
|
+
lines.push(` Controller-->>-Middleware: response`);
|
|
467
|
+
lines.push(` Middleware-->>-Client: response`);
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
lines.push(` Controller-->>-Client: response`);
|
|
471
|
+
}
|
|
227
472
|
return lines.join("\n");
|
|
228
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* Find Yii2 route handlers via convention: controller-id/action-id → ControllerIdController::actionActionId().
|
|
476
|
+
* Supports modules: module-id/controller-id/action-id.
|
|
477
|
+
*/
|
|
478
|
+
async function findYii2Handlers(index, searchPath) {
|
|
479
|
+
const handlers = [];
|
|
480
|
+
const normalized = searchPath.replace(/^\/|\/$/g, "").toLowerCase();
|
|
481
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
482
|
+
if (segments.length === 0)
|
|
483
|
+
return handlers;
|
|
484
|
+
// Determine controller ID and action ID
|
|
485
|
+
// Patterns: "controller/action", "module/controller/action", "controller" (default action=index)
|
|
486
|
+
let controllerId;
|
|
487
|
+
let actionId;
|
|
488
|
+
if (segments.length === 1) {
|
|
489
|
+
controllerId = segments[0];
|
|
490
|
+
actionId = "index";
|
|
491
|
+
}
|
|
492
|
+
else if (segments.length === 2) {
|
|
493
|
+
controllerId = segments[0];
|
|
494
|
+
actionId = segments[1];
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
// Module routing: take last two segments as controller/action
|
|
498
|
+
controllerId = segments[segments.length - 2];
|
|
499
|
+
actionId = segments[segments.length - 1];
|
|
500
|
+
}
|
|
501
|
+
// Convert kebab-case to PascalCase for class name: "site" → "Site", "user-comment" → "UserComment"
|
|
502
|
+
const toPascal = (s) => s.split("-").map(p => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
503
|
+
// Convert kebab-case to camelCase for action method: "hello-world" → "HelloWorld"
|
|
504
|
+
const toCamelAction = (s) => s.split("-").map(p => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
505
|
+
const controllerName = toPascal(controllerId) + "Controller";
|
|
506
|
+
const actionMethod = "action" + toCamelAction(actionId);
|
|
507
|
+
// Find controller class in index
|
|
508
|
+
const controllerSymbol = index.symbols.find((s) => s.name === controllerName && s.kind === "class");
|
|
509
|
+
if (!controllerSymbol) {
|
|
510
|
+
// Fallback: try urlManager rules from config/web.php
|
|
511
|
+
return findYii2HandlersFromConfig(index, searchPath);
|
|
512
|
+
}
|
|
513
|
+
// Find action method within the controller
|
|
514
|
+
const actionSymbol = index.symbols.find((s) => s.name === actionMethod && s.parent === controllerSymbol.id);
|
|
515
|
+
if (actionSymbol) {
|
|
516
|
+
handlers.push({
|
|
517
|
+
symbol: stripSource(actionSymbol),
|
|
518
|
+
file: actionSymbol.file,
|
|
519
|
+
method: "GET",
|
|
520
|
+
framework: "yii2",
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
// Fallback: controller found but action method not indexed — report controller
|
|
525
|
+
handlers.push({
|
|
526
|
+
symbol: stripSource(controllerSymbol),
|
|
527
|
+
file: controllerSymbol.file,
|
|
528
|
+
framework: "yii2",
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
return handlers;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Fallback: parse Yii2 urlManager rules from config/web.php.
|
|
535
|
+
* Matches patterns like: 'GET api/users/<id>' => 'user/view'
|
|
536
|
+
*/
|
|
537
|
+
async function findYii2HandlersFromConfig(index, searchPath) {
|
|
538
|
+
const handlers = [];
|
|
539
|
+
const configFile = index.files.find((f) => /config\/web\.php$/.test(f.path));
|
|
540
|
+
if (!configFile)
|
|
541
|
+
return handlers;
|
|
542
|
+
const { readFile: rf } = await import("node:fs/promises");
|
|
543
|
+
const { join: j } = await import("node:path");
|
|
544
|
+
let source;
|
|
545
|
+
try {
|
|
546
|
+
source = await rf(j(index.root, configFile.path), "utf-8");
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
return handlers;
|
|
550
|
+
}
|
|
551
|
+
const normalized = searchPath.replace(/^\/|\/$/g, "").toLowerCase();
|
|
552
|
+
// Match: 'route/pattern' => 'controller/action' or ['GET method/pattern'] => 'controller/action'
|
|
553
|
+
const ruleRe = /['"](?:(?:GET|POST|PUT|DELETE|PATCH)\s+)?([^'"]+)['"]\s*=>\s*['"]([^'"]+)['"]/g;
|
|
554
|
+
let match;
|
|
555
|
+
while ((match = ruleRe.exec(source)) !== null) {
|
|
556
|
+
const rulePattern = match[1].replace(/<\w+(?::[^>]+)?>/g, "[param]").toLowerCase();
|
|
557
|
+
if (!matchPath(rulePattern, normalized))
|
|
558
|
+
continue;
|
|
559
|
+
const route = match[2]; // e.g. "user/view"
|
|
560
|
+
const parts = route.split("/");
|
|
561
|
+
if (parts.length < 2)
|
|
562
|
+
continue;
|
|
563
|
+
const controllerId = parts[parts.length - 2];
|
|
564
|
+
const actionId = parts[parts.length - 1];
|
|
565
|
+
const toPascal = (s) => s.split("-").map(p => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
566
|
+
const controllerName = toPascal(controllerId) + "Controller";
|
|
567
|
+
const actionMethod = "action" + toPascal(actionId);
|
|
568
|
+
const ctrlSym = index.symbols.find(s => s.name === controllerName && s.kind === "class");
|
|
569
|
+
if (!ctrlSym)
|
|
570
|
+
continue;
|
|
571
|
+
const actionSym = index.symbols.find(s => s.name === actionMethod && s.parent === ctrlSym.id);
|
|
572
|
+
handlers.push({
|
|
573
|
+
symbol: stripSource(actionSym ?? ctrlSym),
|
|
574
|
+
file: (actionSym ?? ctrlSym).file,
|
|
575
|
+
method: "GET",
|
|
576
|
+
framework: "yii2",
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
return handlers;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Find Laravel route handlers by scanning route files for Route::method() patterns.
|
|
583
|
+
*/
|
|
584
|
+
async function findLaravelHandlers(index, searchPath) {
|
|
585
|
+
const handlers = [];
|
|
586
|
+
const routeFiles = index.files.filter((f) => /routes\/(web|api)\.php$/.test(f.path));
|
|
587
|
+
if (routeFiles.length === 0)
|
|
588
|
+
return handlers;
|
|
589
|
+
const { readFile } = await import("node:fs/promises");
|
|
590
|
+
const { join } = await import("node:path");
|
|
591
|
+
const methods = ["get", "post", "put", "delete", "patch"];
|
|
592
|
+
for (const file of routeFiles) {
|
|
593
|
+
let source;
|
|
594
|
+
try {
|
|
595
|
+
source = await readFile(join(index.root, file.path), "utf-8");
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
for (const method of methods) {
|
|
601
|
+
// Match: Route::get('/path', [Controller::class, 'method']) or Route::get('/path', 'Controller@method')
|
|
602
|
+
const re = new RegExp(`Route::${method}\\s*\\(\\s*['"\`]([^'"\`]+)['"\`]\\s*,\\s*(?:\\[([\\w\\\\]+)::class\\s*,\\s*['"\`](\\w+)['"\`]\\]|['"\`](\\w+)@(\\w+)['"\`])`, "gi");
|
|
603
|
+
let match;
|
|
604
|
+
while ((match = re.exec(source)) !== null) {
|
|
605
|
+
const routePath = match[1] ?? "";
|
|
606
|
+
const controllerClass = match[2] ?? match[4] ?? "";
|
|
607
|
+
const methodName = match[3] ?? match[5] ?? "";
|
|
608
|
+
if (!matchPath(routePath, searchPath))
|
|
609
|
+
continue;
|
|
610
|
+
// Find the controller method in the index
|
|
611
|
+
const controllerName = controllerClass.split("\\").pop() ?? controllerClass;
|
|
612
|
+
const sym = index.symbols.find((s) => s.name === methodName && s.kind === "method" &&
|
|
613
|
+
index.symbols.some((c) => c.id === s.parent && c.name === controllerName));
|
|
614
|
+
handlers.push({
|
|
615
|
+
symbol: sym
|
|
616
|
+
? stripSource(sym)
|
|
617
|
+
: { id: `${controllerName}::${methodName}`, name: methodName, kind: "method", file: file.path, start_line: 0, end_line: 0 },
|
|
618
|
+
file: sym?.file ?? file.path,
|
|
619
|
+
method: method.toUpperCase(),
|
|
620
|
+
framework: "laravel",
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return handlers;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Find Ktor route handlers via `routing { get("/path") { ... } }` DSL.
|
|
629
|
+
* Supports nested `route("/prefix") { get("/sub") { } }` patterns.
|
|
630
|
+
*/
|
|
631
|
+
async function findKtorHandlers(index, searchPath) {
|
|
632
|
+
const handlers = [];
|
|
633
|
+
const methods = ["get", "post", "put", "delete", "patch", "head", "options"];
|
|
634
|
+
// Ktor handlers are in .kt files, typically in files containing "routing {" or with "Route" in name
|
|
635
|
+
const kotlinFiles = index.files.filter((f) => /\.kts?$/.test(f.path));
|
|
636
|
+
if (kotlinFiles.length === 0)
|
|
637
|
+
return handlers;
|
|
638
|
+
const { readFile } = await import("node:fs/promises");
|
|
639
|
+
const { join } = await import("node:path");
|
|
640
|
+
for (const file of kotlinFiles) {
|
|
641
|
+
let source;
|
|
642
|
+
try {
|
|
643
|
+
source = await readFile(join(index.root, file.path), "utf-8");
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
// Skip files without routing DSL
|
|
649
|
+
if (!/\b(routing|route)\s*[({]/.test(source))
|
|
650
|
+
continue;
|
|
651
|
+
// Extract route("/prefix") blocks to support nested prefixes
|
|
652
|
+
// Simple approach: find all method calls with path args, combine with enclosing route() prefix via line scan
|
|
653
|
+
const lines = source.split("\n");
|
|
654
|
+
const prefixStack = [];
|
|
655
|
+
let braceDepth = 0;
|
|
656
|
+
for (let i = 0; i < lines.length; i++) {
|
|
657
|
+
const line = lines[i];
|
|
658
|
+
// Track route("/prefix") { ... } blocks
|
|
659
|
+
const routeMatch = /\broute\s*\(\s*["']([^"']+)["']\s*\)\s*\{/.exec(line);
|
|
660
|
+
if (routeMatch) {
|
|
661
|
+
prefixStack.push({ prefix: routeMatch[1], braceDepth });
|
|
662
|
+
}
|
|
663
|
+
// Count braces to detect when route() scope closes
|
|
664
|
+
for (const ch of line) {
|
|
665
|
+
if (ch === "{")
|
|
666
|
+
braceDepth++;
|
|
667
|
+
else if (ch === "}") {
|
|
668
|
+
braceDepth--;
|
|
669
|
+
// Pop route prefixes whose scope ended
|
|
670
|
+
while (prefixStack.length > 0 &&
|
|
671
|
+
prefixStack[prefixStack.length - 1].braceDepth >= braceDepth) {
|
|
672
|
+
prefixStack.pop();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// Match method handlers: get("/path") { ... } or post("/path") { ... }
|
|
677
|
+
for (const method of methods) {
|
|
678
|
+
const re = new RegExp(`\\b${method}\\s*\\(\\s*["']([^"']+)["']\\s*\\)\\s*\\{`);
|
|
679
|
+
const match = re.exec(line);
|
|
680
|
+
if (!match)
|
|
681
|
+
continue;
|
|
682
|
+
const methodPath = match[1];
|
|
683
|
+
const prefix = prefixStack.map((p) => p.prefix).join("");
|
|
684
|
+
const fullPath = `${prefix}/${methodPath}`.replace(/\/+/g, "/");
|
|
685
|
+
if (!matchPath(fullPath, searchPath))
|
|
686
|
+
continue;
|
|
687
|
+
// Find enclosing function symbol (if any) for this line
|
|
688
|
+
const lineNum = i + 1;
|
|
689
|
+
const sym = index.symbols.find((s) => s.file === file.path && s.start_line <= lineNum && s.end_line >= lineNum);
|
|
690
|
+
handlers.push({
|
|
691
|
+
symbol: sym
|
|
692
|
+
? stripSource(sym)
|
|
693
|
+
: {
|
|
694
|
+
id: `${file.path}:${method}:${methodPath}`,
|
|
695
|
+
name: `${method} ${methodPath}`,
|
|
696
|
+
kind: "function",
|
|
697
|
+
file: file.path,
|
|
698
|
+
start_line: lineNum,
|
|
699
|
+
end_line: lineNum,
|
|
700
|
+
},
|
|
701
|
+
file: file.path,
|
|
702
|
+
method: method.toUpperCase(),
|
|
703
|
+
framework: "ktor",
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return handlers;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Find Spring Boot Kotlin route handlers via @RestController/@Controller + @GetMapping/etc.
|
|
712
|
+
*/
|
|
713
|
+
async function findSpringBootKotlinHandlers(index, searchPath) {
|
|
714
|
+
const handlers = [];
|
|
715
|
+
const mappingAnnotations = [
|
|
716
|
+
{ ann: "GetMapping", method: "GET" },
|
|
717
|
+
{ ann: "PostMapping", method: "POST" },
|
|
718
|
+
{ ann: "PutMapping", method: "PUT" },
|
|
719
|
+
{ ann: "DeleteMapping", method: "DELETE" },
|
|
720
|
+
{ ann: "PatchMapping", method: "PATCH" },
|
|
721
|
+
];
|
|
722
|
+
const kotlinFiles = index.files.filter((f) => /\.kts?$/.test(f.path));
|
|
723
|
+
if (kotlinFiles.length === 0)
|
|
724
|
+
return handlers;
|
|
725
|
+
const { readFile } = await import("node:fs/promises");
|
|
726
|
+
const { join } = await import("node:path");
|
|
727
|
+
for (const file of kotlinFiles) {
|
|
728
|
+
let source;
|
|
729
|
+
try {
|
|
730
|
+
source = await readFile(join(index.root, file.path), "utf-8");
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
// Must have @RestController or @Controller annotation
|
|
736
|
+
if (!/@(?:RestController|Controller)\b/.test(source))
|
|
737
|
+
continue;
|
|
738
|
+
// Extract class-level @RequestMapping prefix (optional)
|
|
739
|
+
const classRequestMatch = /@RequestMapping\s*\(\s*(?:value\s*=\s*)?["']([^"']*)["']/.exec(source);
|
|
740
|
+
const classPrefix = classRequestMatch?.[1] ?? "";
|
|
741
|
+
for (const { ann, method } of mappingAnnotations) {
|
|
742
|
+
// Match: @GetMapping("/path") fun funcName(...)
|
|
743
|
+
// Or: @GetMapping(value = "/path") fun funcName(...)
|
|
744
|
+
const re = new RegExp(`@${ann}\\s*\\(\\s*(?:value\\s*=\\s*)?["']([^"']*)["'](?:[^)]*)?\\)\\s*(?:fun|\\n\\s*fun)\\s+(\\w+)`, "g");
|
|
745
|
+
let match;
|
|
746
|
+
while ((match = re.exec(source)) !== null) {
|
|
747
|
+
const routePath = match[1] ?? "";
|
|
748
|
+
const funcName = match[2] ?? "";
|
|
749
|
+
const fullPath = `${classPrefix}/${routePath}`.replace(/\/+/g, "/");
|
|
750
|
+
if (!matchPath(fullPath, searchPath))
|
|
751
|
+
continue;
|
|
752
|
+
const sym = index.symbols.find((s) => s.file === file.path && s.name === funcName);
|
|
753
|
+
handlers.push({
|
|
754
|
+
symbol: sym
|
|
755
|
+
? stripSource(sym)
|
|
756
|
+
: {
|
|
757
|
+
id: `${file.path}:${funcName}`,
|
|
758
|
+
name: funcName,
|
|
759
|
+
kind: "method",
|
|
760
|
+
file: file.path,
|
|
761
|
+
start_line: 1,
|
|
762
|
+
end_line: 1,
|
|
763
|
+
},
|
|
764
|
+
file: file.path,
|
|
765
|
+
method,
|
|
766
|
+
framework: "spring-kotlin",
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return handlers;
|
|
772
|
+
}
|
|
773
|
+
// --- Python frameworks ---
|
|
774
|
+
/**
|
|
775
|
+
* Detect Python test files by pytest naming conventions.
|
|
776
|
+
* Matches: test_*.py, *_test.py, conftest.py, and tests/ subdirectories.
|
|
777
|
+
*/
|
|
778
|
+
function isPythonTestFile(path) {
|
|
779
|
+
const basename = path.split("/").pop() ?? path;
|
|
780
|
+
if (basename === "conftest.py")
|
|
781
|
+
return true;
|
|
782
|
+
if (/^test_.*\.py$/.test(basename))
|
|
783
|
+
return true;
|
|
784
|
+
if (/_test\.py$/.test(basename))
|
|
785
|
+
return true;
|
|
786
|
+
if (/\/tests?\//.test(path))
|
|
787
|
+
return true;
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Find Flask route handlers via @app.route() and @bp.route() decorators.
|
|
792
|
+
* Also handles @app.get/post/put/delete() (Flask 2.0+ shorthand).
|
|
793
|
+
*/
|
|
794
|
+
function findFlaskHandlers(index, searchPath) {
|
|
795
|
+
const handlers = [];
|
|
796
|
+
// Exclude test files and conftest — users tracing a production route rarely
|
|
797
|
+
// want test fixture routes like `@app.route('/')` inside test_*.py
|
|
798
|
+
const pyFiles = index.files.filter((f) => f.path.endsWith(".py")
|
|
799
|
+
&& !isPythonTestFile(f.path));
|
|
800
|
+
for (const file of pyFiles) {
|
|
801
|
+
const fileSymbols = index.symbols.filter((s) => s.file === file.path);
|
|
802
|
+
for (const sym of fileSymbols) {
|
|
803
|
+
if (!sym.decorators || sym.decorators.length === 0)
|
|
804
|
+
continue;
|
|
805
|
+
for (const dec of sym.decorators) {
|
|
806
|
+
// Match @app.route('/path') or @bp.route('/path') — Flask-specific.
|
|
807
|
+
// For HTTP verb shortcuts (@app.get, @app.post), prefer FastAPI handler
|
|
808
|
+
// since the syntax is ambiguous between Flask 2.0+ and FastAPI.
|
|
809
|
+
const routeMatch = dec.match(/@\w+\.route\s*\(\s*['"]([^'"]*)['"]/);
|
|
810
|
+
if (!routeMatch)
|
|
811
|
+
continue;
|
|
812
|
+
const routePath = routeMatch[1] ?? "";
|
|
813
|
+
if (!matchPath(routePath, searchPath))
|
|
814
|
+
continue;
|
|
815
|
+
// @app.route can handle any method — omit method field
|
|
816
|
+
handlers.push({
|
|
817
|
+
symbol: stripSource(sym),
|
|
818
|
+
file: file.path,
|
|
819
|
+
framework: "flask",
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return handlers;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Find FastAPI route handlers via @app.get/post/put/delete() and @router.get() decorators.
|
|
828
|
+
* Handles APIRouter prefix extraction.
|
|
829
|
+
*/
|
|
830
|
+
function findFastAPIHandlers(index, searchPath) {
|
|
831
|
+
const handlers = [];
|
|
832
|
+
const pyFiles = index.files.filter((f) => f.path.endsWith(".py")
|
|
833
|
+
&& !isPythonTestFile(f.path));
|
|
834
|
+
for (const file of pyFiles) {
|
|
835
|
+
const fileSymbols = index.symbols.filter((s) => s.file === file.path);
|
|
836
|
+
for (const sym of fileSymbols) {
|
|
837
|
+
if (!sym.decorators || sym.decorators.length === 0)
|
|
838
|
+
continue;
|
|
839
|
+
for (const dec of sym.decorators) {
|
|
840
|
+
// Match @app.get('/path') or @router.get('/path') etc.
|
|
841
|
+
const routeMatch = dec.match(/@\w+\.(get|post|put|delete|patch|options|head)\s*\(\s*['"]([^'"]*)['"]/);
|
|
842
|
+
if (!routeMatch)
|
|
843
|
+
continue;
|
|
844
|
+
const method = routeMatch[1].toUpperCase();
|
|
845
|
+
const routePath = routeMatch[2] ?? "";
|
|
846
|
+
if (!matchPath(routePath, searchPath))
|
|
847
|
+
continue;
|
|
848
|
+
handlers.push({
|
|
849
|
+
symbol: stripSource(sym),
|
|
850
|
+
file: file.path,
|
|
851
|
+
method,
|
|
852
|
+
framework: "fastapi",
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return handlers;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Find Django route handlers by parsing urlpatterns in urls.py files.
|
|
861
|
+
* Handles path(), re_path(), and include() chains.
|
|
862
|
+
*/
|
|
863
|
+
async function findDjangoHandlers(index, searchPath) {
|
|
864
|
+
const handlers = [];
|
|
865
|
+
const { readFile } = await import("node:fs/promises");
|
|
866
|
+
const { join } = await import("node:path");
|
|
867
|
+
// Find urls.py files
|
|
868
|
+
const urlFiles = index.files.filter((f) => f.path.endsWith("urls.py"));
|
|
869
|
+
for (const file of urlFiles) {
|
|
870
|
+
let source;
|
|
871
|
+
try {
|
|
872
|
+
source = await readFile(join(index.root, file.path), "utf-8");
|
|
873
|
+
}
|
|
874
|
+
catch {
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
// Extract path() patterns: path('users/', views.user_list, name='...')
|
|
878
|
+
// and path('users/<int:pk>/', views.user_detail)
|
|
879
|
+
const pathRe = /path\s*\(\s*['"]([^'"]*)['"]\s*,\s*([\w.]+)/g;
|
|
880
|
+
let match;
|
|
881
|
+
// Get the URL prefix from the file's directory context
|
|
882
|
+
// e.g., if this urls.py is included from a parent with prefix 'api/'
|
|
883
|
+
while ((match = pathRe.exec(source)) !== null) {
|
|
884
|
+
const routePath = match[1] ?? "";
|
|
885
|
+
const viewRef = match[2] ?? "";
|
|
886
|
+
// Skip include() references
|
|
887
|
+
if (viewRef === "include")
|
|
888
|
+
continue;
|
|
889
|
+
// Convert Django <type:name> to :name for matchPath
|
|
890
|
+
const normalizedPath = `/${routePath}`.replace(/<\w+:(\w+)>/g, ":$1").replace(/\/+/g, "/");
|
|
891
|
+
if (!matchPath(normalizedPath, searchPath))
|
|
892
|
+
continue;
|
|
893
|
+
// Resolve view reference to a symbol
|
|
894
|
+
const viewName = viewRef.split(".").pop() ?? viewRef;
|
|
895
|
+
const sym = index.symbols.find((s) => s.name === viewName && s.file.endsWith(".py"));
|
|
896
|
+
handlers.push({
|
|
897
|
+
symbol: sym
|
|
898
|
+
? stripSource(sym)
|
|
899
|
+
: {
|
|
900
|
+
id: `${file.path}:${viewName}`,
|
|
901
|
+
name: viewName,
|
|
902
|
+
kind: "function",
|
|
903
|
+
file: file.path,
|
|
904
|
+
start_line: 1,
|
|
905
|
+
end_line: 1,
|
|
906
|
+
},
|
|
907
|
+
file: sym?.file ?? file.path,
|
|
908
|
+
framework: "django",
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return handlers;
|
|
913
|
+
}
|
|
229
914
|
/**
|
|
230
915
|
* Trace an HTTP route: find handler, trace callees, identify DB calls.
|
|
231
916
|
*/
|
|
@@ -234,10 +919,21 @@ export async function traceRoute(repo, path, outputFormat) {
|
|
|
234
919
|
if (!index)
|
|
235
920
|
throw new Error(`Repository "${repo}" not found.`);
|
|
236
921
|
// Try all frameworks
|
|
922
|
+
const astroHandlers = findAstroHandlers(index, path);
|
|
237
923
|
const handlers = [
|
|
238
924
|
...(await findNestJSHandlers(index, path)),
|
|
239
925
|
...findNextJSHandlers(index, path),
|
|
926
|
+
...findPagesRouterHandlers(index, path),
|
|
240
927
|
...findExpressHandlers(index, path),
|
|
928
|
+
...(await findHonoHandlers(repo, index, path)),
|
|
929
|
+
...(await findYii2Handlers(index, path)),
|
|
930
|
+
...(await findLaravelHandlers(index, path)),
|
|
931
|
+
...(await findKtorHandlers(index, path)),
|
|
932
|
+
...(await findSpringBootKotlinHandlers(index, path)),
|
|
933
|
+
...astroHandlers,
|
|
934
|
+
...findFastAPIHandlers(index, path),
|
|
935
|
+
...findFlaskHandlers(index, path),
|
|
936
|
+
...(await findDjangoHandlers(index, path)),
|
|
241
937
|
];
|
|
242
938
|
if (handlers.length === 0) {
|
|
243
939
|
return { path, handlers: [], call_chain: [], db_calls: [] };
|
|
@@ -268,6 +964,36 @@ export async function traceRoute(repo, path, outputFormat) {
|
|
|
268
964
|
}
|
|
269
965
|
const dbCalls = findDbCalls(allCalleeSymbols);
|
|
270
966
|
const result = { path, handlers, call_chain: callChain, db_calls: dbCalls };
|
|
967
|
+
// Next.js-specific: layout chain, middleware, and server actions tracing
|
|
968
|
+
const hasNextjsHandler = handlers.some((h) => h.framework === "nextjs");
|
|
969
|
+
if (hasNextjsHandler) {
|
|
970
|
+
const repoRoot = index.root;
|
|
971
|
+
// Layout chain from the first handler's file
|
|
972
|
+
const firstFile = handlers[0]?.file;
|
|
973
|
+
if (firstFile) {
|
|
974
|
+
try {
|
|
975
|
+
result.layout_chain = await computeLayoutChain(firstFile, repoRoot);
|
|
976
|
+
}
|
|
977
|
+
catch {
|
|
978
|
+
result.layout_chain = [];
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
else {
|
|
982
|
+
result.layout_chain = [];
|
|
983
|
+
}
|
|
984
|
+
// Middleware tracing
|
|
985
|
+
try {
|
|
986
|
+
const mw = await traceMiddleware(repoRoot, path);
|
|
987
|
+
if (mw) {
|
|
988
|
+
result.middleware = mw;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
// Middleware tracing failed — skip
|
|
993
|
+
}
|
|
994
|
+
// Server actions detection
|
|
995
|
+
result.server_actions = await findServerActions(repoRoot, allCalleeSymbols, callChain);
|
|
996
|
+
}
|
|
271
997
|
if (outputFormat === "mermaid") {
|
|
272
998
|
return { mermaid: routeToMermaid(result) };
|
|
273
999
|
}
|