@ts-for-gir/language-server 0.1.0

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.
package/README.md ADDED
@@ -0,0 +1,257 @@
1
+ # @ts-for-gir/language-server
2
+
3
+ A TypeScript validation and hover utility for testing generated GIR type definitions.
4
+
5
+ ## Features
6
+
7
+ - TypeScript code validation optimized for GJS/GIR types
8
+ - Essential compiler error detection
9
+ - Support for GIR type definitions and module resolution
10
+ - **Hover functionality** for type inspection at specific positions
11
+ - Automatic type discovery for GIR definitions
12
+ - Clean API for validation and type checking
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ yarn add @ts-for-gir/language-server
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ### Basic TypeScript Validation
23
+
24
+ ```typescript
25
+ import { validateTypeScript } from '@ts-for-gir/language-server';
26
+
27
+ const result = validateTypeScript(`
28
+ interface Test {
29
+ name: string;
30
+ }
31
+ const obj: Test = { name: "test" };
32
+ `);
33
+
34
+ console.log(result.success); // true or false
35
+ console.log(result.errors); // Array of error messages
36
+ ```
37
+
38
+ ### GIR Type Validation
39
+
40
+ ```typescript
41
+ import { validateGIRTypeScript } from '@ts-for-gir/language-server';
42
+ import path from 'path';
43
+
44
+ const typesPath = path.resolve('./types/@types');
45
+ const result = validateGIRTypeScript(`
46
+ import Gtk from 'gi://Gtk?version=4.0';
47
+
48
+ const button = new Gtk.Button();
49
+ button.set_label("Hello World");
50
+ `, typesPath);
51
+
52
+ console.log(result.success); // true or false
53
+ console.log(result.errors); // Array of error messages
54
+ ```
55
+
56
+ ### Automatic GIR Type Discovery
57
+
58
+ ```typescript
59
+ import { validateGIRTypeScriptAuto } from '@ts-for-gir/language-server';
60
+
61
+ const result = validateGIRTypeScriptAuto(`
62
+ import Gtk from 'gi://Gtk?version=4.0';
63
+
64
+ const button = new Gtk.Button();
65
+ button.set_label("Hello World");
66
+ `);
67
+
68
+ console.log(result.success); // true or false
69
+ ```
70
+
71
+ ### Hover Functionality - Type Inspection
72
+
73
+ ```typescript
74
+ import { getIdentifierTypeAuto } from '@ts-for-gir/language-server';
75
+
76
+ const code = `
77
+ import Gtk from 'gi://Gtk?version=4.0';
78
+
79
+ const button = new Gtk.Button();
80
+ const window = new Gtk.Window();
81
+ `;
82
+
83
+ // Get type information for the 'button' variable
84
+ const result = getIdentifierTypeAuto(code, 'button');
85
+
86
+ console.log(result.success); // true or false
87
+ console.log(result.type); // e.g., "Gtk.Button"
88
+ console.log(result.documentation); // JSDoc documentation if available
89
+ ```
90
+
91
+ ### Type Expectation - Validate Expected Types
92
+
93
+ ```typescript
94
+ import { expectIdentifierTypeAuto } from '@ts-for-gir/language-server';
95
+
96
+ const code = `
97
+ import Gtk from 'gi://Gtk?version=4.0';
98
+
99
+ const button = new Gtk.Button();
100
+ const window = new Gtk.Window();
101
+ `;
102
+
103
+ // Validate that 'button' is of type 'Gtk.Button' (exact match)
104
+ const result = expectIdentifierTypeAuto(code, 'button', 'Gtk.Button');
105
+
106
+ console.log(result.success); // true or false
107
+ console.log(result.matches); // true if types match
108
+ console.log(result.actualType); // e.g., "Gtk.Button"
109
+ console.log(result.expectedType); // "Gtk.Button"
110
+ console.log(result.error); // Error message if types don't match
111
+
112
+ // Validate using RegExp pattern (flexible matching)
113
+ const patternResult = expectIdentifierTypeAuto(code, 'button', /Button|Gtk\.Button/);
114
+ console.log(patternResult.matches); // true if type matches pattern
115
+ ```
116
+
117
+ ## API
118
+
119
+ ### Validation Functions
120
+
121
+ #### `validateTypeScript(code: string, options?: ValidationOptions)`
122
+
123
+ Validates TypeScript code and returns validation results.
124
+
125
+ **Returns:** `ValidationResult`
126
+ - `success: boolean` - Whether validation passed
127
+ - `errors: string[]` - Array of error messages
128
+ - `warnings: string[]` - Array of warning messages
129
+
130
+ **Options:**
131
+ - `strict?: boolean` - Enable strict mode (default: true)
132
+ - `typesPath?: string` - Path to directory containing GIR type definitions
133
+
134
+ #### `validateGIRTypeScript(code: string, typesPath: string, options?: ValidationOptions)`
135
+
136
+ Validates GIR TypeScript code with type definitions.
137
+
138
+ #### `validateGIRTypeScriptAuto(code: string, options?: ValidationOptions)`
139
+
140
+ Validates GIR TypeScript code with automatic type discovery.
141
+
142
+ ### Hover Functions
143
+
144
+ #### `getIdentifierType(code: string, identifier: string, options?: HoverOptions)`
145
+
146
+ Gets type information for a specific identifier in TypeScript code.
147
+
148
+ **Returns:** `HoverResult`
149
+ - `success: boolean` - Whether type checking succeeded
150
+ - `type?: string` - The inferred type (e.g., "Gtk.Button", "string", "number")
151
+ - `documentation?: string` - JSDoc documentation if available
152
+ - `error?: string` - Error message if failed
153
+
154
+ #### `getIdentifierTypeAuto(code: string, identifier: string, options?: HoverOptions)`
155
+
156
+ Gets type information with automatic GIR type discovery.
157
+
158
+ ### Type Expectation Functions
159
+
160
+ #### `expectIdentifierType(code: string, identifier: string, expectedType: string | RegExp, options?: HoverOptions)`
161
+
162
+ Validates that a specific identifier has the expected type.
163
+
164
+ **Returns:** `TypeExpectationResult`
165
+ - `success: boolean` - Whether type checking succeeded
166
+ - `matches: boolean` - Whether the actual type matches the expected type
167
+ - `actualType?: string` - The actual inferred type
168
+ - `expectedType: string | RegExp` - The expected type or pattern that was tested for
169
+ - `documentation?: string` - JSDoc documentation if available
170
+ - `error?: string` - Error message if validation failed
171
+
172
+ **Expected Type Patterns:**
173
+ - `string` - Exact type name match (e.g., "Gtk.Button")
174
+ - `RegExp` - Pattern matching (e.g., `/Variant.*\[\]|Array.*Variant/`)
175
+
176
+ #### `expectIdentifierTypeAuto(code: string, identifier: string, expectedType: string | RegExp, options?: HoverOptions)`
177
+
178
+ Validates that a specific identifier has the expected type with automatic GIR type discovery.
179
+
180
+ ## Use Cases
181
+
182
+ ### Testing GIR Type Definitions
183
+
184
+ ```typescript
185
+ import { expectIdentifierTypeAuto } from '@ts-for-gir/language-server';
186
+
187
+ // Test if Gtk.Button type is correctly inferred
188
+ const result = expectIdentifierTypeAuto(`
189
+ import Gtk from 'gi://Gtk?version=4.0';
190
+ const button = new Gtk.Button();
191
+ `, 'button', 'Gtk.Button');
192
+
193
+ // Should return: { success: true, matches: true, actualType: "Gtk.Button" }
194
+ console.log(result.matches); // true if correctly typed
195
+ ```
196
+
197
+ ### Verifying Complex Type Inference
198
+
199
+ ```typescript
200
+ // Test complex type inference
201
+ const result = expectIdentifierTypeAuto(`
202
+ import Gtk from 'gi://Gtk?version=4.0';
203
+
204
+ const box = new Gtk.Box(Gtk.Orientation.VERTICAL, 10);
205
+ const label = new Gtk.Label("Hello");
206
+
207
+ box.append(label);
208
+ `, 'box', 'Gtk.Box');
209
+
210
+ // Verify the type is correctly inferred
211
+ console.log(result.matches); // Should be true
212
+ console.log(result.actualType); // Should be "Gtk.Box"
213
+ ```
214
+
215
+ ### Unit Testing Type Definitions
216
+
217
+ ```typescript
218
+ import { expectIdentifierTypeAuto } from '@ts-for-gir/language-server';
219
+
220
+ describe('GIR Type Tests', () => {
221
+ test('Gtk.Button should be correctly typed', () => {
222
+ const result = expectIdentifierTypeAuto(`
223
+ import Gtk from 'gi://Gtk?version=4.0';
224
+ const button = new Gtk.Button();
225
+ `, 'button', 'Gtk.Button');
226
+
227
+ expect(result.success).toBe(true);
228
+ expect(result.matches).toBe(true);
229
+ });
230
+
231
+ test('GVariant unpack methods should return correct types', () => {
232
+ const result = expectIdentifierTypeAuto(`
233
+ import GLib from 'gi://GLib?version=2.0';
234
+ const variant = new GLib.Variant("as", ["one", "two"]);
235
+ const unpacked = variant.unpack();
236
+ `, 'unpacked', /Variant.*\[\]|Array.*Variant/);
237
+
238
+ expect(result.success).toBe(true);
239
+ expect(result.matches).toBe(true);
240
+ });
241
+
242
+ test('Complex type patterns should work', () => {
243
+ const result = expectIdentifierTypeAuto(`
244
+ import GLib from 'gi://GLib?version=2.0';
245
+ const variant = new GLib.Variant("(si)", ["hello", 42]);
246
+ const deepUnpack = variant.deepUnpack();
247
+ `, 'deepUnpack', /\[string,\s*number\]/);
248
+
249
+ expect(result.success).toBe(true);
250
+ expect(result.matches).toBe(true);
251
+ });
252
+ });
253
+ ```
254
+
255
+ ## Development
256
+
257
+ This package provides the foundation for building GIR type validation and inspection tools. The type expectation functionality enables precise testing of whether generated TypeScript definitions match the expected types, making it ideal for automated testing and validation of GIR type generation.
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@ts-for-gir/language-server",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript Language Server utilities for validating generated GIR type definitions",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "keywords": [
11
+ "typescript",
12
+ "language-server",
13
+ "gir",
14
+ "gobject",
15
+ "introspection",
16
+ "validation"
17
+ ],
18
+ "author": "ts-for-gir contributors",
19
+ "license": "MIT",
20
+ "engines": {
21
+ "node": ">=22"
22
+ },
23
+ "dependencies": {
24
+ "@ts-for-gir/lib": "^4.0.0-beta.26",
25
+ "typescript": "^5.9.2"
26
+ },
27
+ "scripts": {
28
+ "test": "vitest run",
29
+ "test:watch": "vitest"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^24.2.1",
33
+ "vitest": "^3.2.4"
34
+ }
35
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Basic validation tests for language-server utilities
3
+ * Tests the core functionality of TypeScript validation and hover features
4
+ */
5
+
6
+ import { describe, expect, it } from "vitest";
7
+ import { expectIdentifierType, getIdentifierType, validateTypeScript } from "../index.ts";
8
+
9
+ describe("Language Server Validation", () => {
10
+ describe("validateTypeScript", () => {
11
+ it("should validate correct TypeScript code", () => {
12
+ const code = `
13
+ interface User {
14
+ name: string;
15
+ age: number;
16
+ }
17
+
18
+ const user: User = {
19
+ name: "John",
20
+ age: 30
21
+ };
22
+ `;
23
+
24
+ const result = validateTypeScript(code);
25
+ expect(result.success).toBe(true);
26
+ expect(result.errors).toHaveLength(0);
27
+ });
28
+
29
+ it("should detect type errors", () => {
30
+ const code = `
31
+ const num: number = "not a number";
32
+ `;
33
+
34
+ const result = validateTypeScript(code);
35
+ expect(result.success).toBe(false);
36
+ expect(result.errors.length).toBeGreaterThan(0);
37
+ expect(result.errors[0]).toContain("Type 'string' is not assignable to type 'number'");
38
+ });
39
+
40
+ it("should detect missing properties", () => {
41
+ const code = `
42
+ interface Config {
43
+ host: string;
44
+ port: number;
45
+ }
46
+
47
+ const config: Config = {
48
+ host: "localhost"
49
+ // missing port
50
+ };
51
+ `;
52
+
53
+ const result = validateTypeScript(code);
54
+ expect(result.success).toBe(false);
55
+ expect(result.errors.length).toBeGreaterThan(0);
56
+ });
57
+ });
58
+
59
+ describe("getIdentifierType", () => {
60
+ it("should return type for simple variables", () => {
61
+ const code = `
62
+ const message: string = "Hello World";
63
+ const count: number = 42;
64
+ const isActive: boolean = true;
65
+ `;
66
+
67
+ const messageType = getIdentifierType(code, "message");
68
+ expect(messageType.success).toBe(true);
69
+ expect(messageType.type).toContain("string");
70
+
71
+ const countType = getIdentifierType(code, "count");
72
+ expect(countType.success).toBe(true);
73
+ expect(countType.type).toContain("number");
74
+
75
+ const activeType = getIdentifierType(code, "isActive");
76
+ expect(activeType.success).toBe(true);
77
+ expect(activeType.type).toContain("boolean");
78
+ });
79
+
80
+ it("should return type for interface instances", () => {
81
+ const code = `
82
+ interface Product {
83
+ id: string;
84
+ name: string;
85
+ price: number;
86
+ }
87
+
88
+ const product: Product = {
89
+ id: "123",
90
+ name: "Laptop",
91
+ price: 999.99
92
+ };
93
+ `;
94
+
95
+ const result = getIdentifierType(code, "product");
96
+ expect(result.success).toBe(true);
97
+ expect(result.type).toContain("Product");
98
+ });
99
+
100
+ it("should return type for function signatures", () => {
101
+ const code = `
102
+ function add(a: number, b: number): number {
103
+ return a + b;
104
+ }
105
+
106
+ const multiply = (x: number, y: number): number => x * y;
107
+ `;
108
+
109
+ const addType = getIdentifierType(code, "add");
110
+ expect(addType.success).toBe(true);
111
+ expect(addType.type).toMatch(/function|=>/);
112
+ expect(addType.type).toContain("number");
113
+
114
+ const multiplyType = getIdentifierType(code, "multiply");
115
+ expect(multiplyType.success).toBe(true);
116
+ expect(multiplyType.type).toMatch(/=>/);
117
+ expect(multiplyType.type).toContain("number");
118
+ });
119
+
120
+ it("should handle non-existent identifiers", () => {
121
+ const code = `
122
+ const existing = "value";
123
+ `;
124
+
125
+ const result = getIdentifierType(code, "nonExistent");
126
+ expect(result.success).toBe(false);
127
+ expect(result.error).toContain("not found");
128
+ });
129
+ });
130
+
131
+ describe("expectIdentifierType", () => {
132
+ it("should match exact types", () => {
133
+ const code = `
134
+ const text: string = "Hello";
135
+ const num: number = 42;
136
+ `;
137
+
138
+ const textResult = expectIdentifierType(code, "text", "string");
139
+ expect(textResult.success).toBe(true);
140
+ expect(textResult.matches).toBe(true);
141
+
142
+ const numResult = expectIdentifierType(code, "num", "number");
143
+ expect(numResult.success).toBe(true);
144
+ expect(numResult.matches).toBe(true);
145
+ });
146
+
147
+ it("should match with regex patterns", () => {
148
+ const code = `
149
+ const arr: string[] = ["one", "two"];
150
+ const tuple: [string, number] = ["hello", 42];
151
+ `;
152
+
153
+ const arrResult = expectIdentifierType(code, "arr", /string.*\[\]/);
154
+ expect(arrResult.success).toBe(true);
155
+ expect(arrResult.matches).toBe(true);
156
+
157
+ const tupleResult = expectIdentifierType(code, "tuple", /\[string,\s*number\]/);
158
+ expect(tupleResult.success).toBe(true);
159
+ expect(tupleResult.matches).toBe(true);
160
+ });
161
+
162
+ it("should detect type mismatches", () => {
163
+ const code = `
164
+ const value: string = "text";
165
+ `;
166
+
167
+ const result = expectIdentifierType(code, "value", "number");
168
+ expect(result.success).toBe(true);
169
+ expect(result.matches).toBe(false);
170
+ expect(result.actualType).toContain("string");
171
+ });
172
+
173
+ it("should handle complex types", () => {
174
+ const code = `
175
+ type Status = 'pending' | 'success' | 'error';
176
+ interface Response<T> {
177
+ data: T;
178
+ status: Status;
179
+ }
180
+
181
+ const response: Response<string> = {
182
+ data: "result",
183
+ status: 'success'
184
+ };
185
+ `;
186
+
187
+ const result = expectIdentifierType(code, "response", /Response<string>/);
188
+ expect(result.success).toBe(true);
189
+ expect(result.matches).toBe(true);
190
+ });
191
+ });
192
+ });
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./types/index.ts";
2
+ export * from "./validation.ts";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Result of a hover operation
3
+ */
4
+ export interface HoverResult {
5
+ success: boolean;
6
+ /** The inferred type at the position */
7
+ type?: string;
8
+ /** Documentation or additional information */
9
+ documentation?: string;
10
+ /** Error message if hover failed */
11
+ error?: string;
12
+ }
13
+
14
+ /**
15
+ * Options for hover operations
16
+ */
17
+ export interface HoverOptions {
18
+ strict?: boolean;
19
+ /**
20
+ * Path to the directory containing GIR type definitions (@types folder)
21
+ */
22
+ typesPath?: string;
23
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./validation.ts";
2
+ export * from "./hover.ts";
3
+ export * from "./type-expectation.ts";
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Result of a type expectation validation
3
+ */
4
+ export interface TypeExpectationResult {
5
+ success: boolean;
6
+ /** Whether the actual type matches the expected type */
7
+ matches: boolean;
8
+ /** The actual inferred type */
9
+ actualType?: string;
10
+ /** The expected type or pattern */
11
+ expectedType: string | RegExp;
12
+ /** Documentation or additional information */
13
+ documentation?: string;
14
+ /** Error message if validation failed */
15
+ error?: string;
16
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Result of a TypeScript validation operation
3
+ */
4
+ export interface ValidationResult {
5
+ success: boolean;
6
+ errors: string[];
7
+ warnings: string[];
8
+ }
9
+
10
+ /**
11
+ * Options for TypeScript validation operations
12
+ */
13
+ export interface ValidationOptions {
14
+ strict?: boolean;
15
+ /**
16
+ * Path to the directory containing GIR type definitions (@types folder)
17
+ */
18
+ typesPath?: string;
19
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./path.ts";
2
+ export * from "./string.ts";
3
+ export * from "./typescript.ts";
@@ -0,0 +1,34 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ /**
5
+ * Common paths where GIR types might be located
6
+ */
7
+ export const COMMON_TYPES_PATHS = [
8
+ path.resolve("./@types"),
9
+ path.resolve("../@types"),
10
+ path.resolve("../../@types"),
11
+ path.resolve("./types"),
12
+ path.resolve("../types"),
13
+ path.resolve("../../types"),
14
+ ] as const;
15
+
16
+ /**
17
+ * Find the first available types directory that contains .d.ts files
18
+ */
19
+ export function findTypesPath(): string | undefined {
20
+ for (const possiblePath of COMMON_TYPES_PATHS) {
21
+ try {
22
+ if (fs.existsSync(possiblePath)) {
23
+ // Check if it contains .d.ts files
24
+ const files = fs.readdirSync(possiblePath);
25
+ if (files.some((file) => file.endsWith(".d.ts"))) {
26
+ return possiblePath;
27
+ }
28
+ }
29
+ } catch {
30
+ // Continue to next path
31
+ }
32
+ }
33
+ return undefined;
34
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Helper function to normalize type strings for comparison
3
+ */
4
+ export function normalizeTypeString(typeString: string): string {
5
+ // Remove extra whitespace and normalize common type variations
6
+ return (
7
+ typeString
8
+ .trim()
9
+ .replace(/\s+/g, " ")
10
+ // Normalize union types (remove spaces around |)
11
+ .replace(/\s*\|\s*/g, "|")
12
+ // Normalize array types
13
+ .replace(/Array<(.+)>/g, "$1[]")
14
+ // Remove import() type wrappers for comparison
15
+ .replace(/import\(".*?"\)\./g, "")
16
+ );
17
+ }
18
+
19
+ /**
20
+ * Check if two type strings match, considering type aliases and normalization
21
+ */
22
+ export function typesMatch(actual: string, expected: string | RegExp): boolean {
23
+ // If expected is a RegExp, test against the actual type
24
+ if (expected instanceof RegExp) {
25
+ return expected.test(actual);
26
+ }
27
+
28
+ const normalizedActual = normalizeTypeString(actual);
29
+ const normalizedExpected = normalizeTypeString(expected);
30
+
31
+ // Direct match
32
+ if (normalizedActual === normalizedExpected) {
33
+ return true;
34
+ }
35
+
36
+ // Check for common type aliases
37
+ const typeAliases = new Map([
38
+ ["number", "Number"],
39
+ ["string", "String"],
40
+ ["boolean", "Boolean"],
41
+ ["object", "Object"],
42
+ ]);
43
+
44
+ // Check both directions of type aliases
45
+ return (
46
+ typeAliases.get(normalizedActual) === normalizedExpected || typeAliases.get(normalizedExpected) === normalizedActual
47
+ );
48
+ }
@@ -0,0 +1,161 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as ts from "typescript";
4
+ import type { HoverOptions } from "../types/index.ts";
5
+
6
+ /**
7
+ * Create TypeScript compiler options optimized for GJS/GIR types
8
+ */
9
+ export function createCompilerOptions(strict?: boolean): ts.CompilerOptions {
10
+ return {
11
+ target: ts.ScriptTarget.ESNext,
12
+ module: ts.ModuleKind.ESNext,
13
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
14
+ skipLibCheck: true,
15
+ strict: strict ?? true,
16
+ noImplicitAny: true,
17
+ strictNullChecks: true,
18
+ noImplicitThis: true,
19
+ alwaysStrict: true,
20
+ noEmit: true,
21
+ allowSyntheticDefaultImports: true,
22
+ esModuleInterop: true,
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Create a virtual file system for type definitions
28
+ */
29
+ export function createVirtualFileSystem(typesPath?: string): Map<string, string> {
30
+ const virtualFileSystem = new Map<string, string>();
31
+
32
+ if (!typesPath) {
33
+ return virtualFileSystem;
34
+ }
35
+
36
+ try {
37
+ const files = fs.readdirSync(typesPath);
38
+ for (const file of files) {
39
+ if (file.endsWith(".d.ts")) {
40
+ const filePath = path.resolve(typesPath, file);
41
+ const content = fs.readFileSync(filePath, "utf-8");
42
+ // Store with full path for primary access
43
+ virtualFileSystem.set(filePath, content);
44
+ // Also store with relative path for triple-slash references
45
+ virtualFileSystem.set(`./${file}`, content);
46
+ virtualFileSystem.set(`./types/${file}`, content);
47
+ virtualFileSystem.set(`@types/${file}`, content);
48
+ }
49
+ }
50
+ } catch {
51
+ // Types path not accessible, continue without types
52
+ }
53
+
54
+ return virtualFileSystem;
55
+ }
56
+
57
+ /**
58
+ * Create a TypeScript compiler host with virtual file system support
59
+ */
60
+ export function createCompilerHost(
61
+ compilerOptions: ts.CompilerOptions,
62
+ sourceFile: ts.SourceFile,
63
+ virtualFileSystem: Map<string, string>,
64
+ fileName: string,
65
+ ): ts.CompilerHost {
66
+ const host = ts.createCompilerHost(compilerOptions);
67
+
68
+ // Override file resolution to use virtual file system
69
+ const originalGetSourceFile = host.getSourceFile;
70
+ host.getSourceFile = (name, languageVersion) => {
71
+ if (name === fileName) {
72
+ return sourceFile;
73
+ }
74
+
75
+ // Check virtual file system first
76
+ if (virtualFileSystem.has(name)) {
77
+ const content = virtualFileSystem.get(name);
78
+ if (content !== undefined) {
79
+ return ts.createSourceFile(name, content, languageVersion, true);
80
+ }
81
+ }
82
+
83
+ // Check for relative path references
84
+ const baseName = path.basename(name);
85
+ if (virtualFileSystem.has(baseName)) {
86
+ const content = virtualFileSystem.get(baseName);
87
+ if (content !== undefined) {
88
+ return ts.createSourceFile(name, content, languageVersion, true);
89
+ }
90
+ }
91
+
92
+ return originalGetSourceFile.call(host, name, languageVersion);
93
+ };
94
+
95
+ return host;
96
+ }
97
+
98
+ /**
99
+ * Create a TypeScript program with virtual file system support
100
+ */
101
+ export function createTypeScriptProgram(code: string, options: HoverOptions = {}) {
102
+ const fileName = "test.ts";
103
+ const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.ESNext, true);
104
+
105
+ const compilerOptions = createCompilerOptions(options.strict);
106
+ const virtualFileSystem = createVirtualFileSystem(options.typesPath);
107
+ const host = createCompilerHost(compilerOptions, sourceFile, virtualFileSystem, fileName);
108
+
109
+ // Files to include in the program
110
+ const filesToInclude = [fileName];
111
+
112
+ // Include index.d.ts if available
113
+ if (options.typesPath) {
114
+ const indexPath = path.resolve(options.typesPath, "index.d.ts");
115
+ if (virtualFileSystem.has(indexPath)) {
116
+ filesToInclude.push(indexPath);
117
+ }
118
+ }
119
+
120
+ const program = ts.createProgram(filesToInclude, compilerOptions, host);
121
+
122
+ return { program, sourceFile };
123
+ }
124
+
125
+ /**
126
+ * Find the position of a variable or identifier in TypeScript code
127
+ */
128
+ export function findIdentifierPosition(sourceFile: ts.SourceFile, identifier: string): number | undefined {
129
+ let position: number | undefined;
130
+
131
+ function visit(node: ts.Node): void {
132
+ if (ts.isIdentifier(node) && node.text === identifier) {
133
+ // Prefer variable declarations over references
134
+ if (ts.isVariableDeclaration(node.parent) && node.parent.name === node) {
135
+ position = node.getStart();
136
+ return;
137
+ }
138
+ // Fall back to any identifier if no declaration found
139
+ if (position === undefined) {
140
+ position = node.getStart();
141
+ }
142
+ }
143
+ ts.forEachChild(node, visit);
144
+ }
145
+
146
+ visit(sourceFile);
147
+ return position;
148
+ }
149
+
150
+ /**
151
+ * Get the node at a specific position in the source file
152
+ */
153
+ export function getNodeAtPosition(sourceFile: ts.SourceFile, position: number): ts.Node | undefined {
154
+ function find(node: ts.Node): ts.Node | undefined {
155
+ if (position >= node.getStart() && position < node.getEnd()) {
156
+ return ts.forEachChild(node, find) || node;
157
+ }
158
+ return undefined;
159
+ }
160
+ return find(sourceFile);
161
+ }
@@ -0,0 +1,303 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as ts from "typescript";
4
+ import type {
5
+ HoverOptions,
6
+ HoverResult,
7
+ TypeExpectationResult,
8
+ ValidationOptions,
9
+ ValidationResult,
10
+ } from "./types/index.ts";
11
+ import { findTypesPath } from "./utils/path.ts";
12
+ import { typesMatch } from "./utils/string.ts";
13
+ import { createTypeScriptProgram, findIdentifierPosition, getNodeAtPosition } from "./utils/typescript.ts";
14
+
15
+ /**
16
+ * TypeScript validation for generated GIR types
17
+ */
18
+ export function validateTypeScript(code: string, options: ValidationOptions = {}): ValidationResult {
19
+ try {
20
+ // Compiler options optimized for GJS/GIR types
21
+ const compilerOptions: ts.CompilerOptions = {
22
+ target: ts.ScriptTarget.ESNext,
23
+ module: ts.ModuleKind.ESNext,
24
+ // Don't specify lib to let TypeScript use defaults for target
25
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
26
+ skipLibCheck: true, // Skip lib checking to avoid path resolution issues
27
+ strict: options.strict ?? true,
28
+ noImplicitAny: true,
29
+ strictNullChecks: true,
30
+ noImplicitThis: true,
31
+ alwaysStrict: true,
32
+ noEmit: true,
33
+ // Allow module resolution for GIR modules
34
+ allowSyntheticDefaultImports: true,
35
+ esModuleInterop: true,
36
+ };
37
+
38
+ // Create source file
39
+ const fileName = "test.ts";
40
+ const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.ESNext, true);
41
+
42
+ // Files to include in the program
43
+ const filesToInclude = [fileName];
44
+
45
+ // Create virtual file system for type definitions
46
+ const virtualFileSystem = new Map<string, string>();
47
+
48
+ // Load all type definition files if types path is provided
49
+ if (options.typesPath) {
50
+ try {
51
+ const files = fs.readdirSync(options.typesPath);
52
+ for (const file of files) {
53
+ if (file.endsWith(".d.ts")) {
54
+ const filePath = path.resolve(options.typesPath, file);
55
+ const content = fs.readFileSync(filePath, "utf-8");
56
+ // Store with full path for primary access
57
+ virtualFileSystem.set(filePath, content);
58
+ // Also store with relative path for triple-slash references
59
+ virtualFileSystem.set(`./${file}`, content);
60
+ virtualFileSystem.set(`./types/${file}`, content);
61
+ virtualFileSystem.set(`@types/${file}`, content);
62
+ }
63
+ }
64
+ } catch {
65
+ // Types path not accessible, continue without types
66
+ }
67
+ }
68
+
69
+ // Include index.d.ts if available
70
+ if (options.typesPath) {
71
+ const indexPath = path.resolve(options.typesPath, "index.d.ts");
72
+ if (virtualFileSystem.has(indexPath)) {
73
+ filesToInclude.push(indexPath);
74
+ }
75
+ }
76
+
77
+ // Create compiler host
78
+ const host = ts.createCompilerHost(compilerOptions);
79
+
80
+ // Override file resolution to use virtual file system
81
+ const originalGetSourceFile = host.getSourceFile;
82
+ host.getSourceFile = (name, languageVersion) => {
83
+ if (name === fileName) {
84
+ return sourceFile;
85
+ }
86
+
87
+ // Check virtual file system first
88
+ if (virtualFileSystem.has(name)) {
89
+ const content = virtualFileSystem.get(name);
90
+ if (content !== undefined) {
91
+ return ts.createSourceFile(name, content, languageVersion, true);
92
+ }
93
+ }
94
+
95
+ // Check for relative path references
96
+ const baseName = path.basename(name);
97
+ if (virtualFileSystem.has(baseName)) {
98
+ const content = virtualFileSystem.get(baseName);
99
+ if (content !== undefined) {
100
+ return ts.createSourceFile(name, content, languageVersion, true);
101
+ }
102
+ }
103
+
104
+ return originalGetSourceFile.call(host, name, languageVersion);
105
+ };
106
+
107
+ // Create program and get diagnostics
108
+ const program = ts.createProgram(filesToInclude, compilerOptions, host);
109
+ const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile);
110
+
111
+ const errors: string[] = [];
112
+ const warnings: string[] = [];
113
+
114
+ for (const diagnostic of diagnostics) {
115
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
116
+
117
+ if (diagnostic.category === ts.DiagnosticCategory.Error) {
118
+ errors.push(message);
119
+ } else if (diagnostic.category === ts.DiagnosticCategory.Warning) {
120
+ warnings.push(message);
121
+ }
122
+ }
123
+
124
+ return {
125
+ success: errors.length === 0,
126
+ errors,
127
+ warnings,
128
+ };
129
+ } catch (error) {
130
+ return {
131
+ success: false,
132
+ errors: [`Validation failed: ${error}`],
133
+ warnings: [],
134
+ };
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Validate GIR TypeScript code with type definitions
140
+ */
141
+ export function validateGIRTypeScript(
142
+ code: string,
143
+ typesPath: string,
144
+ options: Omit<ValidationOptions, "typesPath"> = {},
145
+ ): ValidationResult {
146
+ return validateTypeScript(code, {
147
+ ...options,
148
+ typesPath,
149
+ });
150
+ }
151
+
152
+ /**
153
+ * Validate GIR TypeScript code with automatic type discovery
154
+ * Automatically finds and includes GIR type definitions from common locations
155
+ */
156
+ export function validateGIRTypeScriptAuto(
157
+ code: string,
158
+ options: Omit<ValidationOptions, "typesPath"> = {},
159
+ ): ValidationResult {
160
+ const typesPath = findTypesPath();
161
+ return validateTypeScript(code, {
162
+ ...options,
163
+ typesPath,
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Get type information for a specific identifier in TypeScript code
169
+ */
170
+ export function getIdentifierType(code: string, identifier: string, options: HoverOptions = {}): HoverResult {
171
+ try {
172
+ const { program, sourceFile } = createTypeScriptProgram(code, options);
173
+
174
+ // Find the position of the identifier
175
+ const position = findIdentifierPosition(sourceFile, identifier);
176
+ if (position === undefined) {
177
+ return {
178
+ success: false,
179
+ error: `Identifier '${identifier}' not found in code`,
180
+ };
181
+ }
182
+
183
+ // Get type checker
184
+ const typeChecker = program.getTypeChecker();
185
+
186
+ // Find the node at the position
187
+ const node = getNodeAtPosition(sourceFile, position);
188
+ if (!node) {
189
+ return {
190
+ success: false,
191
+ error: `No node found at position ${position}`,
192
+ };
193
+ }
194
+
195
+ // Get the type of the node
196
+ const type = typeChecker.getTypeAtLocation(node);
197
+ const typeString = typeChecker.typeToString(type);
198
+
199
+ // Get symbol information for documentation
200
+ const symbol = typeChecker.getSymbolAtLocation(node);
201
+ const documentation = symbol
202
+ ?.getDocumentationComment(typeChecker)
203
+ ?.map((part) => part.text)
204
+ ?.join("\n");
205
+
206
+ return {
207
+ success: true,
208
+ type: typeString,
209
+ documentation: documentation || undefined,
210
+ };
211
+ } catch (error) {
212
+ return {
213
+ success: false,
214
+ error: `Type checking failed: ${error}`,
215
+ };
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Get type information for a specific identifier in TypeScript code with automatic type discovery
221
+ */
222
+ export function getIdentifierTypeAuto(
223
+ code: string,
224
+ identifier: string,
225
+ options: Omit<HoverOptions, "typesPath"> = {},
226
+ ): HoverResult {
227
+ const typesPath = findTypesPath();
228
+ return getIdentifierType(code, identifier, {
229
+ ...options,
230
+ typesPath,
231
+ });
232
+ }
233
+
234
+ /**
235
+ * Validate that a specific identifier has the expected type
236
+ */
237
+ export function expectIdentifierType(
238
+ code: string,
239
+ identifier: string,
240
+ expectedType: string | RegExp,
241
+ options: HoverOptions = {},
242
+ ): TypeExpectationResult {
243
+ try {
244
+ // First get the actual type
245
+ const hoverResult = getIdentifierType(code, identifier, options);
246
+
247
+ if (!hoverResult.success) {
248
+ return {
249
+ success: false,
250
+ matches: false,
251
+ expectedType,
252
+ error: hoverResult.error,
253
+ };
254
+ }
255
+
256
+ const actualType = hoverResult.type;
257
+ if (!actualType) {
258
+ return {
259
+ success: false,
260
+ matches: false,
261
+ expectedType,
262
+ error: "No type information available",
263
+ };
264
+ }
265
+
266
+ // Check if types match
267
+ const matches = typesMatch(actualType, expectedType);
268
+
269
+ const expectedTypeDisplay = expectedType instanceof RegExp ? expectedType.toString() : expectedType;
270
+
271
+ return {
272
+ success: true,
273
+ matches,
274
+ actualType,
275
+ expectedType,
276
+ documentation: hoverResult.documentation,
277
+ error: matches ? undefined : `Expected type '${expectedTypeDisplay}' but got '${actualType}'`,
278
+ };
279
+ } catch (error) {
280
+ return {
281
+ success: false,
282
+ matches: false,
283
+ expectedType,
284
+ error: `Type expectation validation failed: ${error}`,
285
+ };
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Validate that a specific identifier has the expected type with automatic type discovery
291
+ */
292
+ export function expectIdentifierTypeAuto(
293
+ code: string,
294
+ identifier: string,
295
+ expectedType: string | RegExp,
296
+ options: Omit<HoverOptions, "typesPath"> = {},
297
+ ): TypeExpectationResult {
298
+ const typesPath = findTypesPath();
299
+ return expectIdentifierType(code, identifier, expectedType, {
300
+ ...options,
301
+ typesPath,
302
+ });
303
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["ESNext"],
4
+ "target": "ESNext",
5
+ "module": "ESNext",
6
+ "outDir": "lib",
7
+ "strict": true,
8
+ "noImplicitAny": false,
9
+ "strictNullChecks": true,
10
+ "noImplicitThis": true,
11
+ "alwaysStrict": true,
12
+ "esModuleInterop": true,
13
+ "moduleResolution": "bundler",
14
+ "allowImportingTsExtensions": true,
15
+ "verbatimModuleSyntax": true,
16
+ "noEmit": true,
17
+ "rootDir": "src",
18
+ "resolveJsonModule": true,
19
+ "skipLibCheck": true,
20
+ "diagnostics": true,
21
+ "types": ["vitest/globals"]
22
+ },
23
+ "include": ["src/**/*.ts"],
24
+ "exclude": ["lib", "node_modules", "**/*.spec.ts"]
25
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: "node",
6
+ include: ["src/**/*.test.ts", "src/**/*.spec.ts"],
7
+ testTimeout: 10000,
8
+ hookTimeout: 10000,
9
+ globals: true,
10
+ },
11
+ });