Skip to content

feat(vscode): report GraphQLSP diagnostics through TypeScript 7's API sessions - #439

Open
JoviDeCroock wants to merge 2 commits into
feat/ts7-api-hostfrom
feat/vscode-ts7-api-session
Open

JoviDeCroock wants to merge 2 commits into
feat/ts7-api-hostfrom
feat/vscode-ts7-api-session

Conversation

@JoviDeCroock

@JoviDeCroock JoviDeCroock commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Stacked on #438 (base feat/ts7-api-host). Context: 0no-co/gql.tada#606 and 0no-co/gql.tada#610 handle .graphql files through a TypeScript 7.1 content mapper. This PR covers inline graphql() documents in the editor.

TypeScript 7's language server doesn't load the tsserver plugin, so with the TypeScript 7 VS Code extension, GraphQLSP's diagnostics are gone in the editor. With this PR, once TypeScript 7's extension initializes its language server, the GraphQLSP extension opens an API session of it. It runs #438's diagnostics core on the server's current snapshot, which includes unsaved editor changes, and publishes the results through a DiagnosticCollection. The TypeScript 5/6 plugin path is unchanged.

What changed

@0no-co/graphqlsp/check: createLanguageServerSession

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

const session = await createLanguageServerSession({
  packageJsonPath: string;      // TypeScriptSDK.packageJsonPath
  pipe: string;                 // TypeScriptSDK.initializeAPIConnection() / custom/initializeAPISession
  compiler?: 'typescript' | 'tsrs';
  logger?: (message: string) => void;
  persist?: boolean;            // write tadaOutputLocation typings; default true, like the plugin
  onSchemaChange?: () => void;  // a schema reloaded, so recheck open files
});

session.check(fileName): Promise<{ text: string; diagnostics: CheckDiagnostic[] } | null>;
session.dispose(): void;
  • check reads getCurrentLanguageServerSnapshot(), then getDefaultProjectForFile(). It reads the plugin entry from that project's tsconfig (following extends) and loads the schema once per project. Diagnostics use the same CheckDiagnostic shape as checkProject.
  • It returns null for files outside projects that configure GraphQLSP.
  • text is the file text the diagnostics were computed for. If it doesn't match the editor's text, the language server hasn't caught up yet, and the caller should retry.
  • CheckDiagnostic, the conversion helpers and line/column mapping moved to src/ts7/report.ts, so checkProject and the session share them.
  • loadSchema gained an onChange option.

The sync client can't use LSP pipes yet, so there's a relay (src/ts7/relay.ts)

The plan was API.fromLSPConnection({ pipe }) from typescript/sync. That fails in typescript@7.1.0-beta, and in today's typescript@next (7.1.0-dev.20261024.1), with "Socket connections are not yet supported in the sync client".

  • The language server's API sessions (handleInitializeAPISession in tsc/internal/lsp/server.go) only speak JSON-RPC over an async connection.
  • The sync client only speaks its MessagePack tuple protocol over the stdio of a process it spawns.
  • The async client supports pipes, but every checker call returns a Promise. GraphQLSP's diagnostics are synchronous.

So the session tries fromLSPConnection({ pipe }) first. When that throws "not yet supported", it passes a generated tsserverPath to the sync client instead:

  • That path is a shell script that runs a small relay (plain CommonJS, Node built-ins only) with the current Node or Electron binary.
  • The relay translates the sync client's [type, method, payload] tuples to JSON-RPC requests on the pipe.
  • For the 7 source-file methods (isSourceFileResponseMethod in tsc/internal/api/session.go), it decodes JSON-RPC's { data: base64 } back into raw bytes.

Once the sync client supports pipes, the relay drops out without changes. Windows isn't supported: the sync client can only spawn an executable there.

