UNPKG

package-detector

Version:

A fast and comprehensive Node.js CLI tool to analyze your project's package.json and detect various package-related issues

297 lines (296 loc) 10.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.clearCaches = clearCaches; exports.readPackageJson = readPackageJson; exports.getAllDependencies = getAllDependencies; exports.findProjectFiles = findProjectFiles; exports.extractImports = extractImports; exports.executeNpmCommand = executeNpmCommand; exports.parseNpmOutdated = parseNpmOutdated; exports.parseNpmLs = parseNpmLs; exports.isPackageUsed = isPackageUsed; exports.batchCheckPackageUsage = batchCheckPackageUsage; exports.getPackageNameWithoutScope = getPackageNameWithoutScope; exports.isScopedPackage = isScopedPackage; const fs_1 = require("fs"); const path_1 = require("path"); const child_process_1 = require("child_process"); // Performance optimization: Cache for file content and imports const fileContentCache = new Map(); const importCache = new Map(); const packageUsageCache = new Map(); /** * Clear all caches (useful for testing or memory management) */ function clearCaches() { fileContentCache.clear(); importCache.clear(); packageUsageCache.clear(); } /** * Read and parse package.json file */ function readPackageJson() { const packagePath = (0, path_1.join)(process.cwd(), 'package.json'); if (!(0, fs_1.existsSync)(packagePath)) { throw new Error('package.json not found in current directory'); } try { const content = (0, fs_1.readFileSync)(packagePath, 'utf8'); return JSON.parse(content); } catch (error) { throw new Error(`Failed to parse package.json: ${error instanceof Error ? error.message : String(error)}`); } } /** * Get all dependencies from package.json (including dev, peer, optional) */ function getAllDependencies() { const pkg = readPackageJson(); const allDeps = {}; // Merge all dependency types if (pkg.dependencies) Object.assign(allDeps, pkg.dependencies); if (pkg.devDependencies) Object.assign(allDeps, pkg.devDependencies); if (pkg.peerDependencies) Object.assign(allDeps, pkg.peerDependencies); if (pkg.optionalDependencies) Object.assign(allDeps, pkg.optionalDependencies); return allDeps; } /** * Find all project files that might contain imports (optimized) */ function findProjectFiles(dir = process.cwd(), extensions = ['.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte'], excludeDirs = ['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.nuxt', '.cache']) { const files = []; const excludeSet = new Set(excludeDirs); // Use Set for O(1) lookup function scanDirectory(currentDir, depth = 0) { // Limit recursion depth to prevent stack overflow on deep directories if (depth > 20) return; try { const items = (0, fs_1.readdirSync)(currentDir); for (const item of items) { const fullPath = (0, path_1.join)(currentDir, item); try { const stat = (0, fs_1.statSync)(fullPath); if (stat.isDirectory()) { // Skip excluded directories if (!excludeSet.has(item)) { scanDirectory(fullPath, depth + 1); } } else if (stat.isFile()) { // Check if file has a relevant extension const ext = (0, path_1.extname)(item).toLowerCase(); if (extensions.includes(ext)) { files.push(fullPath); } } } catch (error) { // Skip files/directories we can't access continue; } } } catch (error) { // Skip directories we can't read console.warn(`Warning: Could not read directory ${currentDir}: ${error}`); } } scanDirectory(dir); return files; } /** * Extract import/require statements from a file (optimized with caching) */ function extractImports(filePath) { // Check cache first if (importCache.has(filePath)) { return importCache.get(filePath); } try { const content = (0, fs_1.readFileSync)(filePath, 'utf8'); const imports = []; // Optimized regex patterns - compile once and reuse const es6ImportPattern = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"`]([^'"`]+)['"`]/g; const commonjsPattern = /require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g; // Match ES6 imports let match; while ((match = es6ImportPattern.exec(content)) !== null) { imports.push(match[1]); } // Match CommonJS requires while ((match = commonjsPattern.exec(content)) !== null) { imports.push(match[1]); } // Cache the result importCache.set(filePath, imports); return imports; } catch (error) { console.warn(`Warning: Could not read file ${filePath}: ${error}`); importCache.set(filePath, []); // Cache empty result return []; } } /** * Execute npm command and return result */ function executeNpmCommand(command) { try { return (0, child_process_1.execSync)(command, { cwd: process.cwd(), encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }); } catch (error) { // npm outdated returns non-zero exit code when there are outdated packages // but still produces valid output in stdout if (error instanceof Error && 'stdout' in error && error.stdout) { return error.stdout; } if (error instanceof Error && 'stderr' in error) { throw new Error(`npm command failed: ${error.stderr}`); } throw new Error(`npm command failed: ${error instanceof Error ? error.message : String(error)}`); } } /** * Parse npm outdated output */ function parseNpmOutdated(output) { const lines = output.trim().split('\n'); const results = []; // Skip header line for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; // Parse line: package current wanted latest location const parts = line.split(/\s+/); if (parts.length >= 5) { results.push({ package: parts[0], current: parts[1], wanted: parts[2], latest: parts[3], location: parts[4] }); } } return results; } /** * Parse npm ls output (simplified) */ function parseNpmLs(output) { const lines = output.trim().split('\n'); const results = []; // Look for lines with package names and versions // Match any line that contains a package name followed by @ and version for (const line of lines) { const match = line.match(/([^@\s]+)@([^\s]+)/); if (match) { results.push({ name: match[1], version: match[2] }); } } return results; } /** * Check if a package is used in the project (optimized with caching and early termination) */ function isPackageUsed(packageName, projectFiles) { // Check cache first const cacheKey = `${packageName}:${projectFiles.length}`; if (packageUsageCache.has(cacheKey)) { return packageUsageCache.get(cacheKey); } // Pre-compile regex patterns for better performance const exactMatchPattern = new RegExp(`^${packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`); const prefixPattern = new RegExp(`^${packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/`); const scopedPattern = packageName.startsWith('@') ? new RegExp(`^${packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`) : null; for (const file of projectFiles) { const imports = extractImports(file); for (const imp of imports) { // Check exact match if (exactMatchPattern.test(imp)) { packageUsageCache.set(cacheKey, true); return true; } // Check if package is a prefix (for sub-modules) if (prefixPattern.test(imp)) { packageUsageCache.set(cacheKey, true); return true; } // Check scoped packages if (scopedPattern && scopedPattern.test(imp)) { packageUsageCache.set(cacheKey, true); return true; } } } packageUsageCache.set(cacheKey, false); return false; } /** * Batch check multiple packages for usage (optimized) */ function batchCheckPackageUsage(packageNames, projectFiles) { const results = {}; // Pre-extract all imports from all files once const allImports = new Set(); for (const file of projectFiles) { const imports = extractImports(file); imports.forEach(imp => allImports.add(imp)); } // Check each package against the pre-extracted imports for (const packageName of packageNames) { const cacheKey = `${packageName}:${projectFiles.length}`; if (packageUsageCache.has(cacheKey)) { results[packageName] = packageUsageCache.get(cacheKey); continue; } let isUsed = false; // Check exact match if (allImports.has(packageName)) { isUsed = true; } else { // Check prefix matches for (const imp of allImports) { if (imp.startsWith(packageName + '/')) { isUsed = true; break; } // Check scoped packages if (packageName.startsWith('@') && imp.startsWith(packageName)) { isUsed = true; break; } } } results[packageName] = isUsed; packageUsageCache.set(cacheKey, isUsed); } return results; } /** * Get package name without scope */ function getPackageNameWithoutScope(packageName) { return packageName.replace(/^@[^/]+\//, ''); } /** * Check if a package is a scoped package */ function isScopedPackage(packageName) { return packageName.startsWith('@'); }