UNPKG

@prometx/trpc-navigation-plugin

Version:

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

368 lines 15.4 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.Navigator = void 0; const path = __importStar(require("node:path")); const ts = __importStar(require("typescript/lib/tsserverlibrary")); class Navigator { logger; serverHost; config; constructor(logger, serverHost, config) { this.logger = logger; this.serverHost = serverHost; this.config = config; } /** * Navigates through a router structure to find the target definition */ navigateRouterPath(routerSymbol, pathSegments, typeChecker) { try { if (!routerSymbol.valueDeclaration) { this.logger.debug(`No value declaration for router symbol`); return null; } let currentDeclaration = routerSymbol.valueDeclaration; // Process each segment for (let i = 0; i < pathSegments.length; i++) { const segment = pathSegments[i]; const isLastSegment = i === pathSegments.length - 1; this.logger.debug(`Processing segment: ${segment} (${i + 1}/${pathSegments.length})`); const segmentResult = this.processRouterSegment(currentDeclaration, segment, isLastSegment, typeChecker); if (segmentResult) { if (segmentResult.definition) { return segmentResult.definition; } if (segmentResult.nextDeclaration) { currentDeclaration = segmentResult.nextDeclaration; continue; } } // If we can't process further, return current location break; } // Return the current declaration location return this.createDefinitionFromDeclaration(currentDeclaration, pathSegments[pathSegments.length - 1] || 'router'); } catch (error) { this.logger.error(`Error navigating router path`, error); return null; } } processRouterSegment(declaration, segment, isLastSegment, _typeChecker) { if (!ts.isVariableDeclaration(declaration) || !declaration.initializer) { return null; } let routesObject = null; // Check for router() call pattern const routerCall = this.findRouterCall(declaration.initializer); if (routerCall && routerCall.arguments.length > 0 && ts.isObjectLiteralExpression(routerCall.arguments[0])) { this.logger.debug(`Found router call with object literal argument`); routesObject = routerCall.arguments[0]; } // Check for object literal pattern (with or without satisfies) else if (ts.isObjectLiteralExpression(declaration.initializer)) { routesObject = declaration.initializer; } // Check for satisfies expression with object literal else if (ts.isSatisfiesExpression(declaration.initializer) && ts.isObjectLiteralExpression(declaration.initializer.expression)) { routesObject = declaration.initializer.expression; } if (!routesObject) { return null; } // Find the property matching the segment for (const prop of routesObject.properties) { if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name) || prop.name.text !== segment) { continue; } // Handle different property value types if (ts.isIdentifier(prop.initializer)) { return this.handleIdentifierProperty(prop.initializer, segment, isLastSegment, declaration.getSourceFile()); } else if (ts.isCallExpression(prop.initializer)) { return this.handleInlineProcedure(prop, segment, isLastSegment); } break; } return null; } handleIdentifierProperty(identifier, segment, isLastSegment, sourceFile) { const declaration = this.findDeclaration(identifier.text, sourceFile); if (!declaration) { this.logger.debug(`Could not find declaration for ${identifier.text}`); return null; } const isProcedure = this.isProcedureDeclaration(declaration); const isRouter = this.isRouterDeclaration(declaration.initializer); this.logger.debug(`Found ${identifier.text}: isProcedure=${isProcedure}, isRouter=${isRouter}, isLastSegment=${isLastSegment}`); if (isProcedure && isLastSegment) { // This is a procedure and we're at the end of the path return { definition: this.createDefinitionFromDeclaration(declaration, segment), }; } else if (!isProcedure && isLastSegment) { // This is a router and we're at the end - go to the router definition return { definition: this.createDefinitionFromDeclaration(declaration, segment), }; } else if (!isProcedure) { // This is a router and not the last segment - continue navigation return { nextDeclaration: declaration }; } else { // This is a procedure but not the last segment - shouldn't happen with valid tRPC return { definition: this.createDefinitionFromDeclaration(declaration, segment), }; } } handleInlineProcedure(prop, segment, isLastSegment) { if (!isLastSegment || !this.isProcedureCall(prop.initializer)) { return null; } const start = prop.getStart(); const sourceFile = prop.getSourceFile(); return { definition: { fileName: sourceFile.fileName, textSpan: { start, length: prop.getEnd() - start, }, kind: ts.ScriptElementKind.functionElement, name: segment, containerKind: ts.ScriptElementKind.moduleElement, containerName: 'TRPC Procedure', }, }; } findDeclaration(name, sourceFile) { let result = null; const visit = (node) => { if (ts.isVariableStatement(node)) { for (const decl of node.declarationList.declarations) { if (ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name) && decl.name.text === name) { result = decl; return; } } } ts.forEachChild(node, visit); }; visit(sourceFile); // If not found locally, check imports if (!result) { this.logger.debug(`${name} not found locally, checking imports`); result = this.findImportedDeclaration(name, sourceFile); } return result; } findImportedDeclaration(name, sourceFile) { let foundImportPath; // Find the import declaration const visit = (node) => { if (ts.isImportDeclaration(node) && node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) { for (const specifier of node.importClause.namedBindings.elements) { if (specifier.name.text === name) { if (ts.isStringLiteral(node.moduleSpecifier)) { foundImportPath = node.moduleSpecifier.text; return; } } } } ts.forEachChild(node, visit); }; visit(sourceFile); if (!foundImportPath) { this.logger.debug(`No import found for ${name}`); return null; } const importPath = foundImportPath; this.logger.debug(`Found import for ${name}: ${importPath}`); // Resolve the import path const currentDir = path.dirname(sourceFile.fileName); let resolvedPath = importPath; if (importPath.startsWith('.')) { resolvedPath = path.resolve(currentDir, importPath); // Try different extensions const extensions = [ '', ...this.config.fileExtensions, ...this.config.fileExtensions.map((ext) => `/index${ext}`), ]; for (const ext of extensions) { const fullPath = resolvedPath + ext; if (this.serverHost.fileExists(fullPath)) { // Read and parse the file const content = this.serverHost.readFile(fullPath); if (content) { const importedFile = ts.createSourceFile(fullPath, content, ts.ScriptTarget.Latest, true); // Look for the exported declaration return this.findExportedDeclaration(name, importedFile); } } } } return null; } findExportedDeclaration(name, sourceFile) { let result = null; const visit = (node) => { if (ts.isVariableStatement(node)) { const isExported = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword); if (isExported) { for (const decl of node.declarationList.declarations) { if (ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name) && decl.name.text === name) { result = decl; return; } } } } ts.forEachChild(node, visit); }; visit(sourceFile); return result; } findRouterCall(node) { if (ts.isCallExpression(node)) { const expr = node.expression; // Support various router creation patterns if (ts.isIdentifier(expr)) { const text = expr.text; if (this.config.patterns.routerFunctions.some((func) => { // Exact match or common variations return text === func || text.toLowerCase() === func.toLowerCase() || // Handle property access patterns like t.router (func.includes('.') && text.endsWith(func)); })) { return node; } } else if (ts.isPropertyAccessExpression(expr) && expr.name.text === 'router') { return node; } } let result = null; ts.forEachChild(node, (child) => { if (!result) { result = this.findRouterCall(child); } }); return result; } isProcedureDeclaration(decl) { if (!ts.isVariableDeclaration(decl) || !decl.initializer) { return false; } // First check if it's a router - routers take precedence if (this.isRouterDeclaration(decl.initializer)) { return false; } // Now check if it's a procedure const text = decl.initializer.getText(); const hasProcedure = text.includes('Procedure'); const hasProcedureType = this.config.patterns.procedureTypes.some((type) => text.includes(`.${type}`)); return hasProcedure && hasProcedureType; } isRouterDeclaration(node) { // Check for router() call pattern const text = node.getText(); if (text.includes('router(')) { return true; } // Check for object router pattern return this.isObjectRouter(node); } isObjectRouter(node) { // Check if it's an object literal (with or without satisfies) that contains procedure definitions let objLiteral = null; if (ts.isObjectLiteralExpression(node)) { objLiteral = node; } else if (ts.isSatisfiesExpression(node) && ts.isObjectLiteralExpression(node.expression)) { objLiteral = node.expression; } if (!objLiteral) return false; // Check if any properties are procedures or router references for (const prop of objLiteral.properties) { if (ts.isPropertyAssignment(prop) && prop.initializer) { const propText = prop.initializer.getText(); // It's a procedure if (this.config.patterns.procedureTypes.some((type) => propText.includes(`.${type}`))) { return true; } // It's likely a reference to another router (identifier that could be a router) if (ts.isIdentifier(prop.initializer)) { return true; } } } return false; } isProcedureCall(node) { const text = node.getText(); const hasProcedure = text.includes('Procedure'); const hasProcedureType = this.config.patterns.procedureTypes.some((type) => text.includes(`.${type}`)); const isNotRouter = !text.includes('router('); return hasProcedure && hasProcedureType && isNotRouter; } createDefinitionFromDeclaration(declaration, name) { const sourceFile = declaration.getSourceFile(); const start = declaration.getStart(); return { fileName: sourceFile.fileName, textSpan: { start, length: declaration.getEnd() - start, }, kind: ts.ScriptElementKind.moduleElement, name, containerKind: ts.ScriptElementKind.moduleElement, containerName: 'TRPC', }; } } exports.Navigator = Navigator; //# sourceMappingURL=navigator.js.map