VS Code extension (packages/vscode-graphqlsp/src/typescript7/)

  • Gated on TypeScript 7. activateTypeScript7 looks for TypeScriptTeam.native-preview or TypeScriptTeam.vscode-typescript-nightly and subscribes to onLanguageServerInitialized. The released TypeScriptTeam.native-preview 1.0 calls that listener without a TypeScriptSDK and has initializeAPIConnection on its extension API. In that case the extension resolves the TypeScript installation from the same js/ts.tsdk.path / typescript.tsdk / typescript.native-preview.tsdk settings, falling back to the workspace's typescript@>=7. Nothing runs unless TypeScript 7's language server starts. On every (re)initialization, the previous session is discarded, as the extension API documents.
  • Out of process. Each initialization forks dist/typescript7/server.js with NODE_PATH pointing at the SDK's node_modules, and talks to it over IPC. The sync API blocks, so it must not run in the extension host. NODE_PATH matters because @gql.tada/internal does require('typescript') at import time, and the vsix doesn't ship typescript.
  • Diagnostics.
    • TS/JS documents are checked on open, on save, 250 ms after a change, and for all open documents after a schema change.
    • A result whose text doesn't match the document (the language server is lagging) is retried every 100 ms, up to 20 times.
    • Diagnostics are mapped through document.positionAt(start), with source graphqlsp.
  • Untouched. The typescriptServerPlugins contribution, configurePlugin and settings sync stay as they were. tsconfig.json gains "types": ["node"], because TypeScript 6 changed the types default to [].

Verification (local, macOS)

  • pnpm run test:e2e: 21 files, 114 passed and 3 skipped, on top of feat: check GraphQL documents with TypeScript 7.1 and tsrs (checkProject) #438 at b423a19. That includes 3 new session tests on TypeScript 7.1; the 3 skips are the tsrs legs.
  • test/e2e/ts7-session.test.ts spawns the real typescript@7.1.0-beta language server (--lsp --stdio), opens valid.ts with an unsaved nmae field and requests custom/initializeAPISession. It checks that:
    • the session reports Cannot query field "nmae"… at the unsaved text's position;
    • after a didChange back to the saved text, there are no diagnostics;
    • files outside the project return null.
  • tsrs: the test skips with tsrs@0.18, which answers MethodNotFound: custom/initializeAPISession is not ported yet. The CI typescript-7 job now also runs the session test.
  • Forked server:
    • Ran the built dist/typescript7/server.js against a live TS 7.1 language server over IPC. The first check returned in about 560 ms, schema load included, and valid.ts and invalid.ts gave the expected diagnostics.
    • Ran it again from the packaged vsix staging dir copied outside the repo, so typescript couldn't be resolved: it works with NODE_PATH, and without it fails with Cannot find module 'typescript' from @gql.tada/internal.
  • Builds: pnpm build and pnpm --filter vscode-graphqlsp build (tsc) pass, and pnpm --filter vscode-graphqlsp package builds the vsix.
  • Typecheck: tsc -p packages/graphqlsp --types node --module esnext --moduleResolution bundler shows no errors in new or changed files. The existing errors in untouched files are the same.
  • VS Code: see the next section.

Dogfooded in VS Code

Setup

  • VS Code 1.143.1 (darwin-arm64), launched through @vscode/test-electron.
  • TypeScriptTeam.native-preview 1.0.1 from the marketplace, plus the vsix built from this branch.
  • Workspace: a copy of fixture-project-ts7 with typescript@7.1.0-beta, gql.tada@1.11.4, and @gql.tada/content-mapper from feat(content-mapper): experimental field IntelliSense via lowered selections gql.tada#614 with intellisense: true.
  • Settings: js/ts.experimental.useTsgo: true and a global js/ts.tsdk.path. Workspace trust was disabled so content mappers run.
  • A test extension asserts on vscode.languages.getDiagnostics(), vscode.executeHoverProvider and vscode.executeCompletionItemProvider.

Bug found and fixed (a708e4d). The released extension calls the onLanguageServerInitialized listener with no argument. The TypeScriptSDK argument only exists in 7.1.0-beta's typescript/vscode types. Because of this, the first VS Code run threw Cannot read properties of undefined (reading 'packageJsonPath') and reported nothing. Both API shapes are handled now. One caveat remains: if TypeScript 7's extension ignores a workspace tsdk setting because the workspace version hasn't been approved, GraphQLSP may load the API client from a different installation than the server's.

