diff --git a/docs/rfcs/graphql-lsp-implementation-details.md b/docs/rfcs/graphql-lsp-implementation-details.md new file mode 100644 index 00000000..8560c746 --- /dev/null +++ b/docs/rfcs/graphql-lsp-implementation-details.md @@ -0,0 +1,591 @@ +# GraphQL LSP Implementation Details Report + +## Purpose + +This document supplements the [GraphQL LSP Multi-Schema RFC](./graphql-lsp-multi-schema.md) with deeper technical analysis of the hybrid LSP architecture. It covers what the `graphql-language-service` interface layer provides, what must be built from scratch for the callback + tagged template API (`gql.{schemaName}(({ query, fragment }) => query`...`)`) with Fragment Arguments RFC syntax, and what opportunities exist for soda-gql-specific extensions beyond standard GraphQL LSP features. + +## Table of Contents + +- [1. LSP Protocol Requirements](#1-lsp-protocol-requirements) +- [2. graphql-language-service Interface Layer Analysis](#2-graphql-language-service-interface-layer-analysis) +- [3. What Must Be Built from Scratch](#3-what-must-be-built-from-scratch) +- [4. soda-gql-Specific Extension Opportunities](#4-soda-gql-specific-extension-opportunities) +- [5. Risk Analysis](#5-risk-analysis) + +--- + +## 1. LSP Protocol Requirements + +### 1.1 Server lifecycle + +Every LSP server must implement the following lifecycle: + +| Phase | Request/Notification | Description | +|-------|---------------------|-------------| +| Handshake | `initialize` | Client sends capabilities, server responds with its own | +| Ready | `initialized` | Client confirms initialization | +| Running | (various) | Handle requests and notifications | +| Shutdown | `shutdown` | Client requests graceful shutdown | +| Exit | `exit` | Client notifies server to terminate | + +The `initialize` response declares which capabilities the server supports. This is where the server selectively opts into features: + +```typescript +connection.onInitialize((params: InitializeParams): InitializeResult => { + return { + capabilities: { + textDocumentSync: TextDocumentSyncKind.Incremental, + completionProvider: { triggerCharacters: ['.', '{', '(', ':', '@', '$', '\n', ' '] }, + hoverProvider: true, + definitionProvider: true, + referencesProvider: true, + documentSymbolProvider: true, + codeActionProvider: true, + // Advanced (see section 4) + codeLensProvider: { resolveProvider: true }, + inlayHintProvider: true, + semanticTokensProvider: { /* ... */ }, + }, + }; +}); +``` + +### 1.2 Document synchronization (mandatory) + +The server must track document state. Three notifications are required: + +| Notification | Trigger | Purpose | +|-------------|---------|---------| +| `textDocument/didOpen` | File opened in editor | Register document, run initial diagnostics | +| `textDocument/didChange` | User edits file | Update cached content, re-parse tagged templates, re-run diagnostics | +| `textDocument/didClose` | File closed | Clean up cached state | + +`vscode-languageserver` provides a `TextDocuments` manager that handles this automatically: + +```typescript +import { TextDocuments } from 'vscode-languageserver/node'; +import { TextDocument } from 'vscode-languageserver-textdocument'; + +const documents = new TextDocuments(TextDocument); +documents.listen(connection); +``` + +### 1.3 Feature categories by priority + +**Tier 1 — Core (MVP)** + +| LSP Method | Purpose | Provided by graphql-language-service? | +|-----------|---------|---------------------------------------| +| `textDocument/completion` | Field, argument, type, directive autocomplete | ✅ `getAutocompleteSuggestions()` | +| `textDocument/publishDiagnostics` | Syntax errors, validation errors, deprecation warnings | ✅ `getDiagnostics()` | +| `textDocument/hover` | Type information, field descriptions, deprecation notices | ✅ `getHoverInformation()` | + +**Tier 2 — Navigation** + +| LSP Method | Purpose | Provided by graphql-language-service? | +|-----------|---------|---------------------------------------| +| `textDocument/definition` | Jump to type definition in schema, fragment definition | ✅ `getDefinitionQueryResultFor*()` (multiple variants) | +| `textDocument/references` | Find all usages of a fragment or type | ❌ Custom implementation needed | +| `textDocument/documentSymbol` | Outline view (operations, fragments) | ✅ `getOutline()` | + +**Tier 3 — Productivity** + +| LSP Method | Purpose | Provided by graphql-language-service? | +|-----------|---------|---------------------------------------| +| `textDocument/codeAction` | Quick fixes, extract fragment | ❌ Custom implementation needed | +| `textDocument/rename` | Rename fragment, rename variable | ❌ Custom implementation needed | +| `textDocument/signatureHelp` | Show argument signatures for fields | ❌ Custom implementation needed | +| `textDocument/formatting` | Format GraphQL content in tagged templates | ❌ Custom (could delegate to prettier) | +| `completionItem/resolve` | Lazy-load detailed completion info | Partial (data already available) | + +**Tier 4 — Advanced (soda-gql extensions)** + +| LSP Method | Purpose | Standard GraphQL LSP? | +|-----------|---------|----------------------| +| `textDocument/codeLens` | Show operation metadata, schema info | ❌ Not in existing GraphQL LSPs | +| `textDocument/inlayHint` | Inline type annotations, argument names | ❌ Not in existing GraphQL LSPs | +| `textDocument/semanticTokens` | Enhanced syntax highlighting | ❌ Not in existing GraphQL LSPs | +| Custom notifications | Schema reload, build status, multi-schema context | N/A (non-standard) | + +--- + +## 2. graphql-language-service Interface Layer Analysis + +### 2.1 Package overview + +The `graphql-language-service` package (v5.5.0) is the official, runtime-independent interface layer. Previously split into `graphql-language-service-interface`, `-parser`, `-utils`, and `-types`, now consolidated into a single package. + +**Key characteristic**: All functions are **stateless**. They take a `GraphQLSchema`, query text, and position as input and return results. No server lifecycle, no file management, no config loading. This makes them ideal for embedding in a custom LSP server. + +### 2.2 Core function signatures + +#### `getAutocompleteSuggestions` + +```typescript +function getAutocompleteSuggestions( + schema: GraphQLSchema, + queryText: string, + cursor: IPosition, // { line: number, character: number } + contextToken?: ContextTokenForCodeMirror, + fragmentDefs?: FragmentDefinitionNode[] | string, + options?: AutocompleteSuggestionOptions, +): CompletionItem[]; +``` + +- `schema`: The GraphQL schema to suggest against. **For soda-gql: must be the correct schema for the tagged template's import path.** +- `queryText`: The raw GraphQL string extracted from the tagged template. +- `cursor`: Position within the GraphQL string (not the TS file — offset mapping required). +- `fragmentDefs`: External fragment definitions that can be referenced. **For soda-gql: fragments from other files need to be collected.** +- Returns `CompletionItem[]` with `label`, `kind`, `documentation`, `insertText`, `detail`. + +#### `getDiagnostics` + +```typescript +function getDiagnostics( + query: string, + schema?: GraphQLSchema | null, + customRules?: ValidationRule[], + isRelayCompatMode?: boolean, + externalFragments?: FragmentDefinitionNode[] | string, +): Diagnostic[]; +``` + +- Works without a schema (syntax-only validation), but full validation requires one. +- `customRules`: Allows injecting soda-gql-specific validation rules (see section 4). +- `externalFragments`: Same as autocomplete — fragments from other files. + +#### `getHoverInformation` + +```typescript +function getHoverInformation( + schema: GraphQLSchema, + queryText: string, + cursor: IPosition, + contextToken?: ContextToken, + config?: GraphQLConfig, +): Hover['contents']; +``` + +- Returns markdown-formatted hover content. +- Shows type name, field description, deprecation reason. + +#### `getOutline` + +```typescript +function getOutline(queryText: string): OutlineTree; +``` + +- Pure parsing, no schema needed. +- Returns a tree structure usable for `textDocument/documentSymbol`. + +#### Definition helpers + +```typescript +function getDefinitionQueryResultForField( + fieldName: string, typeName: string, sourceDocument: DocumentNode, filePath: string +): DefinitionQueryResult; + +function getDefinitionQueryResultForFragmentSpread( + fragmentName: string, sourceDocument: DocumentNode, filePath: string +): DefinitionQueryResult; + +function getDefinitionQueryResultForNamedType( + typeName: string, sourceDocument: DocumentNode, filePath: string +): DefinitionQueryResult; +``` + +- These return position ranges within GraphQL documents. +- For soda-gql, schema-level definitions would need the schema SDL file path. + +#### Utility functions + +```typescript +// Position conversion +function offsetToPosition(text: string, offset: number): IPosition; +function pointToOffset(text: string, point: IPosition): number; + +// AST helpers +function getASTNodeAtPosition(query: string, ast: DocumentNode, point: IPosition): ASTNode | undefined; +function getContextAtPosition(query: string, schema: GraphQLSchema, point: IPosition): { token, state, typeInfo, mode }; +function getTypeInfo(schema: GraphQLSchema, tokenState: State): TypeInfo; + +// Fragment analysis +function getFragmentDependencies(query: string, fragmentDefinitions?: Map): FragmentDefinitionNode[]; + +// Variable schema +function getVariablesJSONSchema(variableToType: Record): JSONSchema6; +``` + +### 2.3 What the interface layer does NOT provide + +| Concern | Status | What you must build | +|---------|--------|---------------------| +| LSP protocol handling | ❌ | Server setup, connection, message routing (`vscode-languageserver`) | +| Document synchronization | ❌ | Track open files, incremental updates (`TextDocuments` manager) | +| File parsing | ❌ | Extract GraphQL from tagged templates in TypeScript files | +| Position mapping | ❌ | Convert TS file positions to GraphQL document positions and back | +| Config loading | ❌ | Read `soda-gql.config.ts`, resolve schemas | +| Schema loading | ❌ | Load `.graphql` files, build `GraphQLSchema` objects | +| Schema caching | ❌ | Cache schemas, invalidate on file change | +| Multi-schema routing | ❌ | Resolve which schema applies to each tagged template | +| Cross-file fragments | ❌ | Collect fragment definitions from other files for completion/validation | +| File watching | ❌ | Watch schema files and config for changes | +| `textDocument/references` | ❌ | Find all usages of a fragment/type across workspace | +| `textDocument/codeAction` | ❌ | Quick fixes, refactorings | +| `textDocument/rename` | ❌ | Symbol renaming | +| `textDocument/formatting` | ❌ | Format GraphQL in templates | + +--- + +## 3. What Must Be Built from Scratch + +### 3.1 Tagged template extractor + +The most critical custom component. It must: + +1. **Parse TypeScript AST via SWC** to find `gql.{schemaName}(callback)` call expressions +2. **Identify the callback**: Extract the arrow function or function expression argument +3. **Find tagged templates inside the callback body**: Match `query`/`mutation`/`subscription`/`fragment` tagged template expressions +4. **Trace imports**: Follow the `gql` identifier back to its import declaration, validate it resolves to the graphql-system output +5. **Extract content**: Get the raw GraphQL string from the template +6. **Preprocess Fragment Arguments**: Strip fragment argument declarations for `graphql-js` compatibility +7. **Compute offset map**: Map between TS file positions and GraphQL content positions + +Complexity factors: +- Two-level AST traversal: first find `gql.{name}(callback)`, then find tagged templates inside the callback +- Template literals can span multiple lines with varying indentation +- The GraphQL content starts after the opening backtick, possibly with leading whitespace +- Must handle re-exports and aliased imports (`import { gql } from ...` with path aliases) +- Must handle TypeScript path aliases (`@/graphql-system` → actual file path) +- Fragment Arguments RFC syntax (`fragment Foo($var: Type) on Bar`) must be stripped before `graphql-js` parsing +- Metadata chaining (`fragment`...`({ metadata })`) — the tagged template result may be called + +**Reuse opportunity**: soda-gql's `@soda-gql/builder` already has a SWC-based AST analyzer (`packages/builder/src/discovery/`) that finds `gql.{schemaName}()` calls. The tagged template extractor extends this to also find tagged templates inside callback bodies — the same `gql.{schemaName}(callback)` pattern, with additional inspection of the callback's body. + +### 3.2 Position mapping + +When the user's cursor is at line 8, column 12 in a TypeScript file, and the tagged template starts at line 5, column 28, the LSP needs to translate this to the corresponding position within the GraphQL string. + +``` +TS file position (8, 12) → GraphQL position (3, 12) +``` + +This mapping must be bidirectional: +- **TS → GraphQL**: For forwarding editor requests (completion, hover) to `graphql-language-service` +- **GraphQL → TS**: For converting diagnostics and definition locations back to the editor + +Edge cases: +- Indentation stripping (common in tagged templates) +- The backtick character itself (not part of GraphQL content) +- Escaped characters in template strings (rare but possible) + +### 3.3 Schema management + +```typescript +type SchemaCache = Map; +``` + +Requirements: +- Load schemas from paths specified in `soda-gql.config.ts` +- Build `GraphQLSchema` objects using `graphql-js`'s `buildSchema()` or `buildASTSchema()` +- Cache schemas and invalidate when source files change +- Support multiple schemas simultaneously (one per config entry) + +**Reuse opportunity**: `@soda-gql/codegen` already has `loadSchema()` (`packages/codegen/src/schema.ts`) and `hashSchema()` functions. + +### 3.4 Cross-file fragment resolution + +When a tagged template references a fragment defined in another file: + +```typescript +// user-fields.ts +export const UserFields = gql.default` + fragment UserFields on User { id name email } +`; + +// get-user.ts +import { UserFields } from './user-fields'; +const GetUser = gql.default` + query GetUser($id: ID!) { + user(id: $id) { ...UserFields } + } +`; +``` + +The LSP must: +1. Detect `...UserFields` in the query +2. Resolve the TypeScript import `'./user-fields'` +3. Find the `UserFields` tagged template in that file +4. Extract the fragment's `DocumentNode` +5. Pass it to `getAutocompleteSuggestions()` and `getDiagnostics()` as `externalFragments` + +This requires a **workspace-level index** of all fragment definitions and their locations. + +### 3.5 LSP handler wiring + +Each LSP method requires a handler that: +1. Receives the LSP request (with TS file URI and position) +2. Finds the tagged template at that position +3. Maps the position to GraphQL coordinates +4. Calls the appropriate `graphql-language-service` function +5. Maps the result back to TS file coordinates +6. Returns the LSP response + +Example for completion: + +```typescript +connection.onCompletion((params: CompletionParams): CompletionItem[] => { + const doc = documents.get(params.textDocument.uri); + if (!doc) return []; + + const state = documentManager.getState(doc.uri); + const template = state.findTemplateAtPosition(params.position); + if (!template) return []; + + const graphqlPosition = positionMapper.tsToGraphql( + params.position, + template.range, + ); + + const schema = schemaResolver.getSchema(template.schemaName); + if (!schema) return []; + + const fragments = fragmentIndex.getFragmentsForSchema(template.schemaName); + + return getAutocompleteSuggestions( + schema, + template.content, + graphqlPosition, + undefined, + fragments, + ); +}); +``` + +--- + +## 4. soda-gql-Specific Extension Opportunities + +This section describes features that go beyond what any existing GraphQL LSP provides. Building a custom LSP opens the door to these soda-gql-specific enhancements. + +### 4.1 Inlay hints for inferred types + +LSP 3.17 introduced `textDocument/inlayHint`. No existing GraphQL LSP implements this. + +soda-gql could show inferred return types and variable types inline: + +```typescript +const GetUser = gql.default` + query GetUser($id: ID!) { + user(id: $id) { // ← inlay: ": User" + id // ← inlay: ": ID!" + name // ← inlay: ": String!" + posts(first: 10) { // ← inlay: ": [Post!]!" + title // ← inlay: ": String!" + } + } + } +`; +``` + +This eliminates the need to hover each field individually — the type information is always visible. + +**Implementation**: Use `getTypeInfo()` from `graphql-language-service` to resolve types at each field position, then emit `InlayHint[]` with type annotations. + +### 4.2 Code lens for operation metadata + +`textDocument/codeLens` can show actionable information above operations: + +```typescript +// ▸ Schema: admin | Fields: 12 | Depth: 3 | Est. complexity: 24 +const GetAdmin = gql.admin` + query GetAdmin { ... } +`; +``` + +Possible code lens items: +- **Schema name**: Which schema this operation targets (critical for multi-schema projects) +- **Field count**: Total number of selected fields +- **Query depth**: Nesting depth of the selection set +- **Estimated complexity**: Based on schema-defined complexity weights +- **Run query**: Open in GraphiQL or execute against a dev server (command-based) + +**Implementation**: Parse the operation, walk the selection set, compute metrics, return `CodeLens[]` with custom commands. + +### 4.3 Custom diagnostics: soda-gql validation rules + +`getDiagnostics()` accepts `customRules: ValidationRule[]`. soda-gql can inject its own rules: + +| Rule | Description | Severity | +|------|-------------|----------| +| `NoExcludedTypes` | Warn when querying types excluded by `typeFilter` in config | Warning | +| `InputDepthLimit` | Warn when input nesting exceeds `defaultInputDepth` or overrides | Warning | +| `FragmentSchemaMatch` | Error when spreading a fragment from a different schema | Error | +| `FragmentArgumentsValidation` | Validate Fragment Arguments RFC syntax (variable types, default values, usage at spread sites) | Error | +| `OperationNaming` | Enforce naming conventions for operations | Info | +| `DeprecatedFieldUsage` | Enhanced deprecation warnings with migration hints | Warning | +| `UnusedFragment` | Warn about fragments defined but not spread anywhere | Warning | + +Example implementation: + +```typescript +import { ValidationContext, ASTVisitor } from 'graphql'; + +const NoExcludedTypesRule = ( + excludedTypes: Set, +) => (context: ValidationContext): ASTVisitor => ({ + NamedType(node) { + if (excludedTypes.has(node.name.value)) { + context.reportError( + new GraphQLError( + `Type "${node.name.value}" is excluded by typeFilter in soda-gql config.`, + { nodes: [node] }, + ), + ); + } + }, +}); +``` + +### 4.4 Schema-aware semantic tokens + +`textDocument/semanticTokens` provides enhanced syntax highlighting beyond TextMate grammars. + +soda-gql could provide schema-aware token types: + +| Token | Semantic Type | Modifier | +|-------|--------------|----------| +| Query field name | `property` | `declaration` | +| Deprecated field | `property` | `deprecated` | +| Scalar type | `type` | — | +| Enum value | `enumMember` | — | +| Fragment name | `function` | `declaration` | +| Variable | `variable` | — | +| Directive | `decorator` | — | + +This would enable visual differentiation between, e.g., deprecated and non-deprecated fields directly in the editor's color scheme, without requiring a hover. + +**Implementation**: Walk the GraphQL AST, resolve types via schema, emit semantic token ranges mapped back to TS file positions. + +### 4.5 Multi-schema context indicator + +A custom LSP notification to inform the editor which schema is active for the current cursor position: + +```typescript +// Custom notification: soda-gql/activeSchema +connection.sendNotification('soda-gql/activeSchema', { + uri: document.uri, + schemaName: 'admin', + schemaPath: './schemas/admin/schema.graphql', +}); +``` + +The VSCode extension could display this in the status bar: + +``` +[GraphQL: admin] ← shown in status bar when cursor is inside a gql.admin tagged template +``` + +This is particularly valuable in multi-schema projects where different parts of the same file may target different schemas. + +### 4.6 Automatic compat migration + +A code action that converts `.graphql` file content to a tagged template: + +``` +// Before (in .graphql file): +query GetUser($id: ID!) { + user(id: $id) { id name } +} + +// Code action: "Convert to soda-gql tagged template" + +// After (in .ts file): +import { gql } from "@/graphql-system"; + +export const GetUserCompat = gql.default(({ query }) => query` + query GetUser($id: ID!) { + user(id: $id) { id name } + } +`); +``` + +This leverages the existing graphql-compat parser (`packages/codegen/src/graphql-compat/parser.ts`) but runs it interactively via LSP code actions instead of CLI codegen. + +### 4.7 `workspace/executeCommand` for build integration + +Custom commands executable via the LSP: + +| Command | Description | +|---------|-------------| +| `soda-gql.reloadSchemas` | Force reload all schemas from disk | +| `soda-gql.generateLspConfig` | Generate `.graphqlrc.generated.json` | +| `soda-gql.showSchemaInfo` | Display schema statistics in a panel | +| `soda-gql.switchSchema` | Change the default schema for new templates | +| `soda-gql.validateWorkspace` | Run full workspace validation | + +These integrate with the existing `@soda-gql/cli` and `@soda-gql/sdk` packages. + +### 4.8 Variables JSON schema generation + +`graphql-language-service` exports `getVariablesJSONSchema()` which generates a JSON Schema from operation variables. soda-gql could expose this to provide: + +- **Variable completion**: When typing variable values in tests or playground +- **Variable validation**: Ensure variable values match expected types +- **Documentation generation**: Auto-generate variable documentation for operations + +--- + +## 5. Risk Analysis + +### 5.1 Complexity risks + +| Risk | Likelihood | Impact | Mitigation | +|------|-----------|--------|------------| +| Position mapping bugs (off-by-one, indentation) | High | Medium | Extensive unit tests with edge cases; snapshot testing | +| Cross-file fragment resolution performance | Medium | High | Lazy indexing; file-level caching; incremental updates | +| TypeScript parser maintenance burden | Medium | Medium | Use `@swc/core` for speed; keep parser minimal (tagged template extraction only) | +| `graphql-language-service` API changes | Low | Medium | Pin version; review changelogs on update | +| Large schema performance (1000+ types) | Medium | High | Lazy schema loading; index only reachable types (reuse `reachability.ts`) | + +### 5.2 Scope risks + +The biggest risk is scope creep. Tier 4 features (section 4) are exciting but should not delay the MVP. Recommended approach: + +1. **MVP**: Tier 1 (completion, diagnostics, hover) + basic multi-schema routing +2. **V1**: Add Tier 2 (definition, document symbols, references) +3. **V2**: Add Tier 3 (code actions, rename, formatting) + select Tier 4 features +4. **V3+**: Full Tier 4 (inlay hints, code lens, semantic tokens, custom commands) + +### 5.3 Dependencies + +For the hybrid approach, the dependency chain is: + +``` +@soda-gql/lsp +├── vscode-languageserver (~200KB) +├── vscode-languageserver-textdocument (~15KB) +├── graphql-language-service (~150KB, depends on graphql) +├── graphql (~800KB, already a project dependency) +├── @soda-gql/config (internal) +└── typescript or @swc/core (for TS AST parsing) +``` + +Total new dependencies: `vscode-languageserver`, `vscode-languageserver-textdocument`, `graphql-language-service`. The `graphql` package is already used by `@soda-gql/codegen`. + +--- + +## References + +- [LSP Specification 3.17](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/) +- [vscode-languageserver-node](https://github.com/microsoft/vscode-languageserver-node) +- [VSCode Language Server Extension Guide](https://code.visualstudio.com/api/language-extensions/language-server-extension-guide) +- [graphql-language-service TypeDoc](https://graphiql-test.netlify.app/typedoc/modules/graphql_language_service) +- [graphql-language-service-server README](https://github.com/graphql/graphiql/blob/main/packages/graphql-language-service-server/README.md) +- [graphql-language-service npm](https://www.npmjs.com/package/graphql-language-service) +- [@0no-co/GraphQLSP](https://github.com/0no-co/GraphQLSP) diff --git a/docs/rfcs/graphql-lsp-multi-schema.md b/docs/rfcs/graphql-lsp-multi-schema.md new file mode 100644 index 00000000..5d3da786 --- /dev/null +++ b/docs/rfcs/graphql-lsp-multi-schema.md @@ -0,0 +1,801 @@ +# RFC: GraphQL LSP with Multi-Schema Support + +## Status + +**Draft** - Direction finalized + +## Summary + +This RFC defines the design for an independent GraphQL Language Server Protocol (LSP) implementation for soda-gql. The LSP provides IDE features (autocomplete, diagnostics, hover, go-to-definition) for GraphQL operations written as tagged template literals in TypeScript files. + +The design commits to these key decisions: + +1. **Callback + Tagged Template API**: `gql.{schemaName}` always receives a callback. Inside the callback, `query`/`mutation`/`subscription`/`fragment` serve as tagged template tags. The tagged template result is callable for metadata chaining. +2. **Fragment Arguments RFC syntax**: Fragment variables are declared using the [GraphQL Fragment Arguments proposal](https://github.com/graphql/graphql-spec/pull/1081) syntax directly in GraphQL. +3. **Hybrid LSP Architecture**: Custom LSP server using `vscode-languageserver` for protocol handling and `graphql-language-service` (interface layer only) for completion/diagnostics/hover algorithms. SWC for TypeScript AST parsing. + +## Motivation + +### Multi-schema conflicts with `.graphql` files + +The unified codegen pipeline enables reuse of existing `.graphql` assets by generating `.compat.ts` files. However, when a project uses multiple schemas (e.g., `default` and `admin`), there is no ergonomic way to associate each `.graphql` file with its corresponding schema. + +Tools like `graphql-config` solve this with path-based schema routing: + +```yaml +# .graphqlrc.yaml +projects: + default: + schema: ./schemas/default/schema.graphql + documents: ./src/default/**/*.graphql + admin: + schema: ./schemas/admin/schema.graphql + documents: ./src/admin/**/*.graphql +``` + +This approach is **implicit and fragile**: the schema association depends on directory structure rather than explicit declaration. Moving a file silently breaks validation. Developers must remember which directories map to which schemas. + +### TypeScript Language Service Plugin deprecation + +`@0no-co/graphqlsp` (the TypeScript LSP plugin powering `gql.tada`) provides excellent IDE support for GraphQL in TypeScript. However, TypeScript 7 (tsgo/Corsa) will not support the Language Service Plugin API. The existing JS-based plugin architecture fundamentally does not translate to the new Go-based implementation. Building on TS Language Service Plugins is not future-proof. + +References: +- [Transformer Plugin or Compiler API - typescript-go #516](https://github.com/microsoft/typescript-go/issues/516) (proposal removed) +- [What will happen to the existing TypeScript/JavaScript codebase? - Discussion #454](https://github.com/microsoft/typescript-go/discussions/454) + +### No IDE support for the existing composer API + +The current `gql.{schemaName}(() => ...)` API provides type-safe field selection at the TypeScript level but has no GraphQL-aware IDE features (no schema-driven autocomplete, no GraphQL validation diagnostics, no hover documentation for fields). + +### Goals + +1. **Explicit schema association**: Each GraphQL document explicitly declares which schema it targets +2. **IDE features**: Autocomplete, diagnostics, hover, and go-to-definition for GraphQL in TypeScript +3. **Editor-agnostic**: Works with any LSP-compatible editor (VSCode, Neovim, Sublime, etc.) +4. **Future-proof**: Independent of TypeScript Language Service Plugin API +5. **Zero-runtime preserved**: All IDE features are dev-time only; no runtime overhead + +## Background & Context + +### Current soda-gql architecture + +``` +soda-gql.config.ts + schemas: { default: {...}, admin: {...} } + │ + ├─► [Codegen] ─► graphql-system/index.ts (typed composer + runtime) + │ exports: gql.default(), gql.admin() + │ + ├─► [GraphQL Compat] ─► .compat.ts files (from .graphql files) + │ + └─► [Builder] ─► Static analysis of gql.{schema}() calls + │ + └─► [Plugin] ─► Build-time transformation to runtime artifacts +``` + +Key packages: +- `@soda-gql/config`: Configuration loading (`soda-gql.config.ts`) +- `@soda-gql/codegen`: Schema code generation with reachability filtering +- `@soda-gql/builder`: Static analysis engine (TypeScript AST and SWC) +- `@soda-gql/core`: Runtime types, composer, fragment/operation definitions + +### GraphQL LSP ecosystem + +Two main approaches exist for providing GraphQL IDE features: + +1. **graphql-language-service-server** (official): A standalone LSP server built on `graphql-config`. Supports tagged templates in JS/TS files. Part of the [graphql/graphiql](https://github.com/graphql/graphiql) monorepo. + +2. **@0no-co/graphqlsp**: A TypeScript Language Service Plugin. Hooks into tsserver for tighter TS integration. Powers `gql.tada`. Not viable for tsgo/TS7. + +### Why tagged template literals + +Tagged template literals provide a natural syntax for embedding GraphQL in TypeScript. By combining the existing `gql.{schemaName}(callback)` pattern with tagged templates inside the callback, the API gains GraphQL-aware IDE support while preserving soda-gql's architecture: + +```typescript +import { gql } from "@/graphql-system"; + +// operation: query tagged template inside callback +const GetUser = gql.default(({ query }) => query` + query GetUser($id: ID!) { + user(id: $id) { id name } + } +`); + +// fragment: with Fragment Arguments RFC syntax for variables +const UserFields = gql.default(({ fragment }) => fragment` + fragment UserFields($showEmail: Boolean = false) on User { + id + email @include(if: $showEmail) + } +`); + +// existing callback API (unchanged, coexists) +const GetUser2 = gql.default(({ query, $var }) => + query.operation({ + name: "GetUser", + variables: { ...$var("id").ID("!") }, + fields: ({ f, $ }) => ({ + ...f.user({ id: $.id })(({ f }) => ({ id: true, name: true })), + }), + }) +); +``` + +Advantages: +- **Schema explicit via `gql.{schemaName}`**: `gql.default` vs `gql.admin` — unambiguous schema association +- **Operation kind explicit via tag name**: `query`, `fragment`, `mutation`, `subscription` — type-level distinction +- **Callback preserves intermediate module compatibility**: `gql.{schemaName}` always receives a function +- **Parser-friendly**: LSP extracts tagged templates from callback bodies via SWC AST analysis +- **Syntax highlighting**: Editor extensions (e.g., vscode-graphql-syntax) already support GraphQL in tagged templates via TextMate grammar injection + +## Design Decisions + +### 4.1 Tagged Template API + +The tagged template API extends the existing `gql.{schemaName}(callback)` composer pattern. The callback context provides `query`/`mutation`/`subscription`/`fragment` that serve as both tagged template tags (new) and existing API method hosts. Both styles produce the same build-time artifacts and runtime behavior. + +#### API design + +```typescript +import { gql } from "@/graphql-system"; + +// --- Operations --- + +// query +const GetUser = gql.default(({ query }) => query` + query GetUser($id: ID!) { + user(id: $id) { id name email } + } +`); + +// mutation +const UpdateUser = gql.default(({ mutation }) => mutation` + mutation UpdateUser($id: ID!, $name: String!) { + updateUser(id: $id, name: $name) { id name } + } +`); + +// subscription +const OnMessage = gql.default(({ subscription }) => subscription` + subscription OnMessage($roomId: ID!) { + messageAdded(roomId: $roomId) { id text sender } + } +`); + +// --- Fragments --- + +// basic fragment +const UserFields = gql.default(({ fragment }) => fragment` + fragment UserFields on User { + id + name + email + } +`); + +// fragment with variables (Fragment Arguments RFC syntax) +const UserProfile = gql.default(({ fragment }) => fragment` + fragment UserProfile($showEmail: Boolean = false) on User { + id + name + email @include(if: $showEmail) + } +`); + +// fragment with metadata chaining +const PostList = gql.default(({ fragment }) => fragment` + fragment PostList($first: Int!) on Query { + posts(first: $first) { id title } + } +`({ + metadata: { pagination: true }, +})); + +// --- Existing callback API (unchanged, coexists) --- + +const GetUser2 = gql.default(({ query, $var }) => + query.operation({ + name: "GetUser", + variables: { ...$var("id").ID("!") }, + fields: ({ f, $ }) => ({ + ...f.user({ id: $.id })(({ f }) => ({ id: true, name: true })), + }), + }) +); +``` + +#### Design rationale + +- **`gql.{schemaName}` always receives a callback**: Required by the intermediate module system. The builder's static analysis depends on `gql.{schemaName}(callback)` call expressions. +- **Tag names encode operation kind**: `query`/`mutation`/`subscription`/`fragment` in the callback distinguish the element type at both the TypeScript type level and the AST level. This eliminates ambiguity that a bare `gql.default`...`` would have. +- **Metadata chaining via call**: The tagged template result is callable, enabling metadata attachment (e.g., `fragment`...`({ metadata: ... })`). This is extensible for future metadata needs without changing the GraphQL syntax. + +#### Fragment Arguments RFC syntax + +This RFC adopts the [Fragment Arguments proposal (graphql-spec #1081)](https://github.com/graphql/graphql-spec/pull/1081) for declaring fragment variables. This is a Stage 2 GraphQL spec proposal that adds variable declarations to fragment definitions: + +```graphql +# Standard GraphQL: no fragment variables +fragment UserFields on User { + id + name +} + +# Fragment Arguments RFC: variables declared on the fragment +fragment UserProfile($showEmail: Boolean = false) on User { + id + name + email @include(if: $showEmail) +} + +# Usage with arguments +query GetUser($id: ID!) { + user(id: $id) { + ...UserProfile(showEmail: true) + } +} +``` + +This syntax is chosen because: +- **Natural**: Variable declarations look identical to operation variable declarations +- **Self-contained**: Fragment variables are declared in the GraphQL string itself, no TypeScript-level API needed +- **Future-proof**: If the spec proposal is accepted, soda-gql's syntax becomes standard GraphQL + +**`graphql-js` compatibility**: The current `graphql-js` parser does not accept Fragment Arguments syntax. The LSP preprocesses fragment definitions by stripping argument declarations before passing to `graphql-language-service` functions (`getDiagnostics()`, `getAutocompleteSuggestions()`). Fragment argument validation is handled by custom soda-gql validation rules. + +#### Type structure + +```typescript +// query/mutation/subscription: tagged template tag + existing API methods +type QueryTag = { + // Tagged template (new) + (strings: TemplateStringsArray, ...values: never[]): ChainableOperation; + // Existing callback API + operation: (...) => AnyOperation; + compat: (...) => AnyGqlDefine; +}; + +// fragment: tagged template tag + existing per-type builders +type FragmentTag = { + // Tagged template (new) + (strings: TemplateStringsArray, ...values: never[]): ChainableFragment; + // Existing callback API (fragment.User, fragment.Post, ...) + readonly [TypeName in keyof Schema["object"]]: FragmentBuilderFor<...>; +}; + +// Tagged template results are callable for metadata chaining +type ChainableOperation = AnyOperation & { + (options: { metadata?: Record }): AnyOperation; +}; + +type ChainableFragment = AnyFragment & { + (options: { metadata?: Record }): AnyFragment; +}; +``` + +`FragmentTag` has dual nature: it is callable as a tagged template tag, and it has properties for the existing `fragment.User(...)` builder API. At runtime, this is implemented via `Object.assign(tagFn, fragmentBuilders)` or `Proxy`. + +#### Runtime behavior + +Key constraints: +- **`gql.{schemaName}` always receives a callback**: The callback is invoked with a context containing `query`/`mutation`/`subscription`/`fragment` tags plus existing API members (`$var`, `$dir`, `$colocate`, etc.). +- **No interpolation**: `` query`...${expr}...` `` is a type error (`never[]` enforces this). Interpolation would break static analysis and LSP autocomplete. +- **Build-time extraction**: The builder plugin detects tagged templates inside `gql.{schemaName}(callback)` calls via SWC AST analysis and transforms them into the same artifact format as existing callback API calls. +- **Runtime transformation**: At build time, tagged templates are replaced with `createRuntimeOperation` / `createRuntimeFragment` calls (same codepath as existing API). + +#### Type inference (Open Question) + +Two approaches for type-safe results from tagged templates: + +**Approach 1: Codegen-generated TypedDocumentNode** (graphql-codegen pattern) +```typescript +const GetUser = gql.default(({ query }) => query` + query GetUser($id: ID!) { user(id: $id) { id name } } +`); +// Type: TypedDocumentNode +``` + +**Approach 2: soda-gql $infer pattern** +```typescript +const GetUser = gql.default(({ query }) => query` + query GetUser($id: ID!) { user(id: $id) { id name } } +`); +type Data = typeof GetUser.$infer.output; +type Vars = typeof GetUser.$infer.input; +``` + +This decision is deferred to implementation phase. See [Open Questions](#open-questions). + +### 4.2 Schema association mechanism + +Schema association is resolved through `gql.{schemaName}` and its import path: + +1. **Codegen generates per-schema entries**: The `gql` object exposes schema-specific members (`gql.default`, `gql.admin`) +2. **LSP resolves tag to schema name**: When the LSP encounters `gql.{name}(callback)`, it extracts `{name}` as the schema identifier +3. **Config provides the mapping**: The existing `schemas: Record` in `soda-gql.config.ts` defines available schemas. The existing `graphqlSystemAliases` config (e.g., `["@/graphql-system"]`) provides alias resolution. + +Resolution algorithm: +``` +1. Find gql.{name}(callback) call expression via SWC AST +2. Extract schema name from member expression (e.g., gql.default → "default") +3. Validate that the gql import resolves to the graphql-system output: + - outdir (e.g., "./graphql-system") + - graphqlSystemAliases[i] (e.g., "@/graphql-system") + - Handle tsconfig paths aliases if tsconfigPath is configured +4. Match schema name against config schemas: Record +5. Inside the callback body, find tagged templates (query`...`, fragment`...`, etc.) +6. For each tagged template, associate with the resolved schema +``` + +### 4.3 LSP architecture: Hybrid approach + +The LSP server uses a hybrid architecture that combines existing battle-tested algorithms from `graphql-language-service` with custom infrastructure for soda-gql's specific needs. + +``` +soda-gql LSP Server +├── vscode-languageserver (LSP protocol handling) +├── graphql-language-service (completion/diagnostics/hover logic only) +│ └── getAutocompleteSuggestions(), getDiagnostics(), getHoverInformation() +├── SWC-based TS parser (tagged template extraction from callbacks) +├── Fragment Arguments preprocessor (strip before graphql-js validation) +├── soda-gql config loader (reads soda-gql.config.ts directly) +└── Schema resolver (gql.{schemaName} → schema mapping) +``` + +#### Why Hybrid + +The hybrid approach strikes the right balance between implementation effort and architectural control: + +- **graphql-language-service interface layer** provides stateless, schema-aware completion/diagnostics/hover functions that are proven across thousands of projects. Reimplementing these algorithms from scratch would add significant effort with no benefit. +- **Custom LSP server** (not wrapping `graphql-language-service-server`) avoids the forced dependency on `graphql-config`, which would create config duplication with `soda-gql.config.ts`. Reading soda-gql config directly ensures a single source of truth. +- **SWC-based TS parser** provides fast AST analysis for extracting tagged templates from callback bodies. SWC is already used by soda-gql's builder and offers significantly better parse performance than the TypeScript compiler API, which matters for an always-on LSP server. +- **Fragment Arguments preprocessor** is required regardless of LSP approach, since `graphql-js` does not support the Fragment Arguments syntax. The preprocessor strips argument declarations before passing to `graphql-language-service` functions. + +#### Characteristics + +**What graphql-language-service provides** (stateless functions, no server lifecycle): +- `getAutocompleteSuggestions()`: Field, argument, type, directive autocomplete +- `getDiagnostics()`: Syntax and validation errors with custom rule injection +- `getHoverInformation()`: Type information, field descriptions, deprecation notices +- `getOutline()`: Document symbols (operations, fragments) +- `getDefinitionQueryResultFor*()`: Go-to-definition helpers + +**What we build ourselves**: +- LSP server lifecycle and protocol handling (`vscode-languageserver`) +- SWC-based TypeScript file parsing and tagged template extraction from callbacks +- Fragment Arguments preprocessing (strip before `graphql-js` validation) +- Position mapping (TS file coordinates ↔ GraphQL document coordinates) +- Multi-schema resolution (`gql.{schemaName}` → schema lookup) +- Config loading from `soda-gql.config.ts` +- Schema caching and reload +- Cross-file fragment resolution +- `.graphql` file support + +### 4.4 Configuration integration + +The LSP reads `soda-gql.config.ts` directly using the existing `@soda-gql/config` package. No separate LSP-specific configuration file is needed. + +For external tool compatibility (graphql-eslint, GraphiQL), a `.graphqlrc.generated.json` can be auto-generated: + +```bash +bun run soda-gql codegen lsp-config +``` + +This generates a `graphql-config`-compatible file from `soda-gql.config.ts`: + +```json +{ + "projects": { + "default": { + "schema": "./schemas/default/schema.graphql", + "documents": "src/**/*.{ts,tsx}" + }, + "admin": { + "schema": "./schemas/admin/schema.graphql", + "documents": "src/**/*.{ts,tsx}" + } + } +} +``` + +The generated file should be gitignored. The LSP itself does **not** read this file — it exists solely for third-party tool compatibility. + +## Detailed Design + +### Package structure + +A new package `@soda-gql/lsp` will be created in `packages/lsp/`: + +``` +packages/lsp/ +├── src/ +│ ├── server.ts # LSP server entry point +│ ├── document-manager.ts # SWC-based TS parsing, tagged template extraction +│ ├── schema-resolver.ts # gql.{schemaName} → schema resolution +│ ├── fragment-args.ts # Fragment Arguments preprocessor +│ ├── handlers/ +│ │ ├── completion.ts # textDocument/completion +│ │ ├── diagnostics.ts # textDocument/publishDiagnostics +│ │ ├── hover.ts # textDocument/hover +│ │ └── definition.ts # textDocument/definition +│ └── utils/ +│ └── position-mapping.ts # TS offset ↔ GraphQL offset conversion +├── test/ +└── package.json +``` + +Dependencies: +- `vscode-languageserver` / `vscode-languageserver-textdocument`: LSP protocol +- `graphql-language-service`: Completion, diagnostics, hover algorithms +- `graphql`: Parsing and validation +- `@swc/core`: TypeScript AST parsing for tagged template extraction +- `@soda-gql/config`: Config loading + +### Core components + +#### Document Manager + +Parses TypeScript files via SWC to extract tagged templates from `gql.{schemaName}(callback)` calls: + +```typescript +type ExtractedTemplate = { + /** Range within the TS file (for offset mapping) */ + range: { start: number; end: number }; + /** Resolved schema name (from gql.{schemaName}) */ + schemaName: string; + /** Operation kind (from tag name: query/mutation/subscription/fragment) */ + kind: "query" | "mutation" | "subscription" | "fragment"; + /** Raw GraphQL content (without template tag) */ + content: string; + /** GraphQL content with Fragment Arguments stripped (for graphql-js) */ + preprocessedContent: string; +}; + +type DocumentState = { + uri: string; + version: number; + templates: ExtractedTemplate[]; +}; +``` + +The document manager: +1. Parses the TypeScript file into an SWC AST +2. Finds all `gql.{name}(callback)` call expressions +3. Extracts the schema name from the member expression (`gql.default` → `"default"`) +4. Inside the callback body, finds tagged template expressions where the tag is `query`, `mutation`, `subscription`, or `fragment` +5. Extracts the GraphQL string content +6. Preprocesses Fragment Arguments syntax (strips argument declarations for `graphql-js` compatibility) +7. Caches results per document (invalidated on change) + +#### Schema Resolver + +Maps schema names to `GraphQLSchema` objects using the soda-gql config: + +```typescript +type SchemaEntry = { + name: string; + schema: GraphQLSchema; + documentNode: DocumentNode; + matchPaths: string[]; // All import paths that resolve to this schema's gql object +}; +``` + +Builds `matchPaths` from config: +- `{outdir}` (e.g., `./graphql-system`) — the `gql` import source +- `{alias}` for each `graphqlSystemAliases` entry (e.g., `@/graphql-system`) +- Handles tsconfig `paths` resolution if `tsconfigPath` is configured + +#### Fragment Arguments Preprocessor + +Transforms Fragment Arguments RFC syntax into standard GraphQL for `graphql-js` compatibility: + +``` +Input: fragment UserProfile($showEmail: Boolean = false) on User { ... } +Output: fragment UserProfile on User { ... } + +Input: ...UserProfile(showEmail: true) +Output: ...UserProfile +``` + +The preprocessor: +1. Strips variable declarations from fragment definitions: `($showEmail: Boolean = false)` → removed +2. Strips arguments from fragment spreads: `(showEmail: true)` → removed +3. Preserves all position information for offset mapping back to the original source + +Custom soda-gql validation rules handle fragment argument validation separately (see Implementation Details report, section 4.3). + +#### Position Mapping + +Converts between positions in the TypeScript file and positions within the GraphQL document: + +``` +TypeScript file: + Line 5: const q = gql.default(({ query }) => query` + Line 6: query GetUser { ← cursor at (6, 8) + Line 7: user { id } + Line 8: } + Line 9: `); + +GraphQL document (extracted): + Line 1: query GetUser { ← mapped to (1, 8) + Line 2: user { id } + Line 3: } +``` + +The mapping accounts for: +- Template literal start offset (after backtick) +- Leading whitespace / indentation +- Multi-line template strings + +### CLI integration + +New CLI command for starting the LSP server: + +```bash +# Start LSP server (typically invoked by editor, not manually) +bun run soda-gql lsp + +# Generate graphql-config for external tools +bun run soda-gql codegen lsp-config +``` + +### Editor integration + +#### VSCode + +A dedicated extension (`soda-gql-vscode`) that: +1. Bundles the LSP server +2. Starts the server when a `soda-gql.config.ts` is detected in the workspace +3. Injects GraphQL syntax highlighting into TypeScript files (via TextMate grammar) + +```jsonc +// Extension activation +{ + "activationEvents": [ + "workspaceContains:**/soda-gql.config.{ts,mts,js,mjs}" + ], + "contributes": { + "grammars": [{ + "scopeName": "inline.graphql", + "path": "./syntaxes/graphql.tmLanguage.json", + "injectTo": ["source.ts", "source.tsx"] + }] + } +} +``` + +#### Neovim + +Configuration via `nvim-lspconfig`: + +```lua +require('lspconfig.configs').soda_gql = { + default_config = { + cmd = { 'soda-gql', 'lsp' }, + filetypes = { 'typescript', 'typescriptreact' }, + root_dir = require('lspconfig.util').root_pattern('soda-gql.config.ts'), + } +} +require('lspconfig').soda_gql.setup{} +``` + +## Implementation Plan + +### Phase 0: Schema resolver and config extension + +- Implement `schema-resolver.ts`: `gql.{schemaName}` → schema resolution using soda-gql config +- Extend config types if needed (e.g., per-schema `importPaths` overrides) +- Add `codegen lsp-config` CLI command to generate `.graphqlrc.generated.json` +- Unit tests for schema resolution with aliases and tsconfig paths + +### Phase 1: LSP server core + +- Set up `@soda-gql/lsp` package +- Implement SWC-based document manager (find `gql.{name}(callback)` → extract tagged templates from callback body) +- Implement Fragment Arguments preprocessor +- Implement position mapping (TS offset ↔ GraphQL offset) +- Wire up LSP handlers using `graphql-language-service` interface functions: + - `textDocument/completion` → `getAutocompleteSuggestions()` + - `textDocument/publishDiagnostics` → `getDiagnostics()` +- Integration tests with sample multi-schema projects + +### Phase 2: VSCode extension + +- Scaffold VSCode extension project +- Bundle LSP server +- Add GraphQL syntax highlighting injection for tagged templates +- Test activation, completion, and diagnostics end-to-end + +### Phase 3: Advanced LSP features + +- `textDocument/hover` → `getHoverInformation()` +- `textDocument/definition` (navigate to schema type definitions) +- Schema file watching and auto-reload +- Fragment cross-file resolution +- Fragment Arguments validation (custom rules) + +### Phase 4: Ecosystem integration + +- Neovim / Sublime integration guides +- `.graphql` file support (in addition to tagged templates) +- Migration guide from `.graphql` files to tagged templates +- graphql-eslint compatibility via generated `.graphqlrc` + +## Backward Compatibility + +This proposal introduces **no breaking changes**: + +- **Existing `gql.{schemaName}(callback)` API**: Fully preserved, no changes required +- **`.graphql` files with graphql-compat**: Continue to work via `codegen graphql` command +- **Existing config**: `soda-gql.config.ts` schema is extended with optional fields only; old configs work without LSP features + +All styles can coexist in the same project: + +```typescript +// Style 1: Callback composer with field builders (existing) +const q1 = gql.default(({ query, $var }) => + query.operation({ + name: "GetUser", + variables: { ...$var("id").ID("!") }, + fields: ({ f, $ }) => ({ ...f.user({ id: $.id })(...) }), + }) +); + +// Style 2: Callback with tagged template (new) +const q2 = gql.default(({ query }) => query` + query GetUser($id: ID!) { user(id: $id) { id name } } +`); + +// Style 3: .graphql file with compat (existing) +// → auto-generated .compat.ts +``` + +## Alternatives Considered + +### Bare tagged template without callback + +```typescript +const GetUser = gql.default`query GetUser { user { id } }`; +``` + +**Rejected**: `gql.{schemaName}` must always receive a callback for intermediate module compatibility. Additionally, bare tagged templates cannot distinguish between fragments and operations at the TypeScript type level, and cannot provide context members like `$var` or `$dir`. + +### Tagged template: Schema-specific subpath imports (Pattern A) + +```typescript +import { graphql } from "@/graphql-system/default"; +import { graphql as adminGraphql } from "@/graphql-system/admin"; + +const GetUser = graphql`query GetUser { user { id } }`; +``` + +**Rejected**: Requires separate imports per schema, leading to import proliferation and aliasing. Does not preserve the callback pattern required by the intermediate module system. + +### Tagged template: Separate `graphql` identifier (Pattern B) + +```typescript +import { graphql } from "@/graphql-system"; + +const GetUser = graphql.default`query GetUser { user { id } }`; +``` + +**Rejected**: Introduces a separate `graphql` identifier that doesn't integrate with the existing `gql.{schemaName}(callback)` API. Does not preserve the callback requirement. + +### Fragment variables via Relay-style directives + +```graphql +fragment UserFields on User + @argumentDefinitions(showEmail: { type: "Boolean", defaultValue: false }) { + id + email @include(if: $showEmail) +} +``` + +**Rejected**: Verbose syntax. Type names must be written as strings (`"Boolean"` instead of `Boolean`). The Fragment Arguments RFC syntax is cleaner and has a path to standardization. + +### Fragment variables via TypeScript API (callback metadata) + +```typescript +const UserFields = gql.default(({ fragment, $var }) => fragment` + fragment UserFields on User { ... } +`({ + variables: { showEmail: $var("showEmail").Boolean() }, +})); +``` + +**Rejected**: Splits variable declarations between GraphQL (usage via `$showEmail`) and TypeScript (declaration via `$var`). Fragment Arguments RFC syntax keeps everything in GraphQL, making the code self-contained and enabling LSP validation of the complete fragment definition. + +### LSP: Wrap graphql-language-service-server + +Use `graphql-language-service-server`'s `startServer()` with custom `parseDocument` for tagged template extraction. + +**Rejected**: Forces dependency on `graphql-config` for schema management, creating config duplication with `soda-gql.config.ts`. The hybrid approach gets the same completion/diagnostics quality by using the interface layer directly, without the server-layer constraints. + +### LSP: Full from-scratch implementation + +Use only `vscode-languageserver` + `graphql-js`, implementing all completion/diagnostics/hover logic from scratch. + +**Rejected**: The `graphql-language-service` interface layer provides stateless, battle-tested algorithms. Reimplementing from scratch would be significant effort with no architectural benefit. + +### TypeScript Language Service Plugin + +Build as a TS plugin (like `@0no-co/graphqlsp`) instead of a standalone LSP server. + +**Rejected**: TypeScript 7 (tsgo/Corsa) drops Language Service Plugin support. This approach has no future. + +### Enhanced `.graphqlrc.yaml` with path-based routing + +**Rejected**: Fundamentally implicit — schema association depends on directory structure, not explicit declaration. + +### Comment-based schema directives in `.graphql` files + +```graphql +# @soda-gql schema: admin +query GetAdmin { ... } +``` + +**Rejected**: Comments are not first-class constructs. Easy to forget, no compile-time validation. + +## Open Questions + +### Type inference strategy + +How should tagged templates provide TypeScript types? + +- **Option A**: Codegen-generated `TypedDocumentNode` (like graphql-codegen client-preset) +- **Option B**: Build-time type inference via the builder plugin (preserving soda-gql's `$infer` pattern) +- **Option C**: No type inference from tagged templates (users choose callback API for type safety) + +### Fragment cross-file resolution + +When a tagged template references a fragment defined in another file: + +```typescript +// fragments.ts +export const UserFields = gql.default(({ fragment }) => fragment` + fragment UserFields on User { id name } +`); + +// queries.ts +import { UserFields } from "./fragments"; +const GetUser = gql.default(({ query }) => query` + query GetUser { user { ...UserFields } } +`); +``` + +How should the LSP resolve `...UserFields`? +- **Option A**: Follow TypeScript imports to find fragment definitions +- **Option B**: Workspace-wide scan for all fragment definitions +- **Option C**: Hybrid (follow imports first, fall back to workspace scan) + +### Schema reload strategy + +When schema files change during development: +- **Option A**: File watcher with auto-reload +- **Option B**: Manual reload via editor command +- **Option C**: Both (auto-reload with manual override) + +## References + +### soda-gql internals +- Config types: `packages/config/src/types.ts` +- GQL composer: `packages/core/src/composer/gql-composer.ts` +- GraphQL compat emitter: `packages/codegen/src/graphql-compat/emitter.ts` +- Builder flow: `docs/guides/builder-flow.md` + +### GraphQL ecosystem +- [graphql-language-service-server](https://github.com/graphql/graphiql/tree/main/packages/graphql-language-service-server) +- [graphql-language-service (interface)](https://github.com/graphql/graphiql/tree/main/packages/graphql-language-service) +- [Fragment Arguments RFC (graphql-spec #1081)](https://github.com/graphql/graphql-spec/pull/1081) +- [graphql-tag-pluck](https://the-guild.dev/graphql/tools/docs/graphql-tag-pluck) +- [@0no-co/GraphQLSP](https://github.com/0no-co/GraphQLSP) +- [gql.tada multi-schema mode](https://gql-tada.0no.co/devlog/2024-04-26) + +### LSP protocol +- [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/) +- [vscode-languageserver-node](https://github.com/microsoft/vscode-languageserver-node) + +### TypeScript future +- [typescript-go](https://github.com/microsoft/typescript-go) +- [Transformer Plugin issue #516](https://github.com/microsoft/typescript-go/issues/516) (removed) +- [Progress on TypeScript 7 - December 2025](https://devblogs.microsoft.com/typescript/progress-on-typescript-7-december-2025/)