polen 0.10.0-next.20 → 0.10.0-next.22

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