@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
JavaScript
"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