RFC: Sequential Execution & Output Hand-off Between GraphQL Codegen Plugins/Presets
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 has to invoke
typescriptandtypescript-resolversitself in order to share the type map between them, rather than composing them declaratively.
This workaround only exists inside the Server Preset source code. 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<TContext> 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.
flowchart TD
SDL["GraphQL Schema / SDL"] --> Preset
subgraph Preset["Server Preset (the wrapper)"]
direction LR
TS["typescript plugin<br/>generates TS types"]
TSR["typescript-resolvers plugin<br/>generates resolvers"]
TS -- "type map<br/>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.
flowchart LR
Schema["GraphQL Schema"] --> ClientPreset["Client Preset"]
ClientPreset --> Output["Codegen output<br/>graphql() calls + types"]
Output --> Post["Post-processor<br/>(Babel / SWC plugin)"]
Post --> Dist["Final bundle<br/>/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
/distoutput 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-operationsgenerates 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-operationsoutput is very hard at the moment: client-specific use cases leak into the base plugin, such as apolloUnmask.
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
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.tsfrom the outer key. - The
typescript-resolversstage runs after thetypescriptstage, 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
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. 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 returnmetaalongsidecontenttoday.
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-operationsreturns its generated type names and translated field types viameta; the subsequent plugin reads thatmetainstead 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-operationsand captures the type names from itsmeta— 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 ownschemaargument in that case; it has to know to pull the transformed schema frommetainstead, 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.
Source: dotansimha/graphql-code-generator