New CLI Workflows 2024/04/15 ​
the tl;dr ​
Today, we're releasing [email protected]
with:
- The new
gql.tada
CLI supporting new workflows - Turbo Mode (via
gql.tada turbo
) speeding up TypeScript performance - Persisted Operations support
In the last few weeks we've been focusing on taking large strides towards making gql.tada
ready for any production projects - existing or new ones alike.
To recap where we were before this new release,
- GraphQLSP is responsible for providing diagnostics and auto-completions in your editor via the TypeScript language server plugin API,
- The main
gql.tada
library contains typings to infer the types of your GraphQL documents.
While, GraphQLSP can provide interactive hints in your editor however, it can't intervene in when things go wrong. Specifically, outside of type checks, gql.tada
leaned on the in-editor plugin telling you when something went wrong.
It also generates the graphql-env.d.ts
, the typings output file that gql.tada
needs to perform its type inferences.
However, to go further, with more diagnostics and type generation that work in large teams and support more advanced workflows with gql.tada
, we need a CLI.
Hi, gql.tada
CLI! ​
Recent efforts have been to work on RFC #76. We focused on a CLI that can support use-cases where the LSP plugin isn't an option or where more outputs and diagnostics are needed, as such, our CLI supports several commands now, starting with version v1.5.0
.
Command | Description |
---|---|
doctor | Automatic checks to ensure gql.tada is set up and installed correctly. |
check | Runs all diagnostics that GraphQLSP runs in the CLI. |
generate output | Generates the gql.tada output file for type checking, like GraphQLSP does. |
generate schema | Used to introspect a GraphQL API or schema. |
generate persisted | Used to extract a JSON manifest of persisted documents. |
generate turbo | Used to generate a typings cache to speed up TypeScript performance. |
We consider the addition of the CLI with these commands to be the minimum required to enable all major client-side workflows the GraphQL community needs to migrate to gql.tada
.
NOTE
The CLI is currently in beta, but we consider it ready to be used as of now.
Please report any issues or shortcomings you encounter in our issues.
Supplementing GraphQLSP workflows ​
The gql.tada check
command outputs diagnostics that the LSP usually outputs inline in the editor.
gql.tada check
We need the check
command because GraphQLSP has GraphQL-relevant diagnostics that will not be output when we run tsc
. Specifically, the check
command performs validation on all your GraphQL documents in gql.tada
.
Similarly, the gql.tada generate output
command replicates how GraphQLSP outputs the introspection file for gql.tada
's typings to function.
gql.tada generate output
Both of these commands stand in for GraphQLSP during continuous integration or during pre-commit checks.
You can read more about these commands on our new reference docs page:
GitHub Actions support ​
The check
command, and other commands that output diagnostics; namely, generate persisted
and generate turbo
, have output support for GitHub Actions.
GitHub's Actions workflows allow for custom commands that can be used to annotate pull requests with rich diagnostics diagnostics output in their UI. The CLI supports this so you don't have to read and parse the CLI output manually on GitHub Actions.
Introspection Schemas ​
To close another gap in the workflows we support, the CLI also supports a generate schema
command. This command is used to introspect a GraphQL API and output a .graphql
SDL file.
npm gql.tada generate schema 'https://api.github.com/graphql'
pnpm gql.tada generate schema 'https://api.github.com/graphql'
yarn gql.tada generate schema 'https://api.github.com/graphql'
bun run gql.tada generate schema 'https://api.github.com/graphql'
By default, this command looks at your schema
configuration and writes to its file path. This is because this command is useful when your GraphQL API is not directly accessible via GraphQLSP, for instance, because you need to add authorization headers to all requests. (Hint: the CLI accepts --header
options)
You can also write any schema you're introspecting to a different file path, by passing the --output
option, or by piping the command to an output file (i.e. gql.tada generate schema [url] > output.graphql
) The CLI also accepts a different .graphql
SDL file as an input, and shares the same introspection logic across GraphQLSP and other CLI commands.
We've seen many requests to support more use-cases where the API is not run locally, is in a separate repository, or simply needs headers sourced from environment variables. This new command is our answer to this request.
Persisted Operations Support ​
Persisted Operations are on their way to be standardized in GraphQL and are an important tool in securing GraphQL APIs and offering a tool to turn GraphQL API communication into an RPC-like transport.
At the most basic level, there are two approaches to persisted operations. "Automatic Persisted Queries" (APQ) already worked with gql.tada
prior to this release as it's a client-side protocol.
With APQ, queries are hashed and generate an ID during runtime. This means that the client:
- generates a docuemnt ID (by convention, most of the time a SHA256 hash of the document)
- it attempts to send this ID instead of the GraphQL document
- if the API doesn't recognize the document, it can optionally attempt to send both the ID and the document, and the API can choose to start recognizing it as an alias.
You can learn more about this on the documentation of several GraphQL clients, for example the urql
page on persisted queries.
Persisted Operations go beyond this approach and remove the dynamic generation of document IDs.
With persisted operations the goal is to discover all GraphQL operations that a GraphQL client can send during build-time. This allows you to build up an allowlist of known GraphQL documents ahead of time.
We've added a new API to gql.tada
to facilitate this, graphql.persisted()
.
import { graphql } from 'gql.tada';
const HelloQuery = graphql(`
query Hello {
hello
}
`);
const HelloQueryPersisted = graphql.persisted('sha256:x', HelloQuery);
In the above example, the graphql.persisted()
call creates a new DocumentNode
that carries the document ID (passed as a first argument) on a documentId
property.
If you're not using a normalized cache (such as Apollo Client's or urql Graphcache) you can then even compile away the original document and just only send the document ID, since you have a manifest of all queries the client can send. We can achieve this by passing the query by type instead:
import { graphql } from 'gql.tada';
const HelloQuery = graphql(`
query Hello {
hello
}
`);
const HelloQueryPersisted = graphql.persisted<typeof HelloQuery>('sha256:x');
In our above example, provided HelloQuery
(and potential fragments your queries may be referencing) are never used by value and only by type, your documents will be tree-shaken and minified out of your output bundle.
Persisted Manifests ​
The gql.tada generate persisted
command scans our codebase for graphql.persisted()
calls and extracts your persisted documents into a JSON manifest.
gql.tada generate persisted -o persisted.json
The JSON manifest contains the document IDs as keys mapped to the document string values, which we can then go on to register with our APIs ahead of deployment.
GraphQLSP Support ​
GraphQLSP has also been updated to support Persisted Operations.
It now provides hints when you're using graphql.persisted()
to validate its usage, and also features a code action that allows you to quickly update the document ID to a new hash whenever the query changes.
You can find the full API docs for Persisted Operations in our reference docs:
Turbo Mode 🔥 ​
We've been working hard on making type inference in gql.tada
as fast as it can be. If you'd like to be up-to-date on what we've done to address TypeScript performance to be better, we now have a GitHub milestone that tracks all performance-related issues and fixes.
We realise that there is a limit to how fast parsing GraphQL in the TypeScript type system can be, but while this is a common assumption, we love to challenge this by finding optimizations in our TypeScript types.
However, even with continuous optimizations and this having been our focus for several weeks, even if gql.tada
's type inference got 10x faster, this can always be negated by a code base having 10x more documents.
This mostly doesn't show itself in editing performance, unless a document has a lot of fragment references, but running tsc
and performing type checks on a full codebase always incurs the cost of inferring the types of all your documents.
Codegen and Inference ​
If we were to only generate types ahead of time, we would:
- lose the benefits of expressing GraphQL in the TypeScript type system
- have decreased editing performance (ironically) and don't get editing feedback in real-time
- have extra steps that are always required to see GraphQL types in TypeScript
However, with gql.tada
, as previously established we get type inference as fast as TypeScript can infer them - all while we edit with no extra commands running in the background and no reevaluation of generated files.
We really do want to have our cake and eat it too, which is why we're introducing a "Turbo Mode" enabled by our CLI.
npm gql.tada turbo
pnpm gql.tada turbo
yarn gql.tada turbo
bun run gql.tada turbo
The gql.tada turbo
command creates a code-generated typings file of the GraphQL documents in your codebase, which is fed into gql.tada
as a cache, of sorts.
After running the command, the types of graphql()
documents are first looked up from the cache that have been cached and generated into the turbo typings file, essentially skipping type inference. If we then start editing our documents, type inference kicks in and infers the type of a document dynamically.
This combines the best of both worlds; GraphQL type generation and GraphQL type inference and is a new middleground to ensure best performance and the dynamic benefits of type inference.
Adapting to Turbo Workflows ​
To gain the benefits of gql.tada turbo
, you could for example, run it before submitting your pull request, or before committing changes, or as a pre-commit hook.
As gql.tada turbo
is always optional, you can run it ahead of time or after you're done editing your documents.
GraphQLSP never generates this file, so you don't have to wait for codegen to complete while editing.
NOTE
We can guarantee that the types between Turbo Mode and gql.tada
's default type inference are consistent because the turbo typings are just a cache.
The gql.tada turbo
command essentially runs TypeScript and caches the type output, using exactly what your editor will see.
The turbo
command uses TypeScript's type output, so make sure that you've got an up-to-date gql.tada
output file present, which you can of course generate using gql.tada generate output
before running the turbo
command.
What's next? ​
We'd like to focus on multi-schema support next.
With just one GraphQLSP and gql.tada
configuration, we should be able to support multiple schemas at the same time, enabling you to use gql.tada
for third-party schemas, while still consuming your own GraphQL API.
We don't have an RFC for this yet, but you can read some discussions and notes about this in the meantime.