Skip to content

feat: check GraphQL documents with TypeScript 7.1 and tsrs (checkProject) - #438

Open
JoviDeCroock wants to merge 2 commits into
mainfrom
feat/ts7-api-host
Open

JoviDeCroock wants to merge 2 commits into
mainfrom
feat/ts7-api-host

Conversation

@JoviDeCroock

@JoviDeCroock JoviDeCroock commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

TypeScript 7 doesn't load language service plugins, so GraphQLSP's diagnostics currently don't run under tsc, the TypeScript 7 language server, or tsrs. This PR moves the diagnostics onto a small host interface and adds an adapter that runs them on the TypeScript 7.1 API. It's exposed as checkProject() from the new @0no-co/graphqlsp/check entry.

Context: 0no-co/gql.tada#606 and 0no-co/gql.tada#610 handle .graphql files through a TypeScript 7.1 content mapper. Content mappers can't register .ts/.tsx, so diagnostics for inline graphql() documents need this path. Stacked on top: #439 (the VS Code extension runs the same diagnostics through the TypeScript 7 extension's API session).

What changed

  • Host interface (src/host.ts). The diagnostics path (document discovery, cross-file fragment resolution, schema validation, colocation, unused fields, persisted checks) now takes a GraphQLSPHost instead of ts.server.PluginCreateInfo. Its checker is a HostChecker, which only exposes getSymbolAtLocation, getAliasedSymbol, getTypeAtLocation, getTypeOfSymbol, getShorthandAssignmentValueSymbol, getContextualType and getExportsOfModule. PluginCreateInfo satisfies the interface as-is, so the tsserver plugin passes its info along unchanged.
  • TypeScript 7.1 adapter (src/ts7/):
    • modules.ts is the only place that loads the API. It resolves typescript or tsrs from the project, or from compilerPath. typescript@7.1.0-beta exports typescript/sync, typescript/ast and typescript/ast/is, while tsrs@0.18 still uses tsrs/unstable/*; both layouts are detected from the package's exports. TypeScript 5/6 is detected too, and goes through its classic compiler API.
    • namespace.ts builds the ts namespace subset GraphQLSP uses from the API's AST modules. Enums are always taken from the loaded compiler: SyntaxKind values differ between TS 5/6, TS 7 and tsrs (SourceFile is 308 in typescript@7.1, 307 in tsrs@0.18).
    • checker.ts wraps the API checker; see the AST/API differences below.
    • config.ts reads the plugin entry from tsconfig.json. TypeScript 7 strips plugin options from parsed compilerOptions.plugins (only name survives), so raw configs are read with readConfigFile, following extends. The schema root is the directory of the config that declares the entry, like @gql.tada/internal.
  • loadSchema(info, logger, { rootPath, watch, persist }). Adds a one-shot mode (no watchers, no typings writes) and an awaitable schema.ready. The defaults are unchanged.
  • escapedText → text. The TypeScript 7.1 API's identifiers have no escapedText, so the six identifier comparisons now use text. On TS 5/6 the two only differ for __-prefixed names, where comparing escapedText (___x) against source text never matched, so the old behaviour there was already broken.

AST/API differences found (handled in the adapter)

TS 5/6 TS 7.1 API (typescript & tsrs)
symbol.declarations / valueDeclaration are nodes NodeHandles that must be .resolve(project)d
type.types, type.isUnionOrIntersection() type.getTypes(), isUnionType() / isIntersectionType()
type.symbol is a Symbol a compact reference (getSymbol())
typeRef.resolvedTypeArguments checker.getTypeArguments(type)
ts.forEachChild(node, cb) node.forEachChild(cb)
identifier.escapedText only identifier.text
isStringLiteralLike, isFunctionLike, isClassLike, isObjectLiteralElement, isParameter, isMethodSignature, isPropertySignature, isCommaListExpression, idText missing or renamed (isClassLikeDeclaration, isParameterDeclaration, …); there's no CommaListExpression

Otherwise the node shapes match Strada closely: kind, parent, pos/end, getStart(), getText(), getSourceFile() and named children all work. Node lists are arrays (RemoteNodeList).

checkProject API (for gql.tada's check CLI)

import { checkProject } from '@0no-co/graphqlsp/check';

checkProject(options: {
  tsconfigPath: string;
  compiler?: 'typescript' | 'tsrs';   // default 'typescript' (5, 6, or 7.1+)
  compilerPath?: string;              // package.json/dir; default: resolved from tsconfigPath
  files?: readonly string[];          // default: project files, excluding .d.ts and node_modules
  logger?: (message: string) => void;
}): Promise<{
  compiler: { name: 'typescript' | 'tsrs'; version: string };
  tsconfigPath: string;               // absolute
  files: string[];                    // absolute, checked files
  diagnostics: Array<{
    file: string;                     // absolute
    start: number; length: number;    // UTF-16 offsets
    line: number; column: number;     // 1-based
    message: string;
    code: number;                     // e.g. 52001 validation, 52003 colocation, 52004 deprecated, 52005 unused field
    severity: 'error' | 'warning' | 'suggestion' | 'message';
  }>;
}>

Plain data only, so it can be serialized as-is. Checks are queued, because GraphQLSP's modules share one ts namespace.

Tests and CI

  • test/e2e/fixture-project-ts7. An on-disk gql.tada project covering a cross-file fragment spread (valid.ts), schema errors and an unknown fragment, a deprecated field, a co-located fragment import that isn't spread, and unused fields. It installs typescript-7@npm:typescript@7.1.0-beta and tsrs@0.18.0-ts7.1.0-dev.20260929. They live in the fixture, not the root: at the root, the alias's tsc bin replaced the workspace's TypeScript 6 tsc.
  • test/e2e/ts7.test.ts. Runs checkProject with the workspace's TypeScript 5/6 (inline-snapshotted), then with TypeScript 7.1 and tsrs, and asserts their diagnostics are identical to TS 5/6's.
  • CI. A new typescript-7 job installs typescript@beta, typescript@next or tsrs@latest into the fixture and runs the parity tests. next and tsrs are continue-on-error. The existing TS 5.9/6.0 matrix also runs the parity tests against the pinned versions.

Verified locally:

  • pnpm run test:e2e: 19 files, 109 tests passed (104 existing + 5 new).
  • The CI TS 5.9.3 leg (set-typescript-version.mjs 5.9.3 plus the parity tests) passed in a clean worktree.
  • The typescript@next leg (it resolved 7.1.0-dev.20261023.1) passed.
  • The built dist/check.js (CJS) and dist/check.mjs (ESM) were smoke-tested from plain Node with TS 7.1 (about 1.2 s), tsrs (about 0.8 s) and TS 6 (about 3.2 s) on the fixture, all with the same 6 diagnostics.
  • Typecheck: tsc -p packages/graphqlsp --types node --module esnext reports no errors in new or changed code. The package tsconfig's existing errors are unchanged.

Known gaps

Moves GraphQLSP's diagnostics onto a small host interface (GraphQLSPHost),
which tsserver's PluginCreateInfo satisfies, and adds an adapter running them
on the TypeScript 7.1 API (typescript@>=7.1, tsrs), exposed as checkProject()
from @0no-co/graphqlsp/check.
@changeset-bot

changeset-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b423a19

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@0no-co/graphqlsp Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

checkProject now accepts runExternalCode, which is passed to the
TypeScript 7.1 API like tsc --runExternalCode, so documents imported from
content-mapped files (e.g. .graphql files mapped by @gql.tada/content-mapper)
are part of the program and can be resolved. Content-mapped files themselves
aren't checked, since their mappers report their diagnostics.

Persisted document hashes are now computed from string literals' values
rather than their source text, which failed to parse documents written
with escape sequences.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant