feat: GraphQL LSP server with multi-schema support (MVP)#308
Conversation
Set up the new LSP package with package.json, tsconfig, and build config. Dependencies include graphql-language-service, vscode-languageserver, and @swc/core. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Define ExtractedTemplate, DocumentState, OperationKind types and LspError discriminated union with constructor helpers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement TS file ↔ GraphQL content position conversion with computeLineOffsets, positionToOffset, offsetToPosition utilities. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Strip Fragment Arguments RFC syntax by replacing argument lists with equal-length whitespace, preserving line/column alignment for graphql-js compatibility. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements SchemaResolver that maps schema names to GraphQLSchema objects using loadSchema/hashSchema from @soda-gql/codegen. Supports eager loading, per-schema reload, and full reload for file watcher integration. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements DocumentManager that parses TypeScript files with SWC and
extracts GraphQL tagged templates from gql.{schemaName} callback bodies.
Handles expression bodies, block bodies, and metadata chaining patterns.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements three LSP feature handlers using graphql-language-service: - computeTemplateDiagnostics: validates GraphQL against schema with TS position mapping - handleCompletion: provides field/argument autocompletion suggestions - handleHover: shows type information on hover All handlers integrate fragment-args preprocessing and position mapping. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements createLspServer that wires all components together via vscode-languageserver: config loading, schema resolution, document management, and feature handlers (diagnostics, completion, hover). Includes end-to-end integration tests validating the full flow. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds 'soda-gql lsp' command that starts the GraphQL LSP server over stdio. Uses dynamic import to avoid loading LSP dependencies for other commands. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use specific return types for error constructors instead of the union type, and exclude test fixtures from typecheck (they reference @/graphql-system which is a runtime alias). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add missing LSP package reference to root tsconfig.json (required for bun quality to pass). Apply auto-formatter fixes across LSP package. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Test Coverage ReportOverall Coverage: 80.4% (11319/14078 lines)
|
| const imported = specifier.imported ? specifier.imported.value : specifier.local.value; | ||
| if (imported === "gql" && !specifier.imported) { | ||
| identifiers.add(specifier.local.value); | ||
| } |
There was a problem hiding this comment.
Aliased gql imports not tracked by LSP
Medium Severity
The collectGqlIdentifiers function fails to track aliased imports like import { gql as g }. The condition imported === "gql" && !specifier.imported only matches direct imports, not renamed ones. When a user aliases gql, the local identifier won't be tracked, causing the LSP to miss all templates in that file. The existing builder package handles this case correctly.
There was a problem hiding this comment.
Won't Fix
Reason: By design - intentional behavior. The builder package actively reports renamed gql imports as RENAMED_IMPORT diagnostics. The LSP correctly aligns with this constraint by not tracking aliased identifiers. This is consistent across both SWC and TypeScript adapters in the builder.
| graphqlToTs: (gqlPosition) => { | ||
| const gqlOffset = positionToOffset(gqlLineOffsets, gqlPosition); | ||
| const tsOffset = gqlOffset + contentStartOffset; | ||
| return offsetToPosition(tsLineOffsets, tsOffset); |
There was a problem hiding this comment.
graphqlToTs may produce incorrect positions for invalid input
Low Severity
The graphqlToTs method doesn't validate that positionToOffset returns a valid result. If gqlPosition has an invalid line number, positionToOffset returns -1, but this value is then used directly: tsOffset = -1 + contentStartOffset. This could produce an incorrect but seemingly valid TS position, leading to misplaced diagnostic highlights.
There was a problem hiding this comment.
Won't Fix
Reason: Low impact - risk too low to address. The graphqlToTs method is only called with positions from getDiagnostics() which always returns valid positions within the GraphQL content. The theoretical scenario of invalid line numbers from the graphql-language-service library has no practical occurrence path.
…yped AST - Replace try-catch with neverthrow Result wrappers in schema-resolver and document-manager - Use Node.js fileURLToPath for cross-platform URI conversion - Replace standalone `any` with @swc/types Node type for AST traversal BugBot comments: - #308 (comment) - #308 (comment) - #308 (comment) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…tion Fixed findMatchingParen string escape detection to count consecutive backslashes before a quote character. An even count means the quote is unescaped (closes the string), while an odd count means it's escaped. BugBot comment: #308 (comment) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| } | ||
|
|
||
| return -1; | ||
| }; |
There was a problem hiding this comment.
Block string quotes break fragment argument preprocessing
Low Severity
The findMatchingParen function toggles string mode on each " character, which doesn't correctly handle GraphQL block strings ("""..."""). When a block string contains an odd number of embedded quote characters (e.g., """a"b"""), the string state tracking becomes desynchronized, causing the function to fail to find the closing parenthesis. This results in fragment arguments not being stripped, leading to false positive validation errors for that fragment.
There was a problem hiding this comment.
Won't Fix
Reason: Low impact - block strings do not appear in fragment argument positions in real-world query-side GraphQL. The preprocessor's failure mode (returning -1, skipping preprocessing) is safe.
Fixed contentStartOffset from 44 to 43 and added intermediate value assertion to prevent symmetric error cancellation from masking bugs. BugBot comment: #308 (comment) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
SWC returns span positions as UTF-8 byte offsets, but JavaScript string operations use UTF-16 code units. For files with non-ASCII characters (CJK, emoji, accented chars), this caused incorrect contentRange values, diagnostic positions, and template extraction offsets. Added createSwcSpanConverter utility in @soda-gql/common with: - ASCII fast path (zero allocation for ASCII-only files) - Pre-computed Uint32Array lookup for non-ASCII sources - Applied to both LSP document-manager and builder SWC adapter BugBot comment: #308 (comment) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
When schemaResolver.reloadAll() fails, show an error message via connection.window.showErrorMessage() instead of silently ignoring the error. BugBot comment: #308 (comment) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>


Summary
@soda-gql/lsppackage providing a GraphQL Language Server Protocol server for soda-gql's tagged template API (RFC RFC: GraphQL LSP with multi-schema support #307, Phase 0 + Phase 1)gql.{schemaName}(({ query }) => query...)callback patterns with multi-schema resolutiongraphql-language-servicevalidation)soda-gql lspcommand (stdio transport)Architecture
src/schema-resolver.tssrc/document-manager.tssrc/fragment-args-preprocessor.tssrc/position-mapping.tssrc/handlers/diagnostics.tssrc/handlers/completion.tssrc/handlers/hover.tssrc/server.tsNot included (deferred)
codegen lsp-configsubcommand (.graphqlrc.generated.jsongeneration) — separate PRTest plan
bun --conditions=@soda-gql test packages/lsp/— 60 tests passbun typecheck— no LSP-introduced errors (pre-existingpackages/coreerrors only)bun --conditions=@soda-gql packages/cli/src/index.ts lsp --help— prints help🤖 Generated with Claude Code