lynkr
Version:
Self-hosted LLM gateway and tier-routing proxy for Claude Code, Cursor, and Codex. Routes across Ollama, AWS Bedrock, OpenRouter, Databricks, Azure OpenAI, llama.cpp, and LM Studio with prompt caching, MCP tools, and 60-80% cost savings.
444 lines (420 loc) • 11.9 kB
JavaScript
const logger = require("../logger");
// Lazy load heavy dependencies
let Parser = null;
let JavaScript = null;
let TypeScript = null;
let TSX = null;
let Python = null;
let treeSitterAvailable = null; // null = not checked, true/false = result
function isTreeSitterAvailable() {
if (treeSitterAvailable !== null) {
return treeSitterAvailable;
}
try {
require.resolve("tree-sitter");
treeSitterAvailable = true;
logger.info("[Parser] tree-sitter available");
} catch {
treeSitterAvailable = false;
logger.info("[Parser] tree-sitter not available - using babel fallback for JS/TS (Python parsing disabled)");
}
return treeSitterAvailable;
}
function getTreeSitterParser() {
if (!isTreeSitterAvailable()) {
return null;
}
if (!Parser) {
try {
Parser = require("tree-sitter");
} catch (err) {
logger.warn({ err: err.message }, "[Parser] Failed to load tree-sitter");
treeSitterAvailable = false;
return null;
}
}
return Parser;
}
function getLanguageModule(language) {
if (!isTreeSitterAvailable()) {
return null;
}
try {
switch (language) {
case "javascript":
case "javascript-react":
if (!JavaScript) {
JavaScript = require("tree-sitter-javascript");
}
return JavaScript;
case "typescript":
if (!TypeScript) {
const ts = require("tree-sitter-typescript");
TypeScript = ts.typescript;
}
return TypeScript;
case "typescript-react":
if (!TSX) {
const ts = require("tree-sitter-typescript");
TSX = ts.tsx;
}
return TSX;
case "python":
if (!Python) {
Python = require("tree-sitter-python");
}
return Python;
default:
return null;
}
} catch (err) {
logger.warn({ err: err.message, language }, "[Parser] Failed to load language module");
return null;
}
}
const parserCache = {};
const LANGUAGE_MAP = {
javascript: { getLanguage: () => getLanguageModule("javascript"), type: "javascript" },
"javascript-react": { getLanguage: () => getLanguageModule("javascript-react"), type: "javascript" },
typescript: { getLanguage: () => getLanguageModule("typescript"), type: "typescript" },
"typescript-react": { getLanguage: () => getLanguageModule("typescript-react"), type: "typescript" },
python: { getLanguage: () => getLanguageModule("python"), type: "python" },
};
function getParser(languageKey) {
if (!isTreeSitterAvailable()) {
return null;
}
const entry = LANGUAGE_MAP[languageKey];
if (!entry) return null;
if (!parserCache[languageKey]) {
const ParserClass = getTreeSitterParser();
if (!ParserClass) return null;
const parser = new ParserClass();
const language = entry.getLanguage();
if (!language) return null;
parser.setLanguage(language);
parserCache[languageKey] = parser;
}
return parserCache[languageKey];
}
function nodeText(node, source) {
return source.slice(node.startIndex, node.endIndex);
}
function positionOf(node) {
return {
line: node.startPosition.row + 1,
column: node.startPosition.column + 1,
};
}
function identifierName(node, source) {
if (!node) return null;
if (node.type === "identifier" || node.type === "property_identifier") {
return nodeText(node, source);
}
return null;
}
function extractJavaScript(tree, source) {
const symbols = [];
const dependencies = [];
const references = [];
const imports = [];
const exports = [];
const registerReference = (nameNode, refNode) => {
if (!nameNode || !refNode) return;
const name = identifierName(nameNode, source);
if (!name) return;
references.push({
name,
line: refNode.startPosition.row + 1,
column: refNode.startPosition.column + 1,
snippet: nodeText(refNode, source),
});
};
const visit = (node) => {
switch (node.type) {
case "function_declaration": {
const nameNode = node.childForFieldName("name");
if (nameNode) {
symbols.push({
name: nodeText(nameNode, source),
kind: "function",
...positionOf(nameNode),
});
}
break;
}
case "class_declaration": {
const nameNode = node.childForFieldName("name");
if (nameNode) {
symbols.push({
name: nodeText(nameNode, source),
kind: "class",
...positionOf(nameNode),
});
}
break;
}
case "method_definition": {
const nameNode = node.childForFieldName("name");
if (nameNode) {
symbols.push({
name: nodeText(nameNode, source),
kind: "method",
...positionOf(nameNode),
});
}
break;
}
case "arrow_function": {
const parent = node.parent;
if (parent && parent.type === "variable_declarator") {
const nameNode = parent.childForFieldName("name");
if (nameNode) {
symbols.push({
name: nodeText(nameNode, source),
kind: "function",
...positionOf(nameNode),
});
}
}
break;
}
case "identifier": {
registerReference(node, node);
break;
}
case "import_statement": {
const sourceNode = node.childForFieldName("source");
if (sourceNode) {
const importPath = nodeText(sourceNode, source).replace(/['"]/g, "");
dependencies.push({
kind: "import",
path: importPath,
metadata: {
clause: nodeText(node, source),
},
});
imports.push({
path: importPath,
clause: nodeText(node, source),
line: sourceNode.startPosition.row + 1,
column: sourceNode.startPosition.column + 1,
});
}
break;
}
case "call_expression": {
const funcNode = node.child(0);
if (funcNode && funcNode.type === "identifier" && nodeText(funcNode, source) === "require") {
const argsNode = node.child(1);
if (argsNode && argsNode.firstChild) {
const required = nodeText(argsNode.firstChild, source).replace(/['"]/g, "");
dependencies.push({
kind: "require",
path: required,
metadata: {
clause: nodeText(node, source),
},
});
imports.push({
path: required,
clause: nodeText(node, source),
line: node.startPosition.row + 1,
column: node.startPosition.column + 1,
});
}
}
break;
}
case "export_statement":
case "export_clause":
case "export_default_declaration": {
exports.push({
clause: nodeText(node, source),
line: node.startPosition.row + 1,
column: node.startPosition.column + 1,
});
break;
}
default:
break;
}
for (const child of node.children) {
visit(child);
}
};
visit(tree.rootNode);
return {
symbols,
dependencies,
references,
imports,
exports,
};
}
function extractPython(tree, source) {
const symbols = [];
const dependencies = [];
const references = [];
const imports = [];
const registerReference = (nameNode) => {
if (!nameNode) return;
const name = nodeText(nameNode, source);
if (!name) return;
references.push({
name,
line: nameNode.startPosition.row + 1,
column: nameNode.startPosition.column + 1,
snippet: nodeText(nameNode, source),
});
};
const visit = (node) => {
switch (node.type) {
case "function_definition": {
const nameNode = node.childForFieldName("name");
if (nameNode) {
symbols.push({
name: nodeText(nameNode, source),
kind: "function",
...positionOf(nameNode),
});
}
break;
}
case "class_definition": {
const nameNode = node.childForFieldName("name");
if (nameNode) {
symbols.push({
name: nodeText(nameNode, source),
kind: "class",
...positionOf(nameNode),
});
}
break;
}
case "import_statement": {
const moduleNode = node.childForFieldName("module");
if (moduleNode) {
dependencies.push({
kind: "import",
path: nodeText(moduleNode, source),
metadata: {
clause: nodeText(node, source),
},
});
}
break;
}
case "import_from_statement": {
const moduleNode = node.childForFieldName("module");
if (moduleNode) {
dependencies.push({
kind: "import_from",
path: nodeText(moduleNode, source),
metadata: {
clause: nodeText(node, source),
},
});
}
break;
}
case "call": // older parser versions
case "function_call": {
const nameNode =
node.childForFieldName("name") ??
node.namedChildren?.find((child) => child.type === "identifier") ??
node.child(0);
registerReference(nameNode);
break;
}
case "identifier": {
registerReference(node);
break;
}
default:
break;
}
for (const child of node.children) {
visit(child);
}
};
visit(tree.rootNode);
return {
symbols,
dependencies,
references,
imports,
exports: [],
};
}
function parseFile(relativePath, content, language) {
// Try tree-sitter first (faster, more accurate)
const parser = getParser(language);
if (parser) {
try {
const tree = parser.parse(content);
const langType = LANGUAGE_MAP[language]?.type;
if (langType === "javascript" || langType === "typescript") {
const analysis = extractJavaScript(tree, content);
return {
...analysis,
language: langType,
definitions: analysis.symbols,
parser: "tree-sitter",
};
}
if (langType === "python") {
const analysis = extractPython(tree, content);
return {
...analysis,
language: langType,
definitions: analysis.symbols,
parser: "tree-sitter",
};
}
} catch (err) {
logger.warn({ err, file: relativePath, language }, "Tree-sitter parse failed, trying babel fallback");
}
}
// Fallback to Babel parser for JS/TS (pure JS, no native modules)
const langType = LANGUAGE_MAP[language]?.type;
if (langType === "javascript" || langType === "typescript") {
try {
const babelParser = require("./babel-parser");
const result = babelParser.parseFile(relativePath, content, language);
if (result) {
logger.debug({ file: relativePath, parser: "babel" }, "Parsed with babel fallback");
return result;
}
} catch (err) {
logger.debug({ err: err.message }, "Babel parser fallback not available");
}
}
return null;
}
/**
* Get info about available parsers
*/
function getParserInfo() {
const treeSitter = isTreeSitterAvailable();
let babel = false;
try {
const babelParser = require("./babel-parser");
babel = babelParser.isBabelAvailable();
} catch {
babel = false;
}
return {
treeSitter,
babel,
jsTs: treeSitter || babel, // JS/TS parsing available
python: treeSitter, // Python only via tree-sitter
};
}
module.exports = {
parseFile,
LANGUAGE_MAP,
getParser,
isTreeSitterAvailable,
getParserInfo,
};