Skip to content

fix: make generated code compile, unblank the blackbox tier, correct the docs - #434

Merged
jonaslagoni merged 2 commits into
mainfrom
fix/generated-code-compiles-blackbox-docs
Jul 30, 2026
Merged

fix: make generated code compile, unblank the blackbox tier, correct the docs#434
jonaslagoni merged 2 commits into
mainfrom
fix/generated-code-compiles-blackbox-docs

Conversation

@jonaslagoni

@jonaslagoni jonaslagoni commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Related Issue: none (sourced from .claude/thoughts/shared/improvements/2026-07-29-2227-improvements.md, ideas 1–3)

What

Three connected problems, fixed in dependency order:

  1. Generated code did not compile. On the default config, with exactly the dependencies the docs list, the first tsc after codegen generate failed. Five distinct defects, all reproduced before and after.
  2. The blackbox tier compiled nothing. All 216 outputs were an empty index.ts, so every pair passed regardless of what was generated — which is why (1) shipped. It now compiles real output; all 214 pairs pass.
  3. The protocol docs taught an API the generator does not have — a removed export, positional call sites, invented function names.

Why

ajv-formats is imported by every validating payload and header model, but it was declared in no package.json in the repo and listed on no docs page. Following docs/generators/payloads.md and installing only ajv produced eight TS2307s and a runtime MODULE_NOT_FOUND. The repo's own suites only passed because a transitively hoisted copy happened to be present.

That went unnoticed because the tier meant to catch it was inert. Since the in-memory generator seam (7d7b566), runGenerators returns files rather than writing them, and the blackbox harness never got the write call. It generated in memory, discarded the result, found no src/, created one containing an empty index.ts, and compiled that. Its project template is moduleResolution: node16 with no ajv — it was configured to catch every one of these failures on the first pair.

Unblanking it also surfaced two bugs shipped by e18b928: a channel whose messages each declare headers emitted A | B | C.unmarshal(...) — a bitwise-or expression, not a call — over identifiers the file never imported; and the NATS client's subscribe wrappers omitted the headers callback slot the channel functions declare.

Changes

Generated code now compiles

  • ajv-formats documented alongside ajv on the payloads/headers/channels pages, and declared in the five examples, the runtime suite, and the blackbox project template that import it.
  • {default as addFormats} resolves to the module namespace under moduleResolution: node16/nodenext, so addFormats(ajv) was not callable (TS2349 ×8) — the source comment claimed otherwise. Import the module and unwrap .default at the call site.
  • importExtension never reached the imports Modelina renders between models (an object model and the enum it was split into), so those stayed extensionless and failed TS2835 in exactly the projects the option exists for. Setting it explicitly did not help either. Applied at the single Modelina emission seam (src/codegen/output/modelina.ts) and threaded through payloads/parameters/headers/models.
  • The companion interface was re-exported as a value, which breaks under isolatedModules: true (TS1205 — Next.js, SWC, esbuild). Split into export type.
  • Union header models are imported as a namespace, like payload unions, and gained module-level unmarshal/marshal via the union preset. New getHeaderTypeAndModule keeps type positions (<Name>Module.<Name>) and value positions (<Name>Module.unmarshal) distinct across all seven protocols.
  • The NATS client's subscribe wrappers declare the headers callback parameter and import the header models; headerType is carried on the rendered-function metadata, and the channels render now exposes headerRender.
  • NATS reply passed a string as ErrorOptions to new Error(...).

The blackbox tier compiles real output

  • writeGeneratedFiles is called after runGenerators, with an assertion that every generated file reaches disk. writeGeneratedFiles and loadOpenapi are exported from the package root.
  • The missing inputType === 'openapi' arm is wired up (the harness previously had no loadOpenapi branch, so OpenAPI fixtures would silently no-op), and inputPath resolves against test/blackbox/ like documentPath already did.
  • Empty output is still tolerated where it is legitimate — a parameters config against a document with no channel parameters — rather than failing the pair.
  • codegen-node16.mjs had no npm script and ran nowhere, so the ESM surface was never regenerated. Wired into the runtime generate chain.

