feat(ts): emit per-proto type modules + shared errors in TS generators#200
feat(ts): emit per-proto type modules + shared errors in TS generators#200hishamank wants to merge 15 commits into
Conversation
db74fd3 to
7096067
Compare
7096067 to
e511743
Compare
🔍 CI Pipeline Status✅ Lint: success 📊 Coverage Report: Available in checks above |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #200 +/- ##
=========================================
+ Coverage 3.32% 10.99% +7.66%
=========================================
Files 62 66 +4
Lines 10966 11219 +253
=========================================
+ Hits 365 1233 +868
+ Misses 10595 9967 -628
- Partials 6 19 +13
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
e511743 to
93abf3b
Compare
Both TypeScript generators (protoc-gen-ts-client, protoc-gen-ts-server) now emit one canonical type module per proto file (<proto>.ts) plus a shared errors.ts, and slim the service files down to service classes / handlers that import their request/response types and error helpers. This replaces the previous behavior of inlining the full transitive type closure into every service file, which duplicated shared types (e.g. SongID, ValidationError) across files. Types are now declared once and imported via relative specifiers. This is the default and only layout — there is no configuration flag. Requires 'strategy: all' in buf.gen.yaml so shared modules are emitted once across the whole graph rather than per-directory. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93abf3b to
01796dc
Compare
The doc comment referenced a discriminated-oneof style field on EmitContext that does not exist on this branch. Describe only the behavior the function actually has: modules-mode cross-module imports via ctx, discriminated-union rendering for annotated oneofs, flattened optional fields otherwise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every existing golden fixture is single-file, so the layout's core feature (importing types across proto packages via relative specifiers) had no golden coverage. Add a two-package fixture: crosspkg.common.v1 defines ItemID and a Category enum; crosspkg.shop.v1 imports them and embeds both in its service request/response messages. Wire the fixture into both generators' golden tests (protoc now takes a list of proto files) and assert explicitly that the emitted shop type module contains the relative cross-package import 'from "../../common/v1/types"', not just via golden comparison. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The TypeScript sections documented the old inline single-file output. Describe the current per-proto modules layout (one type module per source proto, slim client/server modules importing their types, shared errors.ts at the output root, cross-package relative imports) and document the required strategy: all in buf.gen.yaml. Fix the error-handling import examples to point at the type module and errors.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The golden tests exercise the TS client/server generators as subprocess protoc plugins, so their statements never counted toward Go coverage. Add an in-process golden runner per generator: build a protogen.Plugin from the existing fixture protos (via protoc --descriptor_set_out --include_imports, reusing the same protoc-absent skip) and call Generate() directly, then diff every emitted file against the checked-in golden files byte-for-byte. This lights up generator.go, modules.go, imports.go, and the EmitContext paths in types.go with real assertions. Add targeted error-path coverage: a reserved-name fixture (message named ValidationError) asserts CheckReservedNames fails loudly, and the invalid path-param / uncovered-field fixtures now run in-process to cover the server route-config error branches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Node ESM and TypeScript's node16/nodenext module resolution require an explicit file extension on every relative import specifier. The modules layout emitted extensionless specifiers (e.g. "./service", "../../errors"), so the generated package could not be imported under nodenext without a TS2835 error. Append ".js" in tscommon.RelativeImportSpecifier, the single chokepoint through which every relative import flows (cross-module type refs via ref and the shared errors module via NeedErrors). Regenerate all client and server goldens; every relative import line now ends in ".js". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add tscommon.EmitPackageBarrels, called by both TS generators after they
emit their modules. It writes an index.ts into each non-root output
directory that holds generated modules, re-exporting every sibling module
(type modules plus the client/server module) with ".js" specifiers and a
deterministic sort. This lets consumers import a whole proto package by its
directory, e.g. import { Album, AlbumServiceClient } from ".../album/v1".
The output root, where the shared errors module lives, never receives a
barrel: aggregating packages at the root is the consumer's concern
(anghamna generates its own root index). "export *" is safe within a proto
package because message names are unique per package.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Nested proto messages and enums are hoisted to top-level TypeScript declarations using only their leaf name, which collides when a package also defines a top-level type of that name (e.g. a nested GetSubscriptionStatusResponse.SubscriptionStatus vs a top-level SubscriptionStatus). The per-package barrel's `export *` then fails to compile with TS2308 (duplicate export). Add QualifiedTSName, which prefixes a message/enum descriptor's leaf name with its enclosing message names (WrapperStatus), matching the convention proto authors already use for flattened messages. Route the type-name declaration sites (GenerateInterfaceCtx, GenerateEnumType) and the reference sites (RefMessage, RefEnum) through it so the declaration and every reference stay byte-identical. Top-level types are unaffected. Add a nested_collision fixture (top-level Status/Kind plus a Wrapper with nested Status/Kind of the same leaf names) exercised by both TS generators; the emitted package typechecks clean under nodenext. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Final Review — PR #200:
|
…of erroring CheckReservedNames only walked messages and compared leaf proto names, so an enum named ApiError/ValidationError/FieldViolation slipped through and produced a duplicate-identifier import next to the errors.js helper import (invalid TypeScript), while a nested Wrapper.ValidationError (emitted as WrapperValidationError) was needlessly rejected. Replace the loud rename error with deterministic aliasing at the single chokepoint: ImportTracker pre-reserves the three helper names, so any imported type or enum whose emitted TS name collides is aliased (ApiError_1) regardless of kind or nesting. UsedErrorSymbols now matches on identifier boundaries so an aliased colliding type (ApiError_1) no longer drags in an unused errors.js import. The reserved_name fixture now covers the full matrix (colliding message, colliding enum via root unwrap, non-colliding hoisted nested name) and is part of the golden suites of both TS generators. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Type modules are emitted at the source-relative proto path while service modules are emitted at GeneratedFilenamePrefix. Under protoc's default paths=import mode the two diverge: types land at the output root while service modules (and the per-package barrels) nest under the Go import path, so barrels silently omit every message/enum type. Validate up front that every generated file's output prefix matches its source-relative path and fail with an actionable error pointing at paths=source_relative, instead of emitting a subtly broken layout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…utput Running tsc --noEmit (strict, nodenext, noUnusedLocals) over the golden trees exposed three generator bugs: - A proto type named after a global the templates rely on (Response, Request, Error, Record, fetch, ...) shadowed that global in service modules, typechecking template code against the proto type. ImportTracker now pre-reserves those global names alongside the error-helper names, so such types import under a deterministic alias (Response_1). - Map-value unwrap referenced the collapsed wrapper message while resolving the inner type, recording a cross-module import the emitted code never uses (TS6196 under noUnusedLocals). The unwrapped inner type is now resolved directly. - Query params: the client passed number/boolean elements of repeated fields straight to URLSearchParams.append, and the server blind-cast getAll()'s string[] into number[]/boolean[] fields — handlers actually received strings at runtime. Both sides now convert per element (String(v) / .map(Number) / .map(v => v === "true")). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Golden byte-comparison catches output drift but not output that was never valid TypeScript. Both TS generators now compile their full golden tree with tsc --noEmit under strict nodenext settings (module/moduleResolution nodenext, strict, noUnusedLocals), locking in the Node-ESM .js import claim, the reserved-name aliasing, and import minimality. Uses tsc from PATH when present, falls back to a pinned compiler via npx, and skips when no toolchain exists. CI installs typescript@5.9.3 in the test job so the check always runs there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t imports The modules layout requires strategy: all in buf.gen.yaml, but the four TS example configs (ts-client-demo, ts-fullstack-demo, sse-streaming, rn-client-demo) were never migrated, and their hand-written consumers still imported types and error helpers from the old single-file client/server module. Add strategy: all to every TS plugin entry and update the consumers to the modules layout: error helpers from generated/errors, message types from the proto's type module, service classes/handlers from the _client/_server module. All four examples regenerate cleanly with buf generate; the rn-client-demo OpenAPI doc is refreshed from the current generator as a side effect. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Type modules and errors.ts are byte-identical between the two TS generators, but each generator's per-package index.ts barrel re-exports its own service module — pointing both at one output directory is last-writer- wins on the barrel. Document distinct out: directories as the supported setup (matching the examples) and drop the shared-directory framing from the EmitSharedModules docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The TS golden and consistency harnesses reused a prebuilt bin/ binary and only ran make build when it was missing, so a stale binary silently tested old generator code and reported spurious passes or failures. Build the plugin under test into a per-test temp dir instead (cheap via the Go build cache), removing the bin/ dependency entirely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@SebastienMelki all review points handled in a655891..d5531b9:
|
What
Both TypeScript generators (
protoc-gen-ts-client,protoc-gen-ts-server) now emit a canonical per-proto type module layout:<proto>.tsmodule per source proto that defines messages/enums (e.g.core/v1/identifiers.ts), declaring each type once.errors.tsat the output root holding the runtime helpers (ApiError,ValidationError,FieldViolation)._client.ts/_server.tsservice files containing only the service class / handlers + framework types, importing their request/response types from the type modules and helpers fromerrors.tsvia relative specifiers.Why
Previously each service file inlined the full transitive type closure, so shared types were re-declared in every file (
SongID×5,ValidationError×14 in a real consumer). Runtime helpers were likewise duplicated per file. The new layout declares each type once and imports it — matching howts-proto/protobuf-esstructure their output — which is what makes the generated client usable as a real npm types package.Behavior
strategy: allinbuf.gen.yamlso the shared modules (errors.tsand cross-package type modules) are emitted once over the whole file graph instead of once per directory.Notes
_client.ts/_server.tsinto<proto>.ts+errors.ts, and imports are added). Import paths in downstream code must be updated accordingly.ValidationError,ApiError, orFieldViolationnow errors loudly (it would shadow the hoisted error helpers).tscommonemitter).Tests
.tsfile; all fixtures regenerated to the modules layout.httpgencross-generator consistency tests updated to read the type module + service file together.staticcheckgreen.