Results after the fix

  • Inline diagnostics:
    • They appear 1.2 s after opening invalid.ts, cold, including the schema load. After reopening the window, it takes 0.9 s.
    • Unsaved edits are reflected in about 310 ms: a fix clears the error, a new typo is reported, fixing everything leaves only the deprecation warning, and reverting brings the error back. Most of that is the 250 ms debounce.
  • No double reporting:
    • invalid.ts, colocation.ts, field-usage.ts and broken.graphql have no duplicate diagnostics.
    • TS files only carry graphqlsp-source diagnostics. The built-in extension's tsserver plugin doesn't also report while useTsgo is on.
  • Restart: typescript.native-preview.restart clears the collection, and diagnostics are back 0.87 s after the restart.
  • Reopen: relaunching VS Code with the same user data brings back both the inline diagnostics and the content mapper's diagnostics.
  • Content mapper (gql.tada#614):
    • broken.graphql reports Cannot query field "nmae" at 4:5-4:9, and syntax.graphql reports its syntax error at 4:1. pokemon.graphql has none.
    • Hover on a field shows (property) 'name': "String!".
    • Completion after typing cl lists all 15 fields plus __typename.
    • The unsaved partial field is reported after 52 ms.
  • TypeScript 5.9: tested with both extensions installed and useTsgo off.
    • The classic plugin reports with source ts, at the same ranges as the TypeScript 7 path, and nothing comes from the graphqlsp collection. There are no duplicates.
    • Field ranges ending at the start of the next line (e.g. 7:7-8:1) are the plugin's existing behavior, so I kept them the same.
  • Logs:
    • The GraphQLSP output channel shows only the session connecting and the schema loading.
    • The TypeScript 7 log has three [error] lines, all from the deliberate restart: the API socket closed and the server exited.

Observed once: on the very first TypeScript 5.9 run, the classic plugin's diagnostics didn't show up until the first edit. I couldn't reproduce it in 3 later runs, 2 on this branch and 1 on main. This PR doesn't touch that path, and the plugin doesn't refresh diagnostics after the schema loads, so it looks like an existing race.

Not tested

  • Windows or Linux.
  • The vscode-typescript-nightly extension, and the TypeScriptSDK API shape inside a real extension. No released extension ships it yet; that shape is only covered by types.
  • Untrusted workspaces, and the workspace-tsdk approval prompt.
  • workbench.action.reloadWindow in the same process. I relaunched VS Code instead.
  • Schema file edits triggering a recheck in the editor.
  • tsrs.
  • Hover and completion inside inline graphql(). They aren't implemented: in VS Code, inline documents get no GraphQL hover and only TypeScript's completions.

Gaps and TODOs

  • Hover and completions (TODO). Not done. TypeScript 7's extension provides registerLspMiddleware(method, transformer) for textDocument/hover and textDocument/completion (plus textDocument/diagnostic, which could replace the separate DiagnosticCollection). Those transformers are async, and the session would need GraphQLSP's hover and completion entry points behind the host interface, like feat: check GraphQL documents with TypeScript 7.1 and tsrs (checkProject) #438 did for diagnostics.
  • TypeScript 7.1 API: the sync client can't connect to pipes. That's the reason for the relay, and why Windows isn't supported.
  • tsrs: custom/initializeAPISession isn't ported in tsrs@0.18.
  • @gql.tada/internal: the top-level require('typescript') forces the NODE_PATH workaround.
  • Settings: with TypeScript 7, only projects that configure the plugin in tsconfig.json are checked. The graphqlsp.* VS Code settings only reach the tsserver plugin.
  • Changeset: minor for @0no-co/graphqlsp. vscode-graphqlsp is in the changeset ignore list.

@changeset-bot

changeset-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a708e4d

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

Adds createLanguageServerSession to @0no-co/graphqlsp/check, which runs
GraphQLSP's diagnostics on a TypeScript 7.1 language server's snapshot
(including unsaved editor changes) through an API session pipe. Since the
7.1 sync client can't connect to pipes yet, it's bridged by a small relay
that translates the sync client's MessagePack stdio protocol to the
session's JSON-RPC.

The VS Code extension uses it when TypeScript 7's extension initializes
its language server, publishing diagnostics through a DiagnosticCollection
from a forked process. The TypeScript 5/6 plugin path is unchanged.
@JoviDeCroock
JoviDeCroock force-pushed the feat/vscode-ts7-api-session branch from 8465872 to 24265fb Compare October 24, 2026 09:52
TypeScriptTeam.native-preview 1.0 calls onLanguageServerInitialized's
listener without an argument and exposes initializeAPIConnection on its
extension API instead. The TypeScript installation is then resolved from
the same tsdk settings, falling back to the workspace's typescript@>=7.

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