dotansimha / dotansimha/graphql-code-generator
RFC: Sequential Execution & Output Hand-off Between GraphQL Codegen Plugins/Presets
- Dominant language
- TypeScript
- Stars
- 11.3k
- Forks
- 1.4k
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 23
Description
## Summary
Plugins configured under a single `generates` entry currently run in parallel and have no way to pass output, AST, or metadata to one another. This RFC lays out the problem, two candidate designs for letting plugins (and presets) hand off state to downstream stages, with a recommendation for which to adopt.
## Problem
- Some use cases require extending a plugin's output, or "handing off" transformation/output/metadata from one plugin to the next e.g. a types map, a list of scalars, resolved config, etc.
- Today, all plugins in a `plugins: []` array run independently and their string outputs are simply concatenated, there is no data channel between them.
- The current workaround is for plugins/presets to bypass the plugin pipeline entirely and call plugin functions manually, threading extra metadata by hand. For example, the [Server Preset](https://the-guild.dev/graphql/codegen/docs/guides/graphql-server-apollo-yoga-with-server-preset) has to invoke `typescript` and `typescript-resolvers` itself in order to share the type map between them, rather than composing them declaratively.
This workaround only exists [inside the Server Preset source code](https://github.com/eddeee888/graphql-code-generator-plugins/blob/57a074b1606b58abdc39a8ac2b21139878a40c9d/packages/typescript-resolver-files/src/addVirtualTypesFileToTsMorphProject/addVirtualTypesFileToTsMorphProject.ts#L62-L67). Ordinary users writing a plain `plugins: []` list have no equivalent capability; if two plugins need to share data, the only option is to fork one into a preset.
## Goals
- Let plugins/presets consume the output and/or structured metadata produced by a plugin that ran earlier in the same output target.
- Let plugins/presets be expressed as compositions of ordinary plugins instead of writing bespoke orchestration code.
- Let a stage transform input it receives (schema, documents, or a prior stage's output) before passing it on, not just append to it.
- Preserve backward compatibility for existing configs where plugins are independent.
## Use Cases
Three concrete cases motivate this RFC. Each reflects the same underlying gap: no way to pass data or transformations between generation steps.
### 1. Server Preset: types → resolvers
**Need:** resolver signatures depend on the TypeScript types codegen already generated for the schema (so `Resolvers` is typed against the right shape), not just the raw SDL.
**Current approach:** the "wrapper". Server Preset manually invokes the `typescript` and `typescript-resolvers` plugins' functions, captures the type map it returns, and passes that map directly into subsequent preset core logic. This effectively re-implements a small, private plugin runner inside the preset's own source just to get two plugins to talk to each other. None of this orchestration is reusable.
```mermaid
flowchart TD
SDL["GraphQL Schema / SDL"] --> Preset
subgraph Preset["Server Preset (the wrapper)"]
direction LR
TS["typescript plugin
generates TS types"]
TSR["typescript-resolvers plugin
generates resolvers"]
TS -- "type map
passed manually, in JS" --> TSR
end
Preset --> Out["types.generated.ts"]
```
**Cost:** this wrapper is bespoke per preset and invisible to config authors. A user who wants the same types→resolvers hand-off outside this one preset has no path to it short of writing their own preset in JS.
### 2. Client Preset → Compiler build-time compilation
**Need:** the Client Preset optimizes `graphql(...)` tagged-template calls in application source by resolving them to precomputed document references at build time, instead of parsing GraphQL strings at runtime.
**Current approach:** this compilation runs entirely outside codegen, as a babel or SWC plugin wired into the application's own bundler, a second toolchain integration the user installs and configures separately from `codegen.ts`.
```mermaid
flowchart LR
Schema["GraphQL Schema"] --> ClientPreset["Client Preset"]
ClientPreset --> Output["Codegen output
graphql() calls + types"]
Output --> Post["Post-processor
(Babel / SWC plugin)"]
Post --> Dist["Final bundle
/dist"]
```
**Cost:** everything from "Post-processor" onward runs in the application's own build, not codegen's. This has a few friction points for users:
- users who use unsupported bundlers do not get the benefits
- there's no easy way to assert on the final `/dist` output from within codegen's own test suite, making it flaky and hard to test
> [!NOTE]
> The Server Preset has proven that it's possible to use the TypeScript compiler API within codegen to codemod the generated output, the role babel or SWC is playing here.
### 3. Plugin → Plugin: Other use cases
**Need:** additional use cases have surfaced where it makes sense to run plugins sequentially:
- Transforming schema in one plugin, before passing it to the next.
- Augmenting the output of an existing plugin e.g. `typescript-operations` generates client types, but custom directives might need to change those types.
**Current approach:**
- Transforming schema currently requires a separate codegen run before the main one. This is a heavy setup, and watch mode doesn't work well across two separate runs.
- Augmenting `typescript-operations` output is very hard at the moment: client-specific use cases leak into the base plugin, such as [apolloUnmask](https://github.com/dotansimha/graphql-code-generator/blob/2e79816804dc965d8fa6fc1e926e737ca29f4f4f/packages/plugins/other/visitor-plugin-common/src/types.ts#L135-L141).
## Options
### Option 1: Sequential Stages
Change the config shape so a single output target can declare an ordered list of stages. Stages execute top to bottom, and each entry is a stage, either a **plugin stage** (runs plugins, produces `content`/`meta`), or a **preset stage** that delegates to an existing named preset. Each stage may transform input such as the schema or documents and pass it on to the next.
**Example 1: Basic sequential execution**
```ts
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
generates: {
'./src/generated/types.generated.ts': [
{ plugins: ['typescript'] },
{ plugins: ['typescript-resolvers'] },
],
},
};
export default config;
```
- Both stages inherit `types.generated.ts` from the outer key.
- The `typescript-resolvers` stage runs after the `typescript` stage, and its content is appended to that same file.
**Example 2: output key is a directory, and a plugin stage can say which file it targets**
```ts
const config: CodegenConfig = {
generates: {
'./src/generated/': [
{ filename: 'types.generated.ts', plugins: ['typescript', 'typescript-resolvers'] },
{ preset: 'server' }, // preset stage: no filename needed, the preset decides its own
],
},
};
```
The first stage needs `filename` because the directory could hold several files. The second stage doesn't, since presets already decide their own output filenames internally.
`typescript-resolvers` already proves that [it can return a `meta` field alongside `content` today](https://github.com/dotansimha/graphql-code-generator/blob/2e79816804dc965d8fa6fc1e926e737ca29f4f4f/packages/plugins/typescript/resolvers/src/index.ts#L341-L343). This option reuses that existing field rather than inventing a new one, and makes it flow forward to later stages.
> [!WARNING]
> The exact mechanism for how multiple plugins/presets hand off data to each other is still being worked out. If you have thoughts, be sure to comment.
**Pros**
- "Stages" is declarative, and the chain can be composed from plugins/presets.
- Reuses an existing convention instead of inventing one: some plugins (e.g. `typescript-resolvers`) already return `meta` alongside `content` today.
**Cons**
- Purely linear: can't express "A and B run in parallel, then C depends on both." This is acceptable if real hand-off needs are usually two stages deep (observed use cases fall into this category), not a wide graph.
**Use case coverage**
- **1. Server Preset**: Yes. Two stages: `typescript` + `typescript-resolvers` → core Server Preset logic
- **2. Client Preset → Compiler**: Yes. Client Preset → Compiler. First stage to add generated files and meta of where the document docs are, so the Compiler stage can replace the `graphql(...)` calls.
- **3. Plugin → Plugin**: Yes. E.g. `typescript-operations` returns its generated type names and translated field types via `meta`; the subsequent plugin reads that `meta` instead of independently re-deriving names by convention, closing the implicit-agreement gap.
### Option 2: Wrapper Pattern
Plugins/presets can already invoke other plugins directly and use their returned metadata, so they can continue to act as wrappers around them.
**Pros**
- Smallest possible change: no new config syntax, no change to how the execution engine schedules or generates output.
- Proven pattern: the real Server Preset does this today.
**Cons**
- Doesn't solve the multi-stage use cases like Client Preset → Compiler or Plugin → Plugin.
- Every combination needs its own hand-written wrapper.
**Use case coverage**
- **1. Server Preset**: Yes. This *is* the current approach.
- **2. Client Preset → Compiler**: No, not directly. Folding the compiler step into codegen this way means authoring a *new* preset that itself calls the Client Preset's `buildGeneratesSection`, then runs a codemod over the result. A preset wrapping a preset, not something the Client Preset gains for free.
- **3. Plugin → Plugin**: Yes, but heavy. Same shape as the Server Preset case: the wrapper calls `typescript-operations` and captures the type names from its `meta` — or, for schema transformation, transforms the schema and passes the result into the next plugin. Note that the subsequent plugin can't rely on its own `schema` argument in that case; it has to know to pull the transformed schema from `meta` instead, which is a hacky, easy-to-miss contract.
## Recommendation
**Adopt Option 1, with the explicit `{ content, meta }` hand-off shape:**
This ships without changing behavior for any config written today. The new mechanics only exist inside the new array shape; existing single-object `generates` entries are untouched.
Option 2 isn't going away. It still covers the current Server Preset case perfectly well, but it doesn't extend to the compiler or schema-transformation use cases, so it should be treated as fallback, rather than a substitute for Option 1.
## Other Considered Options
- **Declarative plugin dependency graph (capabilities-based):** let each plugin declare what it **provides** and **requires** as metadata alongside its `plugin()` export (e.g. `provides: ['typescript:types']`, `requires: ['typescript:types']`). This is a more complex variant of Option 1, but doesn't solve the use cases any better.
- **Creating multiple codegen runs and sequencing them via scripts:** works for simple cases but is cumbersome, and watch mode experience would be bad.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the existing Server Preset wrapper source and the typescript-resolvers plugin's current meta return, both linked in the RFC. Compare those patterns with the proposed sequential stages and explicit { content, meta } hand-off. Done means an agreed and implemented design that preserves existing generates configurations while enabling ordered plugin or preset stages.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- developer-experience, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 32/100