UNPKG

scanex

Version:

A powerful CLI tool that automatically discovers and bundles related source code into a single markdown file, perfect for sharing with LLMs like ChatGPT, Claude, or Cursor AI.

128 lines (105 loc) 3.93 kB
import Parser from 'tree-sitter'; import JavaScript from 'tree-sitter-javascript'; import TSGrammar from 'tree-sitter-typescript'; import { join } from 'node:path'; import { existsSync } from 'node:fs'; const parserJS = new Parser(); parserJS.setLanguage(JavaScript); const parserTSX = new Parser(); parserTSX.setLanguage(TSGrammar.tsx); const parserTS = new Parser(); parserTS.setLanguage(TSGrammar.typescript); const QUERY_JS = new Parser.Query(JavaScript, ` (import_statement source: (string) @dep) (call_expression function: (identifier) @fn arguments: (arguments (string) @dep) (#eq? @fn "require")) `); const QUERY_TS = new Parser.Query(TSGrammar.typescript, ` (import_statement source: (string) @dep) (call_expression function: (identifier) @fn arguments: (arguments (string) @dep) (#eq? @fn "require")) `); const QUERY_TSX = new Parser.Query(TSGrammar.tsx, ` (import_statement source: (string) @dep) (call_expression function: (identifier) @fn arguments: (arguments (string) @dep) (#eq? @fn "require")) `); export const name = 'javascript'; export const exts = ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx']; export function scan(src, { file }) { if (!src || src.trim() === '') return []; try { // Choose parser and query based on file extension let parser, query; if (file.endsWith('.tsx')) { parser = parserTSX; query = QUERY_TSX; } else if (file.endsWith('.ts')) { parser = parserTS; query = QUERY_TS; } else { parser = parserJS; query = QUERY_JS; } const tree = parser.parse(src); // Check if parsing was successful if (!tree || !tree.rootNode) { throw new Error('Failed to parse syntax tree'); } const imports = [...query.matches(tree.rootNode)] .map(m => m.captures.find(c => c.name === 'dep').node.text.replace(/['"]/g,'')); return imports; } catch (e) { // For TSX files, try falling back to JavaScript parser if (file.endsWith('.tsx') || file.endsWith('.ts')) { // Gracefully skip problematic files console.warn(`⚠️ Skipping ${file.split('/').pop()}: parsing failed. Error: ${e.message}`); return []; } else { console.warn(`⚠️ Skipping ${file.split('/').pop()}: ${e.message}`); } return []; } } export function resolve(spec, { projectRoot, aliasConfig, configBasePath, file }) { // Handle both string specs and object specs (from Ruby) const specValue = typeof spec === 'string' ? spec : spec.value; // Don't try to resolve Ruby constants as JavaScript aliases if (typeof spec === 'object' && spec.type === 'constant') { return null; } if (!aliasConfig?.compilerOptions?.paths) { return null; } const { baseUrl = ".", paths } = aliasConfig.compilerOptions; // Default baseUrl to "." // Use configBasePath (where tsconfig.json was found) as the base for resolution const resolveBasePath = configBasePath || projectRoot; for (const [alias, aliasPaths] of Object.entries(paths)) { const aliasPrefix = alias.slice(0, -2); // remove '/*' if (specValue.startsWith(aliasPrefix)) { const remainingPath = specValue.substring(aliasPrefix.length); for (const p of aliasPaths) { const base = join(resolveBasePath, baseUrl, p.slice(0, -2)); const resolvedPath = join(base, remainingPath); // Check for file with extensions for (const ext of exts) { const pathWithExt = `${resolvedPath}${ext}`; if (existsSync(pathWithExt)) { return pathWithExt; } } // Check for directory with index file for (const ext of exts) { const indexFile = join(resolvedPath, `index${ext}`); if (existsSync(indexFile)) { return indexFile; } } } } } return null; }