UNPKG

@prometx/trpc-navigation-plugin

Version:

TypeScript Language Service Plugin that fixes broken 'go to definition' for tRPC when using declaration emit

227 lines 9.72 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.TypeResolver = void 0; const path = __importStar(require("node:path")); const ts = __importStar(require("typescript/lib/tsserverlibrary")); class TypeResolver { logger; serverHost; config; constructor(logger, serverHost, config) { this.logger = logger; this.serverHost = serverHost; this.config = config; } /** * Extracts router type information using configured router location */ extractRouterType(variableName, _sourceFile, _position, typeChecker, program) { this.logger.debug(`🔍 extractRouterType called for ${variableName}`); try { // Use configured router location if (!this.config.router) { this.logger.error('No router configuration provided'); return null; } // Resolve the router file path // In monorepos, we need to find where the tsconfig.json is located const configFile = program.getCompilerOptions().configFilePath; const configDir = configFile ? path.dirname(configFile) : program.getCurrentDirectory(); const routerPath = path.isAbsolute(this.config.router.filePath) ? this.config.router.filePath : path.resolve(configDir, this.config.router.filePath); this.logger.debug(`Config dir: ${configDir}`); this.logger.debug(`Looking for router at: ${routerPath}`); // Get the router source file let routerSourceFile = program.getSourceFile(routerPath); // If not in program, try to read and parse it directly if (!routerSourceFile) { // Check if file exists if (!this.serverHost.fileExists(routerPath)) { this.logger.error(`Router file does not exist: ${routerPath}`); return null; } // Read and parse the file const fileContent = this.serverHost.readFile(routerPath); if (!fileContent) { this.logger.error(`Could not read router file: ${routerPath}`); return null; } routerSourceFile = ts.createSourceFile(routerPath, fileContent, ts.ScriptTarget.Latest, true); } // Find the router variable in the file const routerSymbol = this.findRouterVariable(routerSourceFile, this.config.router.variableName, typeChecker); if (!routerSymbol) { this.logger.error(`Could not find router variable '${this.config.router.variableName}' in ${routerPath}`); return null; } this.logger.info(`✅ Found router ${this.config.router.variableName} in ${routerPath}`); return { routerSymbol, routerFile: routerPath, }; } catch (error) { this.logger.error(`Error extracting router type for ${variableName}`, error); return null; } } /** * Find a router variable in a source file by name */ findRouterVariable(sourceFile, variableName, typeChecker) { let routerNode = null; const visit = (node) => { if (routerNode) return; // Look for variable declarations if (ts.isVariableStatement(node)) { for (const decl of node.declarationList.declarations) { if (ts.isIdentifier(decl.name) && decl.name.text === variableName) { routerNode = decl; return; } } } // Look for export declarations if (ts.isExportAssignment(node) && !node.isExportEquals && node.expression) { if (ts.isIdentifier(node.expression) && node.expression.text === variableName) { routerNode = node; return; } } // Look for named exports if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) { for (const element of node.exportClause.elements) { const exportedName = element.name?.text || element.propertyName?.text; if (exportedName === variableName) { routerNode = element; return; } } } ts.forEachChild(node, visit); }; visit(sourceFile); if (!routerNode) { return null; } // Try to get symbol from type checker first const symbol = typeChecker.getSymbolAtLocation(routerNode); if (symbol) { return symbol; } // If no symbol (e.g., file not in program), create a pseudo-symbol return { name: variableName, flags: ts.SymbolFlags.Value, valueDeclaration: routerNode, getDeclarations: () => [routerNode], declarations: [routerNode], }; } /** * Checks if a variable is a tRPC client */ isTrpcClient(variableName, sourceFile, position, typeChecker) { this.logger.debug(`🔎 Checking if ${variableName} is a tRPC client`); const identifier = this.findIdentifierAtPosition(sourceFile, variableName, position); if (!identifier) { this.logger.debug(`❌ No identifier found`); return false; } const symbol = typeChecker.getSymbolAtLocation(identifier); if (!symbol) { this.logger.debug(`❌ No symbol found`); return false; } // Check if it's an alias (imported) const resolvedSymbol = symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol; // Check the initializer text const initializer = this.findInitializer(resolvedSymbol); if (initializer) { const text = initializer.getText(); this.logger.debug(`📄 Initializer: ${text.substring(0, 100)}...`); const hasClientInitializer = this.config.patterns.clientInitializers.some((pattern) => text.includes(pattern)); const hasUtilsMethod = text.includes(this.config.patterns.utilsMethod); const hasContext = text.includes('useContext'); if (hasClientInitializer || hasUtilsMethod || hasContext) { this.logger.info(`✅ Found tRPC client by initializer: ${variableName}`); return true; } } // Check the type name const type = typeChecker.getTypeOfSymbolAtLocation(symbol, identifier); const typeName = typeChecker.typeToString(type); this.logger.debug(`📊 Type: ${typeName.substring(0, 200)}...`); const isTrpc = typeName.includes('TRPC') || typeName.includes('CreateTRPC') || typeName.includes('TRPCClient') || typeName.includes('Proxy<DecoratedProcedureRecord') || typeName.includes('AnyRouter'); if (isTrpc) { this.logger.info(`✅ Found tRPC client by type: ${variableName}`); } return isTrpc; } findIdentifierAtPosition(sourceFile, variableName, position) { let result; function visit(node) { if (ts.isIdentifier(node) && node.text === variableName) { // If no result yet, or this identifier is closer to the position if (!result || (position >= node.getStart() && position <= node.getEnd())) { result = node; } } ts.forEachChild(node, visit); } visit(sourceFile); return result; } findInitializer(symbol) { const declarations = symbol.getDeclarations(); if (!declarations) return undefined; for (const decl of declarations) { if (ts.isVariableDeclaration(decl) && decl.initializer) { return decl.initializer; } } return undefined; } } exports.TypeResolver = TypeResolver; //# sourceMappingURL=type-resolver.js.map