feat: discover gRPC and Connect services - #14
Conversation
Add an orthogonal transport axis to EndpointInfo (http | grpc | grpc_web | connect) plus optional gRPC metadata (serviceFqn, method, streamingType), so a gRPC method stays kind:"api" and is distinguished by how it's reached. - New proto-first grpc extractor: parses .proto service/rpc definitions into one endpoint per rpc with wire path /pkg.Service/Method and streaming type. Language-agnostic; auto-detects Connect toolchains and tags transport connect. String-aware comment/brace handling so option-string paths don't corrupt parsing. - NestJS @GrpcMethod/@GrpcStreamMethod for code-first gRPC, deferring to the proto extractor when a .proto is present. - Mapper dedup key includes transport so gRPC and HTTP routes sharing (method, path) don't collapse. - Thread transport/grpc through the endpoint() helper and all formatters. Closes #13.
connect is a valid FrameworkId but no extractor registered under that id, so an explicit --framework connect override looked up nothing and skipped gRPC discovery. Alias the connect id to the grpc extractor (which picks the grpc/connect variant from the toolchain).
… workspaces - NestJS @GrpcMethod scanning no longer disabled by a repo-wide .proto check. The extractor always emits decorator endpoints; the mapper prefers a package-qualified proto endpoint over a bare decorator duplicate for the same service+method. Fixes: --framework nestjs on a proto repo emitting nothing, and monorepo packages with code-first gRPC being hidden by an unrelated proto. - usesConnect scans buf/connect dep files tree-wide instead of only at the repo root, so Connect toolchains declared in nested workspace packages are detected.
…n formatter - Re-export EndpointTransport, GrpcStreaming, and GrpcMeta from the public API so consumers can annotate against the new EndpointInfo fields, matching the existing pattern for HttpMethod/EndpointKind/ParamInfo. - Rename the per-row `label` in the markdown formatter to `methodLabel` so it no longer shadows the section-header `label`.
…proto - Move the bare-decorator vs package-qualified gRPC dedup from map() into mapRaw() so impact() (which builds from mapRaw) no longer double-counts gRPC methods that have both a .proto and an @GrpcMethod decorator. - Decide connect vs plain gRPC per proto by walking up to the nearest package with a Connect/Buf toolchain (memoized per directory), so a Connect service in one package no longer mislabels vanilla gRPC protos elsewhere in a monorepo. Also covers Connect config in nested workspace packages.
…rt tests to vitest Rebased onto feat/grpc-discovery (#14, stacked). Integrate #14's code-first gRPC detection into #9's class-aware nestjs extractor: - @GrpcMethod/@GrpcStreamMethod handling is now a third match loop inside the main per-file loop, reusing findClasses/classAt so the owning class name is resolved consistently (correct even in multi-controller files). Broaden the file filter to include @grpc so gRPC-only controllers are scanned. - gRPC endpoints keep kind:"api" with transport:"grpc"; server/bidi streaming is modeled via grpc.streamingType, not a websocket kind. Documented the decision in grpc.ts. - Port #14's grpc.test.ts and nestjs gRPC tests from bun:test to vitest; move grpc/nestjs-grpc fixtures under src/extractors/__fixtures__/. No bun:test references remain.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Wrong alias dedup key
- Scoped the bare-gRPC-alias dedup key by the endpoint's resolved service so a package-qualified proto RPC in an unrelated monorepo service no longer suppresses a code-first decorator endpoint.
You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit e83540a. Configure here.
…rt tests to vitest Rebased onto feat/grpc-discovery (#14, stacked). Integrate #14's code-first gRPC detection into #9's class-aware nestjs extractor: - @GrpcMethod/@GrpcStreamMethod handling is now a third match loop inside the main per-file loop, reusing findClasses/classAt so the owning class name is resolved consistently (correct even in multi-controller files). Broaden the file filter to include @grpc so gRPC-only controllers are scanned. - gRPC endpoints keep kind:"api" with transport:"grpc"; server/bidi streaming is modeled via grpc.streamingType, not a websocket kind. Documented the decision in grpc.ts. - Port #14's grpc.test.ts and nestjs gRPC tests from bun:test to vitest; move grpc/nestjs-grpc fixtures under src/extractors/__fixtures__/. No bun:test references remain.
…ths; owning-class service - Remove the short-name+method alias dedup: it could drop an unrelated code-first `/Service/Method` handler when any package-qualified proto shared the same unqualified service name and method. gRPC endpoints now only collapse when they resolve to the SAME wire path. - On a genuine identical-path collision, prefer the proto/connect definition over a framework decorator (canonical, package-qualified source), instead of keeping whichever extractor ran first. - Resolve the @GrpcMethod fallback service name from the class that actually owns the decorator, not the file's first class.
987b6cf to
b14fcc4
Compare
|
Note: I replaced the Bugbot Autofix commit ( |
…rt tests to vitest Rebased onto feat/grpc-discovery (#14, stacked). Integrate #14's code-first gRPC detection into #9's class-aware nestjs extractor: - @GrpcMethod/@GrpcStreamMethod handling is now a third match loop inside the main per-file loop, reusing findClasses/classAt so the owning class name is resolved consistently (correct even in multi-controller files). Broaden the file filter to include @grpc so gRPC-only controllers are scanned. - gRPC endpoints keep kind:"api" with transport:"grpc"; server/bidi streaming is modeled via grpc.streamingType, not a websocket kind. Documented the decision in grpc.ts. - Port #14's grpc.test.ts and nestjs gRPC tests from bun:test to vitest; move grpc/nestjs-grpc fixtures under src/extractors/__fixtures__/. No bun:test references remain.
Rebased onto feat/grpc-discovery @ b14fcc4, which removed the buggy short-name dropBareGrpcAliases in favor of an identical-wire-path collapse (proto wins via isProtoGrpc). Reconcile #9's kind-detection layer with the new base: - adopt b14fcc4's nestjs-grpc-proto fixture (package-qualified hero.HeroesService decorators + a BillingController @GrpcMethod() no-arg case). - replace the stale 'proto wins over bare decorator' / 'dedup lives in mapRaw' tests with b14fcc4's 'proto tiebreak + owning-class service resolution' cases (ported to vitest); drop the now-unused mapRaw import. The nestjs extractor already resolves the @GrpcMethod fallback service from the owning class via classAt, matching #14.
…rt tests to vitest Rebased onto feat/grpc-discovery (#14, stacked). Integrate #14's code-first gRPC detection into #9's class-aware nestjs extractor: - @GrpcMethod/@GrpcStreamMethod handling is now a third match loop inside the main per-file loop, reusing findClasses/classAt so the owning class name is resolved consistently (correct even in multi-controller files). Broaden the file filter to include @grpc so gRPC-only controllers are scanned. - gRPC endpoints keep kind:"api" with transport:"grpc"; server/bidi streaming is modeled via grpc.streamingType, not a websocket kind. Documented the decision in grpc.ts. - Port #14's grpc.test.ts and nestjs gRPC tests from bun:test to vitest; move grpc/nestjs-grpc fixtures under src/extractors/__fixtures__/. No bun:test references remain.
Rebased onto feat/grpc-discovery @ b14fcc4, which removed the buggy short-name dropBareGrpcAliases in favor of an identical-wire-path collapse (proto wins via isProtoGrpc). Reconcile #9's kind-detection layer with the new base: - adopt b14fcc4's nestjs-grpc-proto fixture (package-qualified hero.HeroesService decorators + a BillingController @GrpcMethod() no-arg case). - replace the stale 'proto wins over bare decorator' / 'dedup lives in mapRaw' tests with b14fcc4's 'proto tiebreak + owning-class service resolution' cases (ported to vitest); drop the now-unused mapRaw import. The nestjs extractor already resolves the @GrpcMethod fallback service from the owning class via classAt, matching #14.
* test: add vitest + reference fastapi extractor test
Sets up vitest as the test runner and adds the first per-extractor
test using the FastAPI fixture (which already emits kind: "websocket"
for @app.websocket routes). Establishes the fixture-driven pattern that
subsequent per-framework tests will follow.
* feat(django): detect page (TemplateView/render) and websocket (Channels) kinds
Class-based views deriving from TemplateView/ListView/DetailView and FBVs
calling render() are now emitted as kind: "page". websocket_urlpatterns
entries (Django Channels) emit kind: "websocket". Strips .as_view/.as_asgi
from handler names so view classes can be looked up by name.
* feat(express): detect page (res.render/sendFile) and websocket kinds
Replaced the lazy single-line route regex with a balanced-paren walker so
multi-line arrow handler bodies can be inspected. Page detection looks for
res.render(...) or res.sendFile(...) in the handler body. Websocket
detection covers app.ws() (express-ws), io.on('connection')/io.of()
(socket.io), and new WebSocketServer (ws library).
* feat(fastapi): detect page kind for HTMLResponse / TemplateResponse
A route is page-shaped when its decorator args set response_class=HTMLResponse
or its function body returns an HTMLResponse(...) or .TemplateResponse(...).
Body window is bounded to the next top-level decorator/def to avoid bleeding
between adjacent routes.
* feat(flask): detect page kind for handlers using render_template
A handler whose body contains render_template() or render_template_string()
emits kind: "page". Body window is bounded to the next top-level def/class/
decorator at column 0 so adjacent handlers do not leak into each other's
classification.
* feat(go): detect websocket kind for gorilla/websocket upgrades
A route handler emits kind: "websocket" when its file imports
github.com/gorilla/websocket and the handler body contains an .Upgrade(
call. Applies uniformly across gin, echo, fiber, and net/http via a shared
findWebsocketHandlers helper. Also tightened the handler-name regex so
trailing handler args resolve correctly.
* feat(laravel): detect page kind for routes/web.php
Routes defined in routes/web.php emit kind: "page" (session/CSRF/Blade
view convention); routes from routes/api.php remain kind: "api". The
file-of-origin distinction is reused from the existing /api prefix logic.
* feat(nestjs): detect page (@Render) and websocket (@WebSocketGateway) kinds
Methods carrying @Render('view') emit kind: "page". Classes annotated
with @WebSocketGateway() emit each @SubscribeMessage('event') method as a
websocket endpoint with method WS. Methods are now associated with their
owning class via a per-class scan so controller vs gateway is disambiguated.
* feat(rails): detect page vs api by controller superclass
Routes are classified by walking the controller's inheritance chain on
disk: ActionController::Base ancestor → kind: "page"; ActionController::API
ancestor → kind: "api". Resolution memoizes per file with a cycle guard.
Also fixes a pre-existing bug where namespace blocks leaked across the
whole routes.rb file — namespaces are now tracked block-by-block.
* feat(spring): detect page (@controller) and websocket (@MessageMapping) kinds
Methods on a class annotated with @controller (without @RestController) emit
kind: "page"; @RestController stays kind: "api". @MessageMapping and
@SubscribeMapping methods emit a separate websocket endpoint regardless of
class kind. Class context is resolved via a back-scan from each mapping
match, so inner classes and multi-class files are handled.
* chore: exclude __fixtures__ from tsc and eslint
Fixture directories under src/extractors/__fixtures__/ contain synthetic
test inputs that mimic external frameworks (e.g. @nestjs/common imports
without the package being installed). They're scanned as raw source text
by the extractors, never compiled or imported. Excluding them from both
the TypeScript program and the ESLint config keeps tooling clean without
needing per-file @ts-nocheck escape hatches.
* test(nextjs): port nextjs.test.ts from bun:test to vitest
The rest of the test suite uses vitest (introduced for the per-extractor
fixture tests). Unify on one runner so 'bun run test' covers all suites.
Replaces import.meta.dir (bun-only) with the standard ESM
fileURLToPath(import.meta.url) pattern.
* chore: apply prettier formatting to nestjs and rails extractors
* fix(nestjs): scope @Render detection to the current method's decorator stack
The previous heuristic checked a 400-char backward window and a 400-char
forward window for @Render. In a controller with multiple methods, that
easily included @Render decorators belonging to a sibling method, so a
plain @get sitting near a @Render-decorated handler was misclassified as
a page.
Replace both windows with a precise line-by-line walk that captures only
the contiguous decorator stack of the current method — stopping at any
line that ends a previous statement (}, ;) or otherwise looks like code.
Add a regression test where a @get("/data") sibling sits directly below
a @Render-decorated method and asserts its kind is "api".
* fix(nestjs,rails): scope class decorators per-class; order rails end/do correctly
Address Cursor Bugbot review on #9:
- nestjs findClasses: attribute only the immediately-preceding decorator
block to each class, so a @Controller/@WebSocketGateway in an earlier
class in the same file no longer bleeds onto later classes. Add a
multi-controller regression fixture + test.
- rails: process 'end' (close + namespace pop) before 'do' (open) so a
combined 'end; scope do' line pops namespaces at the correct depth.
* feat(nestjs,grpc): integrate #14 gRPC into restructured extractor; port tests to vitest
Rebased onto feat/grpc-discovery (#14, stacked). Integrate #14's code-first
gRPC detection into #9's class-aware nestjs extractor:
- @GrpcMethod/@GrpcStreamMethod handling is now a third match loop inside the
main per-file loop, reusing findClasses/classAt so the owning class name is
resolved consistently (correct even in multi-controller files). Broaden the
file filter to include @grpc so gRPC-only controllers are scanned.
- gRPC endpoints keep kind:"api" with transport:"grpc"; server/bidi
streaming is modeled via grpc.streamingType, not a websocket kind. Documented
the decision in grpc.ts.
- Port #14's grpc.test.ts and nestjs gRPC tests from bun:test to vitest; move
grpc/nestjs-grpc fixtures under src/extractors/__fixtures__/. No bun:test
references remain.
* test(django): lock in kind classification for re_path/string-prefixed routes
Phase 2 kind-coverage audit: confirm #12's re_path()/raw-string routes flow
through the same page-vs-api classification as plain path() routes (no undefined
kind). Audited all extractors — nextjs already assigns page/api correctly (and
is tested), server-actions emits kind:action, grpc/connect stays api with
transport+streamingType, and actix/sst/openapi emit only REST/spec routes so the
api default is correct.
* chore(grpc): reconcile with grpc-discovery per-proto Connect refactor
Rebased onto the updated feat/grpc-discovery (per-proto Connect scoping + gRPC
dedup moved into mapRaw). Preserve its new tests under the vitest port:
- move grpc-mixed monorepo fixtures under src/extractors/__fixtures__/ and keep
the connect-vs-plain-per-package test (as vitest it()).
- re-add the 'dedup lives in mapRaw so impact() sees it too' nestjs test.
* fix(django,go): harden kind heuristics per Bugbot review
Address Cursor Bugbot findings on #9's kind-detection heuristics:
- django: resolve CBV page detection through the full inheritance chain so a
view that extends a template base indirectly (HomeView -> SiteBaseView ->
TemplateView) is still a page.
- django: scope the view/function registry per app directory (with a global
fallback) so two apps defining the same view name don't clobber each other's
page-vs-api kind.
- django: capture single-line function-view bodies (def view(request): return
render(...)) and an optional -> ReturnType annotation, so they aren't
misclassified as api.
- go: scope websocket-handler names per directory (package) so an ordinary HTTP
route isn't mislabeled websocket just because an unrelated package defines a
same-named .Upgrade()-ing handler.
Add regression fixtures + vitest cases for each.
* chore(grpc): align #9 gRPC tests/fixture with #14 b14fcc4 dedup rework
Rebased onto feat/grpc-discovery @ b14fcc4, which removed the buggy short-name
dropBareGrpcAliases in favor of an identical-wire-path collapse (proto wins via
isProtoGrpc). Reconcile #9's kind-detection layer with the new base:
- adopt b14fcc4's nestjs-grpc-proto fixture (package-qualified hero.HeroesService
decorators + a BillingController @GrpcMethod() no-arg case).
- replace the stale 'proto wins over bare decorator' / 'dedup lives in mapRaw'
tests with b14fcc4's 'proto tiebreak + owning-class service resolution' cases
(ported to vitest); drop the now-unused mapRaw import.
The nestjs extractor already resolves the @GrpcMethod fallback service from the
owning class via classAt, matching #14.
* fix(django): index async def views in the kind registry
Address Cursor Bugbot finding: the function-view regex only matched sync `def`,
so `async def` template views were never indexed (misclassified api) and a
following async view could be absorbed into the preceding sync view's body
(bleeding its render() and mismarking a JSON route as page). funcRe now matches
an optional `async` prefix and treats `async def` as a body terminator. Added
a regression fixture (async_page + a sync json_only right before it) + test.
* fix(django,express): nested view packages + genuine socket.io namespaces
Address Cursor Bugbot round 3:
- django: resolve the page/api registry within the whole app subtree (the dir
containing urls.py), not just the immediate parent, so views in a nested
package (myapp/views/*.py) still resolve to their owning app and same-named
views across apps keep the right kind. Add a blogapp fixture with a views/
package colliding by name with an otherapp APIView.
- express: only treat .of()/.on("connection") as socket.io when the file
actually imports socket.io AND the call is on a confirmed server instance
(new Server(...), require("socket.io")(...), or a factory binding). Unrelated
.of() chains are no longer emitted as websocket endpoints. Add a fixture with
a genuine io.of("/rooms") plus an unrelated registry.of("/plugins").
Also format two fixture package.json files that CI's prettier --check flagged.
* fix(nestjs,django,go): mask comments in class scan; make dirOf cross-platform
Address Cursor Bugbot round 5 on the rebased-onto-main head:
- nestjs: findClasses now detects class boundaries on a comment-masked copy of
the file (offsets preserved), so a `class` token inside a // or /* */ comment
no longer creates a phantom class that steals later handlers and drops the
@controller prefix. Add a fixture with `// class Helper` inside a controller.
- django & go: dirOf now normalizes \\ to / and guards the no-separator case, so
per-app view scoping and per-package websocket scoping work on Windows scan
paths (which use \\) instead of corrupting the path via lastIndexOf('/')=-1.
---------
Co-authored-by: Test <test@pensar.dev>

Adds gRPC/Connect discovery to surface. Closes #13.
What
transportaxis onEndpointInfo(http | grpc | grpc_web | connect, absent ⇒ http) plus optionalgrpcmetadata (serviceFqn,method,streamingType). Orthogonal tokind, so a gRPC method is stillkind: "api". Threaded through theendpoint()helper and the JSON/NDJSON/table/markdown formatters (gRPC rows badge asGRPC/CONNECT).grpcproto-first extractor — parses.protoservice/rpcdefinitions into one endpoint per rpc with wire path/pkg.Service/Method, streaming type (unary/server/client/bidi), and service/method metadata. Language-agnostic. Auto-detects Connect toolchains (buf/connect deps) and tagsconnect.@GrpcMethod/@GrpcStreamMethodsupport for code-first gRPC (methods in decorators, no.proto), guarded to defer to the proto extractor when a.protoexists.transportso a gRPC method and an HTTP route sharing a(method, path)don't collapse.The
.protoparser is string-aware — comments and braces inside option-string paths (e.g.option (google.api.http) = { post: "/v1/{parent=shops/*}/orders" }) don't corrupt parsing, and rpcs are associated to services by position rather than brace-matching.Testing
Unit tests + fixtures (12 new): all four streaming types, multi-service files, commented-out rpcs, the option-string brace/slash trap (as a regression guard), Connect detection, and NestJS code-first + defer-to-proto.
Beyond fixtures, validated against a spread of real open-source repos, comparing extracted method counts against the raw
.protorpc counts:@GrpcService)connecttransport; matches non-testdataStreaming direction verified on real streaming APIs (e.g. Speech
StreamingRecognize→ bidi, SpannerExecuteStreamingSql→ server_stream, Pub/SubStreamingPull→ bidi). Iterating on googleapis is what surfaced and fixed the option-string parsing bug noted above.tsc/eslint/prettierclean; full suite (39 tests) green.Note
Medium Risk
Touches core mapping/dedup and adds a regex-based proto parser; incorrect parsing or dedup could drop or mislabel endpoints, but behavior is covered by fixtures and real-repo validation described in the PR.
Overview
Adds gRPC and Connect endpoint discovery alongside existing HTTP scanning, with a new
transportdimension on endpoints (grpc,connect, etc.) and optionalgrpcmetadata (service FQN, method, streaming type). JSON, NDJSON, table, and markdown output now surfacetransport/grpcand label non-HTTP rows as GRPC or CONNECT.A new proto-first
grpcextractor turns each.protorpcinto an endpoint at wire path/package.Service/Method, classifies unary/server/client/bidi streaming, and picks Connect vs plain gRPC from Buf/Connect toolchain hints per package in monorepos. Parsing is string-aware so comments andgoogle.api.httppath literals with{…/*}do not break RPC detection.NestJS gains
@GrpcMethod/@GrpcStreamMethodextraction for code-first handlers; when the same wire path exists in a.proto, the mapper dedup key includestransportand proto wins over decorator-only entries.--framework connectroutes to the same extractor asgrpc.Reviewed by Cursor Bugbot for commit b14fcc4. Bugbot is set up for automated code reviews on this repo. Configure here.