project-indexer
Version:
Local project indexer with browser trigger support
442 lines (407 loc) • 10.6 kB
JavaScript
import fs from 'fs';
import {globby} from 'globby';
import ignore from 'ignore'; // For parsing .gitignore files
import path from 'path';
// Default file extensions that are considered important project files
export const DEFAULT_EXTENSIONS = [
// Web development
'.js', '.ts', '.tsx', '.jsx', '.html', '.css', '.scss', '.less', '.sass',
// Backend development
'.py', '.java', '.rb', '.php', '.go', '.rs', '.cs', '.cpp', '.c', '.h', '.hpp',
// Data and configuration
'.json', '.xml', '.yml', '.yaml', '.toml', '.ini', '.env.example', '.template',
// Documentation
'.md', '.txt', '.rst', '.adoc',
// Templates
'.ejs', '.hbs', '.pug', '.mustache', '.twig',
// Shell scripts
'.sh', '.bash', '.zsh', '.fish', '.bat', '.cmd', '.ps1',
// Project-specific files
'Dockerfile', 'docker-compose.yml', 'Makefile', 'Jenkinsfile', 'Procfile'
];
// Universal ignore patterns for any project
export const UNIVERSAL_IGNORE_PATTERNS = [
// Dependency directories
'node_modules/**',
'bower_components/**',
'jspm_packages/**',
'package-lock.json',
'yarn.lock',
'pnpm-lock.yaml',
'.pnpm-store/**',
'vendor/**',
'.bundle/**',
'composer.lock',
'Gemfile.lock',
'requirements.txt.lock',
'poetry.lock',
'Pipfile.lock',
'cargo.lock',
// Build outputs and cache
'build/**',
'dist/**',
'out/**',
'output/**',
'target/**',
'bin/**',
'obj/**',
'lib/**',
'libs/**',
'debug/**',
'release/**',
'coverage/**',
'public/build/**',
'.svelte-kit/**',
'.next/**',
'.nuxt/**',
'.cache/**',
'.parcel-cache/**',
'.webpack/**',
'.serverless/**',
'.gradle/**',
'.eggs/**',
'__pycache__/**',
'*.py[cod]',
'*$py.class',
'*.so',
'*.dylib',
'*.dll',
'*.exe',
'*.out',
'*.app',
'*.jar',
'*.war',
'*.nar',
'*.ear',
'*.zip',
'*.tar.gz',
'*.rar',
'.vscode',
// IDE and editor configurations
'.vscode/**',
'.idea/**',
'.eclipse/**',
'.settings/**',
'.project',
'.classpath',
'.factorypath',
'*.sublime-*',
'.atom/**',
'.nova/**',
'.vs/**',
'.editorconfig',
'.prettierrc*',
'.eslintrc*',
'.stylelintrc*',
'.jshintrc',
'*.code-workspace',
'*.swp',
'*.swo',
'*~',
'.history/**',
// OS and environment files
'.DS_Store',
'.AppleDouble',
'.LSOverride',
'Icon\r',
'Thumbs.db',
'ehthumbs.db',
'Desktop.ini',
'*.lnk',
'*.url',
'.env',
'.env.*',
'!.env.example',
'!.env.template',
'*.log',
'*.tmp',
'*.temp',
'*.pid',
'*.seed',
'*.bak',
'*.swp',
'*.dump',
'npm-debug.log*',
'yarn-debug.log*',
'yarn-error.log*',
'lerna-debug.log*',
'.pnpm-debug.log*',
// Version control
'.git/**',
'.gitattributes',
'.gitmodules',
'.svn/**',
'.hg/**',
'.bzr/**',
'CVS/**',
// CI/CD and deployment
'.circleci/**',
'.github/**',
'.gitlab/**',
'.gitlab-ci.yml',
'.travis.yml',
'appveyor.yml',
'.azure-pipelines/**',
'.jenkins/**',
'Jenkinsfile',
'.wercker/**',
'.semaphore/**',
'.drone.yml',
'.codeship/**',
// Docker
'.docker/**',
'docker-compose.override.yml',
// Testing
'coverage/**',
'.nyc_output/**',
'.cypress/**',
'.jest-cache/**',
// Framework-specific
'.angular/**',
'.turbo/**',
'.meteor/**',
// TypeScript
'*.tsbuildinfo',
// Misc
'*.iml',
'.sass-cache/**',
'.terraform/**',
'*.tfstate',
'*.tfstate.*',
'.vagrant/**',
];
/**
* Attempts to read and parse all .gitignore files in the project
* @param {string} baseDir - The base directory of the project
* @returns {string[]} - Array of gitignore patterns
*/
export async function getGitignorePatterns(baseDir) {
try {
// Find all .gitignore files in the project
const gitignoreFiles = await globby(['**/.gitignore'], {
cwd: baseDir,
absolute: true,
ignore: ['**/node_modules/**'],
dot: true,
});
let allPatterns = [];
// Read each .gitignore file and collect patterns
for (const gitignoreFile of gitignoreFiles) {
try {
const content = fs.readFileSync(gitignoreFile, 'utf8');
// Get directory containing this .gitignore
const gitignoreDir = path.dirname(gitignoreFile);
const relativeDir = path.relative(baseDir, gitignoreDir);
// Process each line in the .gitignore file
const patterns = content
.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'))
.map(pattern => {
// Make patterns relative to the project root
if (relativeDir && pattern.startsWith('/')) {
return path.join(relativeDir, pattern.substring(1)).replace(/\\/g, '/');
} else if (relativeDir && !pattern.startsWith('!')) {
return path.join(relativeDir, pattern).replace(/\\/g, '/');
}
return pattern;
});
allPatterns = [...allPatterns, ...patterns];
} catch (err) {
console.warn(`Error reading gitignore file ${gitignoreFile}: ${err.message}`);
}
}
return allPatterns;
} catch (err) {
console.warn(`Could not process gitignore files: ${err.message}`);
return [];
}
}
/**
* Get project-type-specific ignore patterns based on detected project files
* @param {string} baseDir - The base directory of the project
* @returns {string[]} - Additional ignore patterns specific to the project type
*/
export async function getProjectSpecificIgnores(baseDir) {
const projectFiles = await globby([
'package.json',
'pom.xml',
'build.gradle',
'requirements.txt',
'Gemfile',
'Cargo.toml',
'go.mod',
'composer.json',
'project.clj',
'build.sbt',
'mix.exs',
'rebar.config',
'pubspec.yaml',
'stackage.yaml',
'Pipfile',
'pyproject.toml',
], {
cwd: baseDir,
deep: 1, // Only look at the root level
absolute: false, // We just need the names
});
const additionalIgnores = [];
// Node.js/JavaScript projects
if (projectFiles.includes('package.json')) {
additionalIgnores.push(
'coverage/**',
'.nyc_output/**',
'.storybook-out/**',
'.tern-port',
'.storybook-static/**',
'storybook-static/**'
);
}
// Java projects
if (projectFiles.includes('pom.xml') || projectFiles.includes('build.gradle')) {
additionalIgnores.push(
'target/**',
'build/**',
'.gradle/**',
'out/**',
'*.class',
'*.jar',
'gradle-app.setting',
'.gradletasknamecache',
'*.war',
'*.ear',
'*.hprof',
'.jrebel',
'rebel.xml',
'.apt_generated/**',
'.apt_generated_tests/**',
'.settings/**',
'.metadata/**'
);
}
// Python projects
if (projectFiles.includes('requirements.txt') ||
projectFiles.includes('Pipfile') ||
projectFiles.includes('pyproject.toml')) {
additionalIgnores.push(
'__pycache__/**',
'*.py[cod]',
'*$py.class',
'*.so',
'.Python',
'env/**',
'venv/**',
'ENV/**',
'env.bak/**',
'venv.bak/**',
'.pytest_cache/**',
'.coverage',
'htmlcov/**',
'.tox/**',
'.nox/**',
'.hypothesis/**',
'.pytype/**',
'cython_debug/**',
'*.egg-info/**',
'.installed.cfg',
'*.egg',
'celerybeat-schedule',
'celerybeat.pid',
'*.sage.py',
'.ipynb_checkpoints/**'
);
}
// Ruby projects
if (projectFiles.includes('Gemfile')) {
additionalIgnores.push(
'*.gem',
'*.rbc',
'/.config',
'/coverage/',
'/InstalledFiles',
'/pkg/',
'/spec/reports/',
'/spec/examples.txt',
'/test/tmp/',
'/test/version_tmp/',
'/tmp/',
'.byebug_history',
'.dat*',
'.repl_history',
'*.bridgesupport',
'build-iPhoneOS/',
'build-iPhoneSimulator/'
);
}
// Rust projects
if (projectFiles.includes('Cargo.toml')) {
additionalIgnores.push(
'/target/',
'**/*.rs.bk',
'Cargo.lock'
);
}
// Go projects
if (projectFiles.includes('go.mod')) {
additionalIgnores.push(
'*.test',
'*.out',
'/vendor/',
'/Godeps/'
);
}
// PHP projects
if (projectFiles.includes('composer.json')) {
additionalIgnores.push(
'/vendor/',
'composer.phar',
'/phpunit.xml',
'/.phpunit.result.cache'
);
}
return additionalIgnores;
}
/**
* Gets files to be indexed based on provided extensions and ignore patterns
* @param {string} baseDir - The base directory of the project
* @param {string[]} extensions - Array of file extensions to include
* @returns {Promise<string[]>} - Array of file paths
*/
export async function getFocusedFiles(baseDir, extensions) {
// Prepare include patterns from extensions
const includePatterns = extensions.map(ext => {
// Handle extensions that start with dot and those that don't
return ext.startsWith('.') ? `**/*${ext}` : `**/*.${ext}`;
});
// Add exact-match filenames without extensions that we want to include
includePatterns.push(
'**/Dockerfile',
'**/docker-compose.yml',
'**/Makefile',
'**/LICENSE',
'**/README*',
'**/CONTRIBUTING*',
'**/Jenkinsfile'
);
// Collect all ignore patterns
const gitignorePatterns = await getGitignorePatterns(baseDir);
const projectSpecificIgnores = await getProjectSpecificIgnores(baseDir);
const allIgnorePatterns = [
...UNIVERSAL_IGNORE_PATTERNS,
...gitignorePatterns,
...projectSpecificIgnores
];
// Use the ignore package to create a proper ignore filter
const ig = ignore().add(allIgnorePatterns);
// First, get all files that match the include patterns
const allMatchingFiles = await globby(includePatterns, {
cwd: baseDir,
absolute: true,
dot: true, // Include dotfiles
});
// Then filter out ignored files
return allMatchingFiles.filter(absPath => {
const relativePath = path.relative(baseDir, absPath).replace(/\\/g, '/');
return !ig.ignores(relativePath);
});
}