polen 0.10.0-next.21 → 0.10.0-next.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/build/api/builder/builder.d.ts +4 -11
  2. package/build/api/builder/builder.d.ts.map +1 -1
  3. package/build/api/builder/builder.js +4 -3
  4. package/build/api/builder/builder.js.map +1 -1
  5. package/build/api/config/configurator.d.ts +62 -11
  6. package/build/api/config/configurator.d.ts.map +1 -1
  7. package/build/api/config/configurator.js +9 -0
  8. package/build/api/config/configurator.js.map +1 -1
  9. package/build/api/config/merge.d.ts.map +1 -1
  10. package/build/api/config/merge.js +8 -0
  11. package/build/api/config/merge.js.map +1 -1
  12. package/build/api/vite/plugins/core.d.ts.map +1 -1
  13. package/build/api/vite/plugins/core.js +1 -0
  14. package/build/api/vite/plugins/core.js.map +1 -1
  15. package/build/cli/commands/build.js +11 -7
  16. package/build/cli/commands/build.js.map +1 -1
  17. package/build/project-data.d.ts +1 -0
  18. package/build/project-data.d.ts.map +1 -1
  19. package/build/sandbox.js +40 -17
  20. package/build/sandbox.js.map +1 -1
  21. package/build/template/components/CodeBlock.d.ts.map +1 -1
  22. package/build/template/components/CodeBlock.js +3 -5
  23. package/build/template/components/CodeBlock.js.map +1 -1
  24. package/build/template/components/Field.js +1 -1
  25. package/build/template/components/Field.js.map +1 -1
  26. package/build/template/components/GraphQLInteractive/GraphQLInteractive.d.ts +31 -0
  27. package/build/template/components/GraphQLInteractive/GraphQLInteractive.d.ts.map +1 -0
  28. package/build/template/components/GraphQLInteractive/GraphQLInteractive.js +275 -0
  29. package/build/template/components/GraphQLInteractive/GraphQLInteractive.js.map +1 -0
  30. package/build/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.d.ts +39 -0
  31. package/build/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.d.ts.map +1 -0
  32. package/build/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.js +51 -0
  33. package/build/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.js.map +1 -0
  34. package/build/template/components/GraphQLInteractive/components/GraphQLTokenPopover.d.ts +33 -0
  35. package/build/template/components/GraphQLInteractive/components/GraphQLTokenPopover.d.ts.map +1 -0
  36. package/build/template/components/GraphQLInteractive/components/GraphQLTokenPopover.js +242 -0
  37. package/build/template/components/GraphQLInteractive/components/GraphQLTokenPopover.js.map +1 -0
  38. package/build/template/components/GraphQLInteractive/hooks/use-popover-state.d.ts +45 -0
  39. package/build/template/components/GraphQLInteractive/hooks/use-popover-state.d.ts.map +1 -0
  40. package/build/template/components/GraphQLInteractive/hooks/use-popover-state.js +176 -0
  41. package/build/template/components/GraphQLInteractive/hooks/use-popover-state.js.map +1 -0
  42. package/build/template/components/GraphQLInteractive/index.d.ts +2 -0
  43. package/build/template/components/GraphQLInteractive/index.d.ts.map +1 -0
  44. package/build/template/components/GraphQLInteractive/index.js +2 -0
  45. package/build/template/components/GraphQLInteractive/index.js.map +1 -0
  46. package/build/template/components/GraphQLInteractive/lib/graphql-node-types.d.ts +52 -0
  47. package/build/template/components/GraphQLInteractive/lib/graphql-node-types.d.ts.map +1 -0
  48. package/build/template/components/GraphQLInteractive/lib/graphql-node-types.js +34 -0
  49. package/build/template/components/GraphQLInteractive/lib/graphql-node-types.js.map +1 -0
  50. package/build/template/components/GraphQLInteractive/lib/parser.d.ts +71 -0
  51. package/build/template/components/GraphQLInteractive/lib/parser.d.ts.map +1 -0
  52. package/build/template/components/GraphQLInteractive/lib/parser.js +836 -0
  53. package/build/template/components/GraphQLInteractive/lib/parser.js.map +1 -0
  54. package/build/template/components/GraphQLInteractive/lib/semantic-nodes.d.ts +98 -0
  55. package/build/template/components/GraphQLInteractive/lib/semantic-nodes.d.ts.map +1 -0
  56. package/build/template/components/GraphQLInteractive/lib/semantic-nodes.js +31 -0
  57. package/build/template/components/GraphQLInteractive/lib/semantic-nodes.js.map +1 -0
  58. package/build/template/components/content/$$.d.ts +0 -1
  59. package/build/template/components/content/$$.d.ts.map +1 -1
  60. package/build/template/components/content/$$.js +0 -1
  61. package/build/template/components/content/$$.js.map +1 -1
  62. package/package.json +5 -21
  63. package/src/api/builder/builder.ts +8 -13
  64. package/src/api/config/configurator.ts +72 -11
  65. package/src/api/config/merge.ts +13 -0
  66. package/src/api/vite/plugins/core.ts +1 -0
  67. package/src/cli/commands/build.ts +11 -7
  68. package/src/lib/kit-temp.test.ts +9 -9
  69. package/src/project-data.ts +1 -0
  70. package/src/sandbox.ts +40 -17
  71. package/src/template/components/CodeBlock.tsx +6 -9
  72. package/src/template/components/Field.tsx +1 -1
  73. package/src/template/components/GraphQLInteractive/GraphQLInteractive.tsx +464 -0
  74. package/src/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.tsx +96 -0
  75. package/src/template/components/GraphQLInteractive/components/GraphQLTokenPopover.tsx +492 -0
  76. package/src/template/components/GraphQLInteractive/hooks/use-popover-state.ts +244 -0
  77. package/src/template/components/GraphQLInteractive/index.ts +1 -0
  78. package/src/template/components/GraphQLInteractive/lib/graphql-node-types.ts +217 -0
  79. package/src/template/components/GraphQLInteractive/lib/parser.ts +1075 -0
  80. package/src/template/components/GraphQLInteractive/lib/semantic-nodes.ts +154 -0
  81. package/src/template/components/GraphQLInteractive/tests/parser-comment.test.ts +33 -0
  82. package/src/template/components/GraphQLInteractive/tests/parser-error-hint.test.ts +102 -0
  83. package/src/template/components/GraphQLInteractive/tests/parser.test.ts +131 -0
  84. package/src/template/components/content/$$.ts +0 -1
  85. package/build/template/components/content/GraphQLDocumentWithSchema.d.ts +0 -8
  86. package/build/template/components/content/GraphQLDocumentWithSchema.d.ts.map +0 -1
  87. package/build/template/components/content/GraphQLDocumentWithSchema.js +0 -13
  88. package/build/template/components/content/GraphQLDocumentWithSchema.js.map +0 -1
  89. package/build/template/components/content/GraphQLDocumentWrapper.d.ts +0 -7
  90. package/build/template/components/content/GraphQLDocumentWrapper.d.ts.map +0 -1
  91. package/build/template/components/content/GraphQLDocumentWrapper.js +0 -48
  92. package/build/template/components/content/GraphQLDocumentWrapper.js.map +0 -1
  93. package/src/template/components/content/GraphQLDocumentWithSchema.tsx +0 -13
  94. package/src/template/components/content/GraphQLDocumentWrapper.tsx +0 -72