Docs match the generator

  • import { Protocols } was removed but still taught on seven pages, while docs/migrations/v0.md told readers to stop using it — and examples/ecommerce-asyncapi-channels still used it, so its documented npm run demo threw.
  • Positional call sites corrected across the protocol pages (every generated function takes one destructured object); postPingPostRequestpostPostPing (verified by generating from the doc's own example spec); AMQP/EventSource/WebSocket/client signatures; the custom.md snippet that was not valid JavaScript; amqp_publish/amqp_consume, which are not in the enum and silently generate no file; mapType's documented default; and the OpenAPI channels/client support claims in docs/generators/channels.md and README.md.
  • Options tables added for payloads and headers, which had none.

Dependency behaviour change — see Notes.

Testing

  • Build — npm run build
  • Lint + typecheck — npm run lint (exit 0)
  • Unit tests — npm test (720 passing, 1 skipped; 47 snapshots updated for the two intentional generated-code changes; two new specs cover applyImportExtension and the header-union resolution)
  • Blackbox — full matrix, all four CI buckets: 63 + 42 + 63 + 46 = 214 pairs, now compiling real generated output
  • Runtime tier (live Docker brokers) — 558 tests green: payload-types (271), http (114), regular (96), nats channels (22), organization (14), nats client (11), mqtt (8), amqp (7), eventsource (6), kafka (5), websocket (4)
  • Generated output type-checks under NodeNext, CommonJS/node, ESNext/bundler, and both with isolatedModules
  • test/runtime/typescript/src/node16 type-checks under module: Node16 + allowImportingTsExtensions
  • Every example with a tsconfig.json type-checks — ecommerce-asyncapi-client, typescript-library, typescript-nextjs, jsonschema-models (the first three failed before)
  • npm run prepare:pr as a single command — not run; its npm ci steps fail on pre-existing lockfile drift. Every stage was run directly instead (build, generate:readme:toc/generate:commands/generate:schema/format/generate:examples, lint, tests, runtime regeneration). See Notes.

Notes

Behaviour change — ajv-formats v3 format: time. Declaring the dependency moved the runtime suite off the transitively hoisted 2.1.1 and onto the ^3.0.1 the examples already pinned, which exposed a real difference: v3 follows RFC 3339 full-time and requires a time offset, so "10:30:00" is now rejected while "10:30:00Z" and "10:30:00+02:00" are accepted. Verified directly against both versions. The three FormatTime cases asserting bare local times were encoding the behaviour of an undeclared, accidentally resolved version; they now assert v3 behaviour, with added coverage for a numeric offset and for the no-offset case. Documented in docs/generators/payloads.md; users needing the lenient behaviour can pin ajv-formats@^2.

Scope beyond the three ideas. The header-union bug and the client callback omission were shipped by e18b928 and are not in the improvements file. Unblanking the blackbox tier surfaced them and they blocked "examples compile", so they were fixed here rather than parked in KNOWN_FAILING.

prepare:pr and generated assets. npm run generate:assets is not idempotent on this checkout independently of these changes: markdown-toc appends a trailing newline to four docs files and rewrites the docs/usage.md ToC (because generate:readme:toc runs before generate:commands), and docs/usage.md embeds a platform string that differs by OS. Those were reverted rather than folded into this PR — worth a separate fix.

Lockfiles. package-lock.json regenerated for the five examples and the runtime suite whose dependencies changed.

Follow-ups not taken here (recorded in the improvements file): modern JSON Schema ($schema: 2020-12 + $defs) is rejected with Input is not a JSON Schema, so it cannot be processed.; discriminator never drives generated unmarshal() (src/codegen/modelina/presets/union.ts:100-110 is unreachable and its emitted line is not valid TypeScript); subscribe callbacks are positional for NATS/Kafka but object-destructured for MQTT/AMQP/WebSocket, contrary to the project's own rule; and .claude/thoughts/shared/progress/2026-07-23-operation-channel-path-filtering-status.json reports 16/16 phases complete for a feature absent from src/.

…the docs

Addresses the three ideas in
.claude/thoughts/shared/improvements/2026-07-29-2227-improvements.md.

Generated code did not compile (idea 1)

- `ajv-formats` is imported by every validating payload/header model but was
  documented nowhere and declared in no package.json. Document it alongside
  `ajv` on the payloads/headers/channels pages, and declare it in the five
  examples, the runtime suite, and the blackbox project template that import it.
- `{default as addFormats}` resolves to the module namespace under
  `moduleResolution: node16`/`nodenext`, so `addFormats(ajv)` was not callable
  (TS2349). Import the module and unwrap `.default` at the call site — verified
  against ajv-formats v2 and v3, under node/node16/nodenext/bundler.
- `importExtension` never reached the imports Modelina renders between models,
  so those stayed extensionless and failed TS2835 in exactly the projects the
  option exists for. Apply it at the single Modelina emission seam.
- The companion interface was re-exported as a value, which breaks under
  `isolatedModules: true` (TS1205, Next.js/SWC/esbuild). Split it into
  `export type`.
- Header unions (a channel whose messages each declare headers) referenced the
  expanded `A | B | C` type without importing its members, and emitted
  `A | B | C.unmarshal(...)` — a bitwise-or expression, not a call. Import union
  header models as a namespace, like payload unions, and give them module-level
  `unmarshal`/`marshal`.
- The client generator's subscribe wrappers omitted the `headers` callback slot
  the channel functions declare, and never imported the header models.
- NATS reply passed a string as `ErrorOptions` to `new Error(...)`.

The blackbox tier compiled nothing (idea 2)

Since the in-memory generator seam, `runGenerators` returns files rather than
writing them; the harness never got the write call, so all 216 outputs were an
empty `index.ts` and every pair passed regardless. Write the files, assert they
reach disk, and wire up the missing OpenAPI loader arm. All 214 pairs pass on
real generated output. Also wires `codegen-node16.mjs` — an orphan config with
no npm script — into the runtime `generate` chain.

Docs described an API the generator does not have (idea 3)

`import { Protocols }` was removed but still taught on seven pages (and used by
an example whose documented `npm run demo` threw), while the migration guide
told readers to stop using it. Fixed that, the positional call sites across the
protocol pages, `postPingPostRequest` -> `postPostPing`, the AMQP/EventSource/
WebSocket/client signatures, the invalid `custom.md` snippet, the
`amqp_publish`/`amqp_consume` values that generate nothing, `mapType`'s default,
and the OpenAPI channels/client support claims. Documents the payloads and
headers options, which had no table at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jonaslagoni
jonaslagoni requested a review from ALagoni97 as a code owner July 30, 2026 09:09
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
the-codegen-project Ready Ready Preview Jul 30, 2026 9:16am
the-codegen-project-mcp Ready Ready Preview Jul 30, 2026 9:16am

Declaring `ajv-formats` moved the runtime suite off a transitively hoisted
2.1.1 and onto the ^3.0.1 the examples already pin, which surfaced a real
behaviour difference: v3 follows RFC 3339 `full-time` and requires a time
offset, so `format: time` rejects a bare `"10:30:00"` that v2 accepted.

The three FormatTime cases asserting bare local times were encoding the
behaviour of an undeclared, accidentally resolved version. Point them at
offset-bearing values, add explicit coverage for a numeric offset and for the
now-invalid no-offset case, and document the change where users read about
validation dependencies.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jonaslagoni
jonaslagoni merged commit c3b3352 into main Jul 30, 2026
24 checks passed
@jonaslagoni
jonaslagoni deleted the fix/generated-code-compiles-blackbox-docs branch July 30, 2026 09:23
@jonaslagoni

Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in version 0.80.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant