You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 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. 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"]
Loading
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.
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.
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.
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.
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
constconfig: 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 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.
Summary
Plugins configured under a single
generatesentry 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
plugins: []array run independently and their string outputs are simply concatenated, there is no data channel between them.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
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
typescriptandtypescript-resolversplugins' 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:
/distoutput from within codegen's own test suite, making it flaky and hard to testNote
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:
typescript-operationsgenerates client types, but custom directives might need to change those types.Current approach:
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
types.generated.tsfrom the outer key.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
The first stage needs
filenamebecause the directory could hold several files. The second stage doesn't, since presets already decide their own output filenames internally.typescript-resolversalready proves that it can return ametafield alongsidecontenttoday. 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
typescript-resolvers) already returnmetaalongsidecontenttoday.Cons
Use case coverage
typescript+typescript-resolvers→ core Server Preset logicgraphql(...)calls.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
Cons
Use case coverage
buildGeneratesSection, then runs a codemod over the result. A preset wrapping a preset, not something the Client Preset gains for free.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
generatesentries 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
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.