@@ -0,0 +1,1075 @@
1
+ /**
2
+ * Tree-sitter GraphQL parsing with semantic analysis
3
+ *
4
+ * This module combines tree-sitter syntax parsing with GraphQL semantic
5
+ * analysis to create unified tokens for interactive code blocks.
6
+ */
7
+
8
+ import type { CodeAnnotation } from 'codehike/code'
9
+ import {
10
+ getNamedType,
11
+ type GraphQLArgument,
12
+ GraphQLEnumType,
13
+ type GraphQLField,
14
+ GraphQLInputObjectType,
15
+ GraphQLInterfaceType,
16
+ GraphQLObjectType,
17
+ type GraphQLOutputType,
18
+ GraphQLScalarType,
19
+ type GraphQLSchema,
20
+ GraphQLUnionType,
21
+ isInterfaceType,
22
+ isObjectType,
23
+ } from 'graphql'
24
+ import graphqlWasmUrl from 'tree-sitter-graphql-grammar-wasm/grammar.wasm?url'
25
+ import * as WebTreeSitter from 'web-tree-sitter'
26
+ import treeSitterWasmUrl from 'web-tree-sitter/web-tree-sitter.wasm?url'
27
+ import {
28
+ isKeywordNodeType,
29
+ isLiteralNodeType,
30
+ isPunctuationNodeType,
31
+ type TreeSitterGraphQLNodeType,
32
+ } from './graphql-node-types.js'
33
+ import type { SemanticNode } from './semantic-nodes.js'
34
+ import {
35
+ isArgument,
36
+ isFragment,
37
+ isInputField,
38
+ isInvalidField,
39
+ isOperation,
40
+ isOutputField,
41
+ isVariable,
42
+ } from './semantic-nodes.js'
43
+
44
+ /**
45
+ * Unified token structure that combines tree-sitter and GraphQL semantics
46
+ *
47
+ * This interface represents a single parsed token from a GraphQL document,
48
+ * enriched with semantic information from the GraphQL schema when available.
49
+ * It provides a consistent API for accessing syntax highlighting, interactivity,
50
+ * and CodeHike annotation data.
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * const tokens = await parseGraphQLWithTreeSitter(code, [], schema)
55
+ * const fieldToken = tokens.find(t => t.text === 'name')
56
+ *
57
+ * if (fieldToken?.polen.isInteractive()) {
58
+ * const url = fieldToken.polen.getReferenceUrl()
59
+ * console.log(`Navigate to: ${url}`)
60
+ * }
61
+ * ```
62
+ */
63
+ export interface GraphQLToken {
64
+ /** Reference to the tree-sitter node that this token represents */
65
+ treeSitterNode: WebTreeSitter.Node
66
+
67
+ /**
68
+ * Optional semantic information from GraphQL schema analysis
69
+ * This includes type information, field definitions, and validation results
70
+ */
71
+ semantic?: SemanticNode
72
+
73
+ /** Text content of the token (computed from tree-sitter node) */
74
+ get text(): string
75
+
76
+ /** Start character position in the source code (computed from tree-sitter node) */
77
+ get start(): number
78
+
79
+ /** End character position in the source code (computed from tree-sitter node) */
80
+ get end(): number
81
+
82
+ /** Polen specific functionality for interactive GraphQL documentation */
83
+ polen: {
84
+ /** Check if this token should be interactive (clickable/hoverable) */
85
+ isInteractive: () => boolean
86
+ /** Get the reference URL for navigation, or null if not applicable */
87
+ getReferenceUrl: () => string | null
88
+ }
89
+
90
+ /** Syntax highlighting functionality */
91
+ highlighter: {
92
+ /** Get the CSS class name for styling this token */
93
+ getCssClass: () => string
94
+ }
95
+
96
+ /** CodeHike integration for enhanced code block features */
97
+ codeHike: {
98
+ /** Array of CodeHike annotations that apply to this token */
99
+ annotations: CodeAnnotation[]
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Implementation of unified token
105
+ */
106
+ class UnifiedToken implements GraphQLToken {
107
+ public polen: { isInteractive: () => boolean; getReferenceUrl: () => string | null }
108
+ public highlighter: { getCssClass: () => string }
109
+ public codeHike: { annotations: CodeAnnotation[] }
110
+
111
+ // Cache these values to avoid WASM access issues
112
+ private _text: string
113
+ private _start: number
114
+ private _end: number
115
+ private _nodeType: TreeSitterGraphQLNodeType
116
+
117
+ constructor(
118
+ public treeSitterNode: WebTreeSitter.Node,
119
+ public semantic: SemanticNode | undefined,
120
+ annotations: CodeAnnotation[],
121
+ ) {
122
+ // Cache the values immediately to avoid WASM access issues later
123
+ // This works for both real WebTreeSitter nodes and synthetic nodes
124
+ this._text = treeSitterNode.text
125
+ this._start = treeSitterNode.startIndex
126
+ this._end = treeSitterNode.endIndex
127
+ this._nodeType = treeSitterNode.type as TreeSitterGraphQLNodeType
128
+
129
+ this.codeHike = { annotations }
130
+
131
+ // Polen namespace
132
+ this.polen = {
133
+ isInteractive: () => this._isInteractive(),
134
+ getReferenceUrl: () => this._getReferenceUrl(),
135
+ }
136
+
137
+ // Highlighter namespace
138
+ this.highlighter = {
139
+ getCssClass: () => this._getCssClass(),
140
+ }
141
+ }
142
+
143
+ get text(): string {
144
+ return this._text
145
+ }
146
+
147
+ get start(): number {
148
+ return this._start
149
+ }
150
+
151
+ get end(): number {
152
+ return this._end
153
+ }
154
+
155
+ private _getCssClass(): string {
156
+ const nodeType = this._nodeType
157
+
158
+ // Development-only validation
159
+ if (process.env['NODE_ENV'] === 'development') {
160
+ // Validate that the node type is actually a valid TreeSitterGraphQLNodeType
161
+ const validTypes = new Set([
162
+ // Add a few common types for validation
163
+ 'document',
164
+ 'name',
165
+ 'field',
166
+ 'argument',
167
+ 'variable',
168
+ 'comment',
169
+ 'error_hint',
170
+ 'whitespace',
171
+ 'string_value',
172
+ 'int_value',
173
+ 'float_value',
174
+ 'query',
175
+ 'mutation',
176
+ 'subscription',
177
+ ])
178
+
179
+ if (!validTypes.has(nodeType as any) && !nodeType.match(/^[a-z_]+$/)) {
180
+ console.warn(`Unknown tree-sitter node type: "${nodeType}". Consider adding to TreeSitterGraphQLNodeType.`)
181
+ }
182
+ }
183
+
184
+ // Error hints
185
+ if (nodeType === 'error_hint') {
186
+ return 'graphql-error-hint'
187
+ }
188
+
189
+ // Comments
190
+ if (nodeType === 'comment' || nodeType === 'description') {
191
+ return 'graphql-comment'
192
+ }
193
+
194
+ // Keywords
195
+ if (isKeywordNodeType(nodeType)) {
196
+ return 'graphql-keyword'
197
+ }
198
+
199
+ // Literals
200
+ if (nodeType === 'string_value') return 'graphql-string'
201
+ if (nodeType === 'int_value' || nodeType === 'float_value') return 'graphql-number'
202
+
203
+ // Punctuation
204
+ if (isPunctuationNodeType(nodeType)) {
205
+ return 'graphql-punctuation'
206
+ }
207
+
208
+ // Names - use semantic info for better classification
209
+ if (nodeType === 'name') {
210
+ // Check if this is an invalid field (has invalidField semantic)
211
+ if (this.semantic && 'kind' in this.semantic && this.semantic.kind === 'InvalidField') {
212
+ return 'graphql-field-error'
213
+ }
214
+
215
+ if (isOutputField(this.semantic) || isInputField(this.semantic)) {
216
+ return 'graphql-field-interactive'
217
+ }
218
+ if (
219
+ this.semantic instanceof GraphQLObjectType
220
+ || this.semantic instanceof GraphQLScalarType
221
+ || this.semantic instanceof GraphQLInterfaceType
222
+ ) {
223
+ return 'graphql-type-interactive'
224
+ }
225
+ if (isVariable(this.semantic)) {
226
+ return 'graphql-variable'
227
+ }
228
+ if (isOperation(this.semantic)) {
229
+ return 'graphql-operation'
230
+ }
231
+ if (isFragment(this.semantic)) {
232
+ return 'graphql-fragment'
233
+ }
234
+ if (isArgument(this.semantic)) {
235
+ return 'graphql-argument'
236
+ }
237
+ }
238
+
239
+ // Variables
240
+ if (nodeType === 'variable') return 'graphql-variable'
241
+
242
+ return 'graphql-text'
243
+ }
244
+
245
+ private _isInteractive(): boolean {
246
+ if (!this.semantic) return false
247
+
248
+ // Fields, type references, arguments, and invalid fields are interactive
249
+ return isOutputField(this.semantic)
250
+ || isInputField(this.semantic)
251
+ || isArgument(this.semantic)
252
+ || isInvalidField(this.semantic) // Invalid fields should show error popovers
253
+ || this.semantic instanceof GraphQLObjectType
254
+ || this.semantic instanceof GraphQLScalarType
255
+ || this.semantic instanceof GraphQLInterfaceType
256
+ || this.semantic instanceof GraphQLUnionType
257
+ || this.semantic instanceof GraphQLEnumType
258
+ || this.semantic instanceof GraphQLInputObjectType
259
+ }
260
+
261
+ private _getReferenceUrl(): string | null {
262
+ if (!this.semantic) return null
263
+
264
+ // Arguments - use #<field>__<argument> pattern
265
+ if (isArgument(this.semantic)) {
266
+ return `/reference/${this.semantic.parentType.name}#${this.semantic.parentField.name}__${this.semantic.argumentDef.name}`
267
+ }
268
+
269
+ // Output fields - use hash links since field routes aren't connected yet
270
+ if (isOutputField(this.semantic)) {
271
+ return `/reference/${this.semantic.parentType.name}#${this.semantic.fieldDef.name}`
272
+ }
273
+
274
+ // Input fields - use hash links since field routes aren't connected yet
275
+ if (isInputField(this.semantic)) {
276
+ return `/reference/${this.semantic.parentType.name}#${this.semantic.fieldDef.name}`
277
+ }
278
+
279
+ // Type references - use :type pattern
280
+ if (this.semantic instanceof GraphQLObjectType) {
281
+ return `/reference/${this.semantic.name}`
282
+ }
283
+
284
+ if (this.semantic instanceof GraphQLScalarType) {
285
+ return `/reference/${this.semantic.name}`
286
+ }
287
+
288
+ if (this.semantic instanceof GraphQLInterfaceType) {
289
+ return `/reference/${this.semantic.name}`
290
+ }
291
+
292
+ if (this.semantic instanceof GraphQLUnionType) {
293
+ return `/reference/${this.semantic.name}`
294
+ }
295
+
296
+ if (this.semantic instanceof GraphQLEnumType) {
297
+ return `/reference/${this.semantic.name}`
298
+ }
299
+
300
+ if (this.semantic instanceof GraphQLInputObjectType) {
301
+ return `/reference/${this.semantic.name}`
302
+ }
303
+
304
+ return null
305
+ }
306
+ }
307
+
308
+ // Cache for the parser instance
309
+ let parserPromise: Promise<WebTreeSitter.Parser> | null = null
310
+
311
+ /**
312
+ * Minimal synthetic node that implements just enough of the WebTreeSitter.Node interface
313
+ * Uses TreeSitterGraphQLNodeType for type safety
314
+ *
315
+ * IMPORTANT: This must be a class with getters to match WebTreeSitter.Node's WASM interface.
316
+ * Plain objects with properties will cause "memory access out of bounds" errors when
317
+ * tree-sitter tries to call the WASM getter functions.
318
+ */
319
+ class SyntheticNode {
320
+ constructor(
321
+ public type: TreeSitterGraphQLNodeType,
322
+ private _text: string,
323
+ private _startIndex: number,
324
+ private _endIndex: number,
325
+ ) {}
326
+
327
+ // These getters match WebTreeSitter.Node's interface
328
+ get text(): string {
329
+ return this._text
330
+ }
331
+
332
+ get startIndex(): number {
333
+ return this._startIndex
334
+ }
335
+
336
+ get endIndex(): number {
337
+ return this._endIndex
338
+ }
339
+
340
+ get childCount(): number {
341
+ return 0
342
+ }
343
+
344
+ get parent(): null {
345
+ return null
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Tracks semantic context while walking the tree-sitter AST
351
+ *
352
+ * This class maintains the current GraphQL execution context as we traverse
353
+ * the syntax tree, allowing us to resolve field references to their schema
354
+ * definitions and validate field access.
355
+ *
356
+ * ## Context Management Strategy
357
+ *
358
+ * The semantic context uses a stack-based approach to track the current type context
359
+ * as we traverse nested GraphQL selections. This is essential for resolving field
360
+ * references since field names are only meaningful within their parent type context.
361
+ *
362
+ * ### Type Stack Management:
363
+ * - Each stack entry contains: { type: GraphQLType, field?: GraphQLField }
364
+ * - The `field` property tracks the field that led us to this type level
365
+ * - Stack depth corresponds to GraphQL selection nesting depth
366
+ * - Root level: operation root type (Query/Mutation/Subscription)
367
+ * - Nested levels: field return types that support sub-selections
368
+ *
369
+ * ### Context Transitions:
370
+ * - `enterOperation()`: Sets root type based on operation type
371
+ * - `enterField()`: Pushes field's return type if it's selectable (Object/Interface)
372
+ * - `exitField()`: Pops from stack when leaving a field's selection set
373
+ * - `enterFragment()`: Switches context to fragment's target type
374
+ *
375
+ * ### Argument Resolution Challenge:
376
+ * Arguments appear in the AST before their parent field context is established,
377
+ * requiring the complex lookup logic documented in the argument parsing section.
378
+ *
379
+ * @example
380
+ * ```typescript
381
+ * const context = new SemanticContext(schema)
382
+ * context.enterOperation('query') // Stack: [Query]
383
+ * context.enterField('user') // Stack: [Query, User]
384
+ * const fieldInfo = context.getFieldInfo('name') // Gets User.name field
385
+ * context.exitField() // Stack: [Query]
386
+ * ```
387
+ */
388
+ class SemanticContext {
389
+ /**
390
+ * Stack of type contexts representing the current selection path.
391
+ * Each entry tracks the type we're currently selecting from and optionally
392
+ * the field that brought us to this type level.
393
+ */
394
+ private typeStack: Array<{ type: GraphQLObjectType | GraphQLInterfaceType; field?: GraphQLField<any, any> }> = []
395
+
396
+ /** Current operation type, set when entering an operation definition */
397
+ operationType: 'query' | 'mutation' | 'subscription' | null = null
398
+
399
+ /** GraphQL schema used for type lookups and validation */
400
+ schema: GraphQLSchema
401
+
402
+ constructor(schema: GraphQLSchema) {
403
+ this.schema = schema
404
+ }
405
+
406
+ enterOperation(type: string) {
407
+ this.operationType = type as 'query' | 'mutation' | 'subscription'
408
+ const rootType = type === 'query'
409
+ ? this.schema.getQueryType()
410
+ : type === 'mutation'
411
+ ? this.schema.getMutationType()
412
+ : type === 'subscription'
413
+ ? this.schema.getSubscriptionType()
414
+ : null
415
+
416
+ if (rootType) {
417
+ this.typeStack = [{ type: rootType }]
418
+ }
419
+ }
420
+
421
+ enterFragment(typeName: string) {
422
+ const type = this.schema.getType(typeName)
423
+
424
+ if (type && (isObjectType(type) || isInterfaceType(type))) {
425
+ this.typeStack = [{ type }]
426
+ }
427
+ }
428
+
429
+ getFieldInfo(
430
+ fieldName: string,
431
+ ): { parentType: GraphQLObjectType | GraphQLInterfaceType; fieldDef: GraphQLField<any, any> } | null {
432
+ const current = this.typeStack[this.typeStack.length - 1]
433
+ if (!current) return null
434
+
435
+ const fields = current.type.getFields()
436
+ const fieldDef = fields[fieldName]
437
+
438
+ if (fieldDef) {
439
+ return { parentType: current.type, fieldDef }
440
+ }
441
+ return null
442
+ }
443
+
444
+ enterField(fieldName: string) {
445
+ const fieldInfo = this.getFieldInfo(fieldName)
446
+ if (fieldInfo) {
447
+ // Only push to stack if field type is object/interface
448
+ const fieldType = getNamedType(fieldInfo.fieldDef.type)
449
+ if (isObjectType(fieldType) || isInterfaceType(fieldType)) {
450
+ // Push new context with the field that brought us here
451
+ this.typeStack.push({ type: fieldType, field: fieldInfo.fieldDef })
452
+ }
453
+ }
454
+ }
455
+
456
+ exitField() {
457
+ // Only pop if we're not at root and the last entry has a field
458
+ // (meaning it was pushed by enterField for an object/interface type)
459
+ if (this.typeStack.length > 1) {
460
+ const last = this.typeStack[this.typeStack.length - 1]
461
+ if (last && last.field) {
462
+ this.typeStack.pop()
463
+ }
464
+ }
465
+ }
466
+
467
+ getArgumentInfo(argName: string) {
468
+ const current = this.typeStack[this.typeStack.length - 1]
469
+ if (!current?.field) return null
470
+
471
+ const arg = current.field.args.find(a => a.name === argName)
472
+ return arg ? { field: current.field, arg, parentType: current.type } : null
473
+ }
474
+
475
+ getCurrentType(): (GraphQLObjectType | GraphQLInterfaceType) | null {
476
+ const current = this.typeStack[this.typeStack.length - 1]
477
+ return current?.type || null
478
+ }
479
+
480
+ lookupType(typeName: string) {
481
+ return this.schema.getType(typeName)
482
+ }
483
+
484
+ reset() {
485
+ this.typeStack = []
486
+ this.operationType = null
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Parse GraphQL code into interactive tokens with semantic information
492
+ *
493
+ * @param code - The raw GraphQL code to parse
494
+ * @param annotations - CodeHike annotations that might affect rendering
495
+ * @param schema - Optional GraphQL schema for semantic analysis
496
+ * @returns Array of tokens representing the parsed code
497
+ */
498
+ export async function parseGraphQLWithTreeSitter(
499
+ code: string,
500
+ annotations: CodeAnnotation[] = [],
501
+ schema?: GraphQLSchema,
502
+ ): Promise<GraphQLToken[]> {
503
+ // Validate input
504
+ if (!code || typeof code !== 'string') {
505
+ throw new Error('Invalid GraphQL code: code must be a non-empty string')
506
+ }
507
+
508
+ // Prevent parsing extremely large documents that could cause performance issues
509
+ if (code.length > 100_000) {
510
+ throw new Error('GraphQL document too large: maximum 100,000 characters allowed')
511
+ }
512
+
513
+ // Step 1: Parse with tree-sitter
514
+ const parser = await getParser()
515
+ const tree = parser.parse(code)
516
+
517
+ if (!tree) {
518
+ throw new Error('Tree-sitter failed to parse GraphQL code')
519
+ }
520
+
521
+ // Check if tree-sitter found syntax errors (disabled for now as it may be too strict)
522
+ // if (tree.rootNode.hasError) {
523
+ // throw new Error('GraphQL syntax error detected by tree-sitter parser')
524
+ // }
525
+
526
+ try {
527
+ // Step 2: Walk tree and attach semantics
528
+ const tokens = collectTokensWithSemantics(tree, code, schema, annotations)
529
+
530
+ // Step 3: Add error hint tokens after invalid fields
531
+ const tokensWithHints = addErrorHintTokens(tokens, code, annotations)
532
+
533
+ return tokensWithHints
534
+ } finally {
535
+ // ## Tree-sitter Resource Lifecycle Management
536
+ //
537
+ // Tree-sitter creates native WASM objects that must be explicitly freed to prevent memory leaks.
538
+ // The tree object holds references to parsed nodes and internal parser state that won't be
539
+ // garbage collected automatically by JavaScript.
540
+ //
541
+ // Critical cleanup points:
542
+ // 1. Always call tree.delete() in a finally block to ensure cleanup even on errors
543
+ // 2. Do not access tree or any of its nodes after calling delete()
544
+ // 3. The parser instance is cached globally and reused across multiple parsing calls
545
+ //
546
+ // Memory safety: Once tree.delete() is called, all WebTreeSitter.Node references become invalid.
547
+ // Our tokens hold references to these nodes, but only use their text and position properties
548
+ // which are copied during token creation, so the nodes can be safely deleted.
549
+ tree.delete()
550
+ }
551
+ }
552
+
553
+ /**
554
+ * Get or create the tree-sitter parser instance
555
+ */
556
+ async function getParser(): Promise<WebTreeSitter.Parser> {
557
+ if (!parserPromise) {
558
+ parserPromise = initializeTreeSitter()
559
+ }
560
+ return parserPromise
561
+ }
562
+
563
+ /**
564
+ * Initialize tree-sitter with the GraphQL grammar
565
+ */
566
+ async function initializeTreeSitter(): Promise<WebTreeSitter.Parser> {
567
+ try {
568
+ // Handle different environments
569
+ const isNode = typeof process !== 'undefined' && process.versions && process.versions.node
570
+
571
+ if (isNode) {
572
+ // Node.js environment (tests)
573
+ const fs = await import('node:fs/promises')
574
+ const path = await import('node:path')
575
+
576
+ // Find the actual WASM files in node_modules
577
+ const treeSitterWasmPath = path.join(process.cwd(), 'node_modules/web-tree-sitter/tree-sitter.wasm')
578
+ const graphqlWasmPath = path.join(process.cwd(), 'node_modules/tree-sitter-graphql-grammar-wasm/grammar.wasm')
579
+
580
+ await WebTreeSitter.Parser.init({
581
+ locateFile: (filename: string) => {
582
+ if (filename === 'tree-sitter.wasm') {
583
+ return treeSitterWasmPath
584
+ }
585
+ return filename
586
+ },
587
+ })
588
+
589
+ const parser = new WebTreeSitter.Parser()
590
+ const wasmBuffer = await fs.readFile(graphqlWasmPath)
591
+ const GraphQL = await WebTreeSitter.Language.load(new Uint8Array(wasmBuffer))
592
+ parser.setLanguage(GraphQL)
593
+
594
+ return parser
595
+ } else {
596
+ // Browser/Vite environment
597
+ await WebTreeSitter.Parser.init({
598
+ locateFile: (filename: string) => {
599
+ if (filename === 'tree-sitter.wasm') {
600
+ return treeSitterWasmUrl
601
+ }
602
+ return filename
603
+ },
604
+ })
605
+
606
+ const parser = new WebTreeSitter.Parser()
607
+
608
+ // Fetch the WASM file as a buffer
609
+ const response = await fetch(graphqlWasmUrl)
610
+ if (!response.ok) {
611
+ throw new Error(
612
+ `Failed to load GraphQL grammar file: ${response.status} ${response.statusText}. `
613
+ + `This may indicate a network issue or missing grammar file.`,
614
+ )
615
+ }
616
+
617
+ const wasmBuffer = await response.arrayBuffer()
618
+
619
+ if (wasmBuffer.byteLength === 0) {
620
+ throw new Error('GraphQL grammar file is empty or corrupted')
621
+ }
622
+
623
+ const GraphQL = await WebTreeSitter.Language.load(new Uint8Array(wasmBuffer))
624
+ parser.setLanguage(GraphQL)
625
+
626
+ return parser
627
+ }
628
+ } catch (error) {
629
+ // Enhance error messages for common issues
630
+ if (error instanceof Error) {
631
+ if (error.message.includes('fetch')) {
632
+ throw new Error(`Tree-sitter initialization failed: ${error.message}. Check your network connection.`)
633
+ }
634
+ if (error.message.includes('Language.load')) {
635
+ throw new Error(`Failed to load GraphQL grammar: ${error.message}. The grammar file may be corrupted.`)
636
+ }
637
+ }
638
+ throw error
639
+ }
640
+ }
641
+
642
+ /**
643
+ * Add error hint tokens after invalid fields
644
+ */
645
+ function addErrorHintTokens(
646
+ tokens: GraphQLToken[],
647
+ code: string,
648
+ annotations: CodeAnnotation[],
649
+ ): GraphQLToken[] {
650
+ const tokensWithHints: GraphQLToken[] = []
651
+ const processedIndices = new Set<number>()
652
+
653
+ // Count invalid fields for debugging
654
+ let invalidFieldCount = 0
655
+ tokens.forEach(t => {
656
+ if (t.semantic && 'kind' in t.semantic && t.semantic.kind === 'InvalidField') {
657
+ invalidFieldCount++
658
+ }
659
+ })
660
+
661
+ if (invalidFieldCount > 10) {
662
+ // Too many invalid fields - likely a schema mismatch
663
+ // Return tokens without error hints to avoid corrupting display
664
+ console.warn(`Polen: ${invalidFieldCount} invalid fields detected. Schema may not match queries.`)
665
+ return tokens
666
+ }
667
+
668
+ for (let i = 0; i < tokens.length; i++) {
669
+ const token = tokens[i]!
670
+
671
+ // Skip if we've already processed this token (due to lookahead for arguments)
672
+ if (processedIndices.has(i)) {
673
+ continue
674
+ }
675
+
676
+ tokensWithHints.push(token)
677
+ processedIndices.add(i)
678
+
679
+ // Check if this is an invalid field
680
+ if (token.semantic && 'kind' in token.semantic && token.semantic.kind === 'InvalidField') {
681
+ // Look ahead to find where the field ends (after arguments if present)
682
+ let fieldEndIndex = i
683
+ let j = i + 1
684
+
685
+ // Skip whitespace
686
+ while (j < tokens.length && tokens[j]!.treeSitterNode.type === 'whitespace') {
687
+ j++
688
+ }
689
+
690
+ // Check if we have arguments starting with '('
691
+ if (j < tokens.length && tokens[j]!.text === '(') {
692
+ // Find the matching closing ')'
693
+ let parenDepth = 1
694
+ j++ // move past the opening '('
695
+
696
+ while (j < tokens.length && parenDepth > 0) {
697
+ const t = tokens[j]!
698
+ if (t.text === '(') parenDepth++
699
+ else if (t.text === ')') parenDepth--
700
+ j++
701
+ }
702
+
703
+ // j is now past the closing ')'
704
+ fieldEndIndex = j - 1
705
+
706
+ // Add all tokens that are part of the field's arguments
707
+ for (let k = i + 1; k <= fieldEndIndex; k++) {
708
+ if (k < tokens.length && !processedIndices.has(k)) {
709
+ tokensWithHints.push(tokens[k]!)
710
+ processedIndices.add(k)
711
+ }
712
+ }
713
+ }
714
+
715
+ // Now add the error hint after the complete field (including arguments)
716
+ const lastFieldToken = tokens[fieldEndIndex] || token
717
+ const hintText = ' ← No such field'
718
+ const hintToken = new UnifiedToken(
719
+ createSyntheticNode(
720
+ 'error_hint',
721
+ hintText,
722
+ lastFieldToken.end,
723
+ lastFieldToken.end + hintText.length,
724
+ ),
725
+ undefined,
726
+ annotations,
727
+ )
728
+ tokensWithHints.push(hintToken)
729
+ }
730
+ }
731
+
732
+ return tokensWithHints
733
+ }
734
+
735
+ /**
736
+ * Walk tree-sitter AST and collect tokens with semantic information
737
+ */
738
+ function collectTokensWithSemantics(
739
+ tree: WebTreeSitter.Tree,
740
+ code: string,
741
+ schema: GraphQLSchema | undefined,
742
+ annotations: CodeAnnotation[],
743
+ ): GraphQLToken[] {
744
+ const tokens: GraphQLToken[] = []
745
+ const cursor = tree.walk()
746
+ const context = schema ? new SemanticContext(schema) : null
747
+ let lastEnd = 0
748
+
749
+ function processNode() {
750
+ const node = cursor.currentNode
751
+ if (!node) return
752
+
753
+ // Handle different node types for semantic context
754
+ if (context) {
755
+ if (node.type === 'operation_definition') {
756
+ // Find the operation type child
757
+ const operationType = findChildByType(cursor, 'operation_type')
758
+ if (operationType) {
759
+ context.enterOperation(operationType.text)
760
+ }
761
+ }
762
+
763
+ if (node.type === 'fragment_definition') {
764
+ // Find the type condition
765
+ const typeCondition = findChildByType(cursor, 'type_condition')
766
+ if (typeCondition) {
767
+ const typeName = findChildByType(cursor, 'named_type', typeCondition)
768
+ if (typeName) {
769
+ const nameNode = findChildByType(cursor, 'name', typeName)
770
+ if (nameNode) {
771
+ context.enterFragment(nameNode.text)
772
+ }
773
+ }
774
+ }
775
+ }
776
+
777
+ // We don't need special handling for selection_set anymore
778
+ // Context is managed at the field level
779
+ }
780
+
781
+ // Collect leaf tokens with semantic info
782
+ // Special case: string_value, int_value, float_value nodes should be collected as whole tokens
783
+ // even though they have children (the quotes or signs)
784
+ const isValueNode = node.type === 'string_value' || node.type === 'int_value' || node.type === 'float_value'
785
+ const shouldCollectToken = (node.childCount === 0 || isValueNode) && node.text.trim() !== ''
786
+
787
+ if (shouldCollectToken) {
788
+ // Add whitespace before this token if needed
789
+ if (node.startIndex > lastEnd) {
790
+ const whitespace = code.slice(lastEnd, node.startIndex)
791
+ tokens.push(
792
+ new UnifiedToken(
793
+ createWhitespaceNode(whitespace, lastEnd, node.startIndex),
794
+ undefined,
795
+ annotations,
796
+ ),
797
+ )
798
+ }
799
+
800
+ // Determine semantic info for this token
801
+ let semantic: SemanticNode | undefined
802
+
803
+ if (context && node.type === 'name') {
804
+ const parent = cursor.currentNode.parent
805
+
806
+ if (parent?.type === 'field') {
807
+ // This is a field name - get info from current context
808
+ const fieldInfo = context.getFieldInfo(node.text)
809
+ const currentType = context.getCurrentType()
810
+
811
+ if (fieldInfo) {
812
+ semantic = {
813
+ kind: 'OutputField',
814
+ parentType: fieldInfo.parentType,
815
+ fieldDef: fieldInfo.fieldDef,
816
+ }
817
+ // Enter this field's context for processing its selection set
818
+ context.enterField(node.text)
819
+ } else if (currentType) {
820
+ // Field doesn't exist - mark as invalid
821
+ semantic = {
822
+ kind: 'InvalidField',
823
+ fieldName: node.text,
824
+ parentType: currentType,
825
+ }
826
+ }
827
+ } else if (parent?.type === 'named_type') {
828
+ // This is a type reference
829
+ const type = context.lookupType(node.text)
830
+ if (type) {
831
+ // Check if it's one of the types we support as semantic nodes
832
+ if (
833
+ type instanceof GraphQLObjectType
834
+ || type instanceof GraphQLScalarType
835
+ || type instanceof GraphQLInterfaceType
836
+ || type instanceof GraphQLUnionType
837
+ || type instanceof GraphQLEnumType
838
+ || type instanceof GraphQLInputObjectType
839
+ ) {
840
+ semantic = type
841
+ }
842
+ }
843
+ } else if (parent?.type === 'operation_definition') {
844
+ // This is an operation name
845
+ semantic = {
846
+ kind: 'Operation',
847
+ type: context.operationType || 'query',
848
+ name: node.text,
849
+ }
850
+ } else if (parent?.type === 'fragment_definition') {
851
+ // This is a fragment name - for now just mark it as a fragment
852
+ semantic = {
853
+ kind: 'Fragment',
854
+ name: node.text,
855
+ onType: context.getCurrentType()!, // We'll have the type from enterFragment
856
+ }
857
+ } else if (parent?.type === 'argument') {
858
+ // This is an argument name
859
+ //
860
+ // ## Complex Argument Parsing Logic
861
+ //
862
+ // Arguments require complex tree traversal because they appear in the tree-sitter AST
863
+ // before the semantic context has been updated for their parent field. This creates
864
+ // a chicken-and-egg problem where we need the field to identify the argument, but
865
+ // the field hasn't been processed yet.
866
+ //
867
+ // Tree structure: field > arguments > argument > name
868
+ // Processing order: argument names are parsed before field context is established
869
+ //
870
+ // Our solution is to traverse up the AST to find the field node, then look for that
871
+ // field in both the root operation type and the current type context. We check the
872
+ // root type first because top-level fields (like Query.pokemon) are most common.
873
+
874
+ let argumentsNode = parent.parent
875
+ if (argumentsNode && argumentsNode.type === 'arguments') {
876
+ let fieldNode = argumentsNode.parent
877
+ if (fieldNode && fieldNode.type === 'field') {
878
+ // Find the field name node within the field node
879
+ for (let i = 0; i < fieldNode.childCount; i++) {
880
+ const child = fieldNode.child(i)
881
+ if (child && child.type === 'name') {
882
+ // We need to find the parent type that contains this field
883
+ // Start with the root type based on the operation type (query/mutation/subscription)
884
+ const rootType = context.schema.getQueryType() || context.schema.getMutationType()
885
+ || context.schema.getSubscriptionType()
886
+
887
+ if (rootType) {
888
+ // First check if the field exists on the root type (most common case)
889
+ let field = rootType.getFields()[child.text]
890
+ let parentType: GraphQLObjectType | GraphQLInterfaceType = rootType
891
+
892
+ // If not found on root, check the current type in our semantic context
893
+ // This handles nested field arguments like User.posts(limit: 10)
894
+ if (!field) {
895
+ const currentType = context.getCurrentType()
896
+ if (currentType) {
897
+ field = currentType.getFields()[child.text]
898
+ parentType = currentType
899
+ }
900
+ }
901
+
902
+ if (field && parentType) {
903
+ const arg = field.args.find((a: GraphQLArgument) => a.name === node.text)
904
+ if (arg) {
905
+ semantic = {
906
+ kind: 'Argument',
907
+ parentType: parentType,
908
+ parentField: field,
909
+ argumentDef: arg,
910
+ }
911
+ }
912
+ }
913
+ }
914
+ break
915
+ }
916
+ }
917
+ }
918
+ }
919
+ } else if (parent?.type === 'variable') {
920
+ // This is a variable name (without the $)
921
+ semantic = {
922
+ kind: 'Variable',
923
+ name: node.text,
924
+ }
925
+ } else if (parent?.type === 'variable_definition') {
926
+ // This is a variable definition in the operation header
927
+ semantic = {
928
+ kind: 'Variable',
929
+ name: node.text,
930
+ }
931
+ }
932
+ } else if (context && node.type === 'variable' && node.text.startsWith('$')) {
933
+ // This is the full variable including $ (usage in arguments or directives)
934
+ semantic = {
935
+ kind: 'Variable',
936
+ name: node.text.slice(1),
937
+ }
938
+ }
939
+
940
+ const token = new UnifiedToken(
941
+ node,
942
+ semantic,
943
+ annotations,
944
+ )
945
+
946
+ tokens.push(token)
947
+
948
+ lastEnd = node.endIndex
949
+ }
950
+
951
+ // Traverse children (but skip children of value nodes since we collect them as whole tokens)
952
+ if (!isValueNode && cursor.gotoFirstChild()) {
953
+ do {
954
+ processNode()
955
+ } while (cursor.gotoNextSibling())
956
+ cursor.gotoParent()
957
+
958
+ // Handle context exit
959
+ if (context && node.type === 'field') {
960
+ // Only exit field context if this field has a selection set
961
+ // (meaning it's an object/interface type that pushed to the stack)
962
+ const hasSelectionSet = node.childCount > 0 && node.children.some(
963
+ child => child?.type === 'selection_set',
964
+ )
965
+ if (hasSelectionSet) {
966
+ context.exitField()
967
+ }
968
+ } else if (context && (node.type === 'operation_definition' || node.type === 'fragment_definition')) {
969
+ // Reset context when exiting operation or fragment
970
+ context.reset()
971
+ }
972
+ }
973
+ }
974
+
975
+ processNode()
976
+
977
+ // Add final whitespace if needed
978
+ if (lastEnd < code.length) {
979
+ const remaining = code.slice(lastEnd)
980
+ tokens.push(
981
+ new UnifiedToken(
982
+ createWhitespaceNode(remaining, lastEnd, code.length),
983
+ undefined,
984
+ annotations,
985
+ ),
986
+ )
987
+ }
988
+
989
+ return tokens
990
+ }
991
+
992
+ /**
993
+ * Helper to find a child node by type
994
+ */
995
+ function findChildByType(
996
+ cursor: WebTreeSitter.TreeCursor,
997
+ type: string,
998
+ node?: WebTreeSitter.Node,
999
+ ): WebTreeSitter.Node | null {
1000
+ const targetNode = node || cursor.currentNode
1001
+ if (!targetNode) return null
1002
+
1003
+ for (let i = 0; i < targetNode.childCount; i++) {
1004
+ const child = targetNode.child(i)
1005
+ if (child && child.type === type) {
1006
+ return child
1007
+ }
1008
+ }
1009
+ return null
1010
+ }
1011
+
1012
+ /**
1013
+ * Create a pseudo tree-sitter node for whitespace
1014
+ */
1015
+ function createWhitespaceNode(text: string, start: number, end: number): WebTreeSitter.Node {
1016
+ // Create a synthetic node with proper getter interface
1017
+ const node = new SyntheticNode('whitespace', text, start, end)
1018
+ return node as unknown as WebTreeSitter.Node
1019
+ }
1020
+
1021
+ /**
1022
+ * Create a pseudo tree-sitter node for synthetic content
1023
+ *
1024
+ * ## Annotation Architecture
1025
+ *
1026
+ * Polen uses synthetic tree-sitter nodes to inject additional content into GraphQL code blocks.
1027
+ * This approach was chosen after considering several alternatives:
1028
+ *
1029
+ * ### Current Approach: Synthetic Nodes
1030
+ * We create fake tree-sitter nodes that implement just enough of the Node interface to flow
1031
+ * through our token rendering pipeline. This is used for error hints that appear after invalid fields.
1032
+ *
1033
+ * **When to use**: When you need to add new content to the code block (not just style existing content)
1034
+ *
1035
+ * ### Alternative Approaches Considered:
1036
+ *
1037
+ * 1. **CodeHike Annotations**
1038
+ * - Use CodeHike's built-in InlineAnnotation/BlockAnnotation system
1039
+ * - Pros: Works with CodeHike's architecture, composable with other handlers
1040
+ * - Cons: More complex, requires understanding CodeHike's annotation pipeline
1041
+ * - Best for: Complex features like collapsible sections, tabs
1042
+ *
1043
+ * 2. **Post-Processing During Render**
1044
+ * - Keep tokens unchanged, add content during the rendering phase
1045
+ * - Pros: Simpler, no fake nodes needed
1046
+ * - Cons: Rendering logic becomes more complex, harder to test
1047
+ * - Best for: Simple conditional content
1048
+ *
1049
+ * 3. **Token Metadata/Props**
1050
+ * - Add annotation data to token properties rather than creating new tokens
1051
+ * - Pros: Clean data model, easy to test
1052
+ * - Cons: Can't add new content, only modify existing tokens
1053
+ * - Best for: Styling annotations (highlights, emphasis, underlines)
1054
+ *
1055
+ * ### Guidelines for Future Annotations:
1056
+ *
1057
+ * - **Styling only** (highlights, emphasis): Use token metadata/props
1058
+ * - **Adding content** (error hints, tooltips): Use synthetic nodes (current approach)
1059
+ * - **Complex UI** (collapsible, tabs): Consider CodeHike annotation handlers
1060
+ * - **User-defined annotations**: Choose based on what the annotation does
1061
+ *
1062
+ * The synthetic node approach works well for Polen's error hints because we're actually
1063
+ * inserting new text content ("← No such field") that needs to be positioned and styled
1064
+ * like a regular token.
1065
+ */
1066
+ function createSyntheticNode(
1067
+ type: TreeSitterGraphQLNodeType,
1068
+ text: string,
1069
+ start: number,
1070
+ end: number,
1071
+ ): WebTreeSitter.Node {
1072
+ // Create a synthetic node with proper getter interface
1073
+ const node = new SyntheticNode(type, text, start, end)
1074
+ return node as unknown as WebTreeSitter.Node
1075
+ }