Skip to Content
Gqlts

Contributing and architecture

This page explains how the repository is organized, how generated clients are built, and which commands to run before shipping changes.

The canonical repo-maintainer docs live in DEVELOPMENT.md, docs/architecture.md, and docs/testing.md.

Repository structure

Gqlts has two published workspace packages and several demo apps that behave like real consumers.

Architecture at a glance

Published packages

CLI

@gqlts/cli loads a schema and writes a generated client directory. It is used from gqlts, npx gqlts, pnpm dlx gqlts, yarn gqlts, bunx gqlts, or the programmatic generate(config) API.

Runtime

@gqlts/runtime is imported by generated clients. It handles operation generation, runtime execution, upload detection, batching, subscriptions, and type-map linking.

Code flow

Generation flow

The generator starts from either the CLI command or the programmatic generate(config) API.

  1. cli/src/cli.ts parses flags such as --schema, --endpoint, --output, --esm, --esm-and-cjs, --sort, -H, and -S.
  2. cli/src/tasks/validateConfigs.ts rejects missing or conflicting schema sources.
  3. cli/src/main.ts creates the Listr pipeline.
  4. cli/src/tasks/schemaTask.ts loads a schema from SDL or endpoint introspection.
  5. cli/src/tasks/clientTasks.ts creates the output directory and writes the generated files.
  6. Renderers add code into RenderContext; file writes stay centralized in the task layer.

RenderContext collects code blocks, tracks generated imports, rewrites relative imports when config.output is known, and returns the final code string.

Renderer map

The type map renderer builds a readable string-keyed map and compresses type names into indexes. The runtime links that map back into objects with linkTypeMap. If generated query paths are wrong, inspect both renderTypeMap and linkTypeMap.

Runtime query flow

Generated entrypoints import runtime helpers, link the generated type map, and expose createClient.

  1. Generated createClient injects queryRoot, mutationRoot, and subscriptionRoot.
  2. Runtime createClient creates query, mutation, and subscription methods.
  3. Query and mutation calls use generateGraphqlOperation.
  4. runtime/src/fetcher.ts executes through Axios, a custom fetcherMethod, multipart upload, or QueryBatcher.
  5. Subscription calls create a graphql-ws client lazily and return a zen-observable-ts observable.

Request object syntax

Arguments become generated GraphQL variables such as v1 and v2. Operation generators return { query, variables }, which can be executed by the built-in client or passed to Apollo, urql, fetch, Axios, or another client.

Type selection flow

runtime/src/client/typeSelection.ts maps request objects to response types through FieldsSelection<SRC, DST>.

Important behaviors:

Generated output

A generated client usually contains:

The generated client exports createClient, everything, operation generators, request/result types, and type guards.

Runtime behavior

The default client uses Axios. You can pass any Axios request config to query and mutation calls, or replace the transport with fetcherMethod.

Headers can be static or a function:

const client = createClient({ url: 'https://api.example.com/graphql', headers: async () => ({ Authorization: `Bearer ${await getToken()}`, }), });

Batching is enabled with batch: true or with explicit options:

const client = createClient({ url: 'https://api.example.com/graphql', batch: { batchInterval: 40, maxBatchSize: 10, }, });

Subscriptions use graphql-ws and return observables. Browser, Node, and Bun clients can pass an explicit webSocketImpl. The runtime validates the shape before passing it through so SSR does not receive invalid browser-only values.

const client = createClient({ url: 'http://localhost:4000/graphql', subscription: { url: 'ws://localhost:4000/graphql', }, webSocketImpl: WebSocket, });

In Next.js SSR, create subscription clients only inside code paths that subscribe. Avoid reading window.WebSocket during module initialization.

Setup

Use Bun from the repo root.

bun ci

The repository uses Bun for local development and CI speed. Published packages still work for npm, pnpm, Yarn, and Bun consumers. bun.lock is the only committed package-manager lockfile.

Build and test commands

Root checks:

bun run buildall bun run test bun run typecheck bun run lint bun run format:check

Package checks:

bun run --cwd runtime build bun run --cwd runtime test bun run --cwd cli build bun run --cwd cli test bun run --cwd website build

Backend demo and SDK:

bun run --cwd demo-apps/backend dev bun run --cwd demo-apps/backend build-sdk bun run --cwd demo-apps/backend typecheck bun run --cwd demo-apps/backend build bun run --cwd demo-apps/backend start bun run --cwd demo-apps/backend test

Integration and generated-client examples:

bun run --cwd demo-apps/integration-tests gen bun run --cwd demo-apps/integration-tests test bun run --cwd demo-apps/try-clients build bun run --cwd demo-apps/try-clients test

Full demo CI procedure

Run this before pushing changes that touch generator output, runtime behavior, uploads, subscriptions, SDK generation, or Next.js behavior:

./demo-apps/build-and-test.sh

The script runs:

  1. Workspace install and schema generation through Bun’s TypeScript runtime.
  2. Backend SDK generation.
  3. Backend typecheck, build, production schema generation, and Bun tests.
  4. Standalone HTML bundle tests against the backend.
  5. Next.js app tests in dev mode.
  6. Next.js production build and tests against next start.
  7. Integration generation and tests.

Release workflow

Gqlts uses semantic-release for coordinated releases of @gqlts/runtime and @gqlts/cli. One release run computes the next version from conventional commits, stamps both package manifests, builds, and publishes both packages at the same version.

Useful commands:

bun run release:verify bun run release:dry bun run release:local 3.5.0-beta.1 --dry-run --tag beta

Branch behavior:

Contributor rules:

Test coverage map

Where to change things

Change checklist

Always finish with:

git diff --check

Troubleshooting

If tsgo reports TS5011, add an explicit rootDir to the tsconfig.json used by that package.

If describe, it, fs, path, process, or __dirname are missing during typecheck, import test helpers from bun:test, install @types/bun and @types/node in that package, and add "types": ["bun", "node"] to its tsconfig.json.

If Next.js warns about multiple lockfiles, verify there is no stray package-manager lockfile under a demo app. The repo should keep bun.lock at the root only.