Skip to content

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.

CommandDescription
doctorAutomatic checks to ensure gql.tada is set up and installed correctly.
checkRuns all diagnostics that GraphQLSP runs in the CLI.
generate outputGenerates the gql.tada output file for type checking, like GraphQLSP does.
generate schemaUsed to introspect a GraphQL API or schema.
generate persistedUsed to extract a JSON manifest of persisted documents.
generate turboUsed 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.

sh
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.

sh
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.

sh
npm gql.tada generate schema 'https://api.github.com/graphql'
sh
pnpm gql.tada generate schema 'https://api.github.com/graphql'
sh
yarn gql.tada generate schema 'https://api.github.com/graphql'
sh
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().

ts
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:

ts
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.

sh
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.

sh
npm gql.tada turbo
sh
pnpm gql.tada turbo
sh
yarn gql.tada turbo
sh
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.