UNPKG

@franklevel/unused-deps-analyzer

Version:

Analyzes which dependencies in package.json are actually being used in the project

195 lines (179 loc) 6.5 kB
import { readFile } from 'fs/promises'; import path from 'path'; import glob from 'fast-glob'; import { parse } from '@babel/parser'; import traversePkg from '@babel/traverse'; const traverse = traversePkg.default; // Configure parser options based on file extension function getParserOptions(filePath) { const ext = path.extname(filePath); const baseOptions = { sourceType: 'module', plugins: [ 'decorators', 'classProperties', 'objectRestSpread', 'dynamicImport', 'optionalChaining', 'nullishCoalescing' ] }; switch (ext) { case '.tsx': return { ...baseOptions, plugins: [...baseOptions.plugins, 'typescript', 'jsx'] }; case '.ts': return { ...baseOptions, plugins: [...baseOptions.plugins, 'typescript'] }; case '.jsx': return { ...baseOptions, plugins: [...baseOptions.plugins, 'jsx'] }; default: return baseOptions; } } export async function analyze(projectPath, includeDevDependencies = false) { try { // Read package.json const packageJsonPath = path.join(projectPath, 'package.json'); const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')); // Get dependencies const dependencies = { ...packageJson.dependencies }; if (includeDevDependencies) { Object.assign(dependencies, packageJson.devDependencies); } // Get all JS files and config files const files = await glob('**/*.{js,jsx,ts,tsx,mjs,cjs,vue,svelte,config.js,config.ts,config.mjs,config.cjs}', { cwd: projectPath, ignore: ['node_modules/**', 'dist/**', 'build/**', 'coverage/**', '.git/**'] }); // Track used dependencies const usedDependencies = new Set(); const errors = []; // Create package details map const packageDetails = new Map(); for (const [name, version] of Object.entries(dependencies)) { try { const pkgPath = path.join(projectPath, 'node_modules', name, 'package.json'); const pkgJson = JSON.parse(await readFile(pkgPath, 'utf8')); const size = await getPackageSize(path.join(projectPath, 'node_modules', name)); packageDetails.set(name, { version: pkgJson.version || version, size: size }); } catch (error) { errors.push(`Failed to read package details for ${name}: ${error.message}`); } } // Analyze each file for (const file of files) { try { const filePath = path.join(projectPath, file); const content = await readFile(filePath, 'utf8'); const parserOptions = getParserOptions(filePath); const ast = parse(content, parserOptions); traverse(ast, { ImportDeclaration(path) { const importPath = path.node.source.value; if (!importPath.startsWith('.') && !importPath.startsWith('/')) { // Handle both direct package imports and submodule imports const pkgName = importPath.split('/')[0]; if (dependencies[pkgName]) { usedDependencies.add(pkgName); } } }, CallExpression(path) { // Handle require calls if (path.node.callee.name === 'require') { const arg = path.node.arguments[0]; if (arg && arg.type === 'StringLiteral') { const importPath = arg.value; if (!importPath.startsWith('.') && !importPath.startsWith('/')) { const pkgName = importPath.split('/')[0]; if (dependencies[pkgName]) { usedDependencies.add(pkgName); } } } } // Handle dynamic imports if (path.node.callee.type === 'Import') { const arg = path.node.arguments[0]; if (arg && arg.type === 'StringLiteral') { const importPath = arg.value; console.log('Found dynamic import:', importPath); if (!importPath.startsWith('.') && !importPath.startsWith('/')) { const pkgName = importPath.split('/')[0]; console.log('Package name extracted:', pkgName); console.log('Is in dependencies?', !!dependencies[pkgName]); if (dependencies[pkgName]) { usedDependencies.add(pkgName); } } } } }, // Handle template literal imports TaggedTemplateExpression(path) { if (path.node.tag.name === 'require' || path.node.tag.name === 'import') { const quasi = path.node.quasi; if (quasi.quasis.length === 1) { const importPath = quasi.quasis[0].value.raw; if (!importPath.startsWith('.') && !importPath.startsWith('/')) { const pkgName = importPath.split('/')[0]; if (dependencies[pkgName]) { usedDependencies.add(pkgName); } } } } } }); } catch (error) { errors.push(`Failed to analyze ${file}: ${error.message}`); console.error(`Error analyzing ${file}:`, error); } } // Get unused dependencies const unusedDependencies = Object.keys(dependencies).filter(dep => !usedDependencies.has(dep)); return { used: Array.from(usedDependencies), unused: unusedDependencies, packageDetails, errors, includesDevDependencies: includeDevDependencies }; } catch (error) { throw new Error(`Failed to analyze dependencies: ${error.message}`); } } async function getPackageSize(pkgPath) { try { const files = await glob('**/*', { cwd: pkgPath, onlyFiles: true, absolute: true }); let totalSize = 0; for (const file of files) { try { const stats = await readFile(file); if (stats && typeof stats.length === 'number' && !isNaN(stats.length)) { totalSize += stats.length; } } catch (error) { // Ignore errors for individual files } } return totalSize || 0; // Ensure we always return a valid number } catch (error) { return 0; // Return 0 if there's any error in the size calculation } }