Created
November 27, 2025 22:24
-
-
Save davidcostadev/df331fc4865f75e2347e8a2bf4d391e2 to your computer and use it in GitHub Desktop.
find-isolated-files.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| /** | |
| * Script to find isolated files in src directory | |
| * A file is considered isolated if it exports something but those exports are never imported | |
| */ | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const SRC_DIR = path.join(__dirname, '..', 'src'); | |
| // Get all TypeScript files in src | |
| function getAllTsFiles(dir, fileList = []) { | |
| const files = fs.readdirSync(dir); | |
| files.forEach((file) => { | |
| const filePath = path.join(dir, file); | |
| const stat = fs.statSync(filePath); | |
| if (stat.isDirectory()) { | |
| getAllTsFiles(filePath, fileList); | |
| } else if (file.endsWith('.ts') && !file.endsWith('.spec.ts') && !file.endsWith('.test.ts')) { | |
| fileList.push(filePath); | |
| } | |
| }); | |
| return fileList; | |
| } | |
| // Extract exports from a file | |
| function extractExports(filePath) { | |
| const content = fs.readFileSync(filePath, 'utf-8'); | |
| const exports = new Set(); | |
| // Match export statements | |
| const exportPatterns = [ | |
| // export class X | |
| /export\s+(?:default\s+)?class\s+(\w+)/g, | |
| // export interface X | |
| /export\s+(?:default\s+)?interface\s+(\w+)/g, | |
| // export type X | |
| /export\s+(?:default\s+)?type\s+(\w+)/g, | |
| // export enum X | |
| /export\s+(?:default\s+)?enum\s+(\w+)/g, | |
| // export const X | |
| /export\s+(?:default\s+)?const\s+(\w+)/g, | |
| // export function X | |
| /export\s+(?:default\s+)?function\s+(\w+)/g, | |
| // export { X, Y, Z } | |
| /export\s*\{[^}]*\b(\w+)\b[^}]*\}/g, | |
| // export { X as Y } | |
| /export\s*\{[^}]*\b(\w+)\s+as\s+\w+\b[^}]*\}/g, | |
| ]; | |
| exportPatterns.forEach((pattern) => { | |
| let match; | |
| while ((match = pattern.exec(content)) !== null) { | |
| if (match[1]) { | |
| exports.add(match[1]); | |
| } | |
| } | |
| }); | |
| // Check for default exports | |
| if (/export\s+default/.test(content)) { | |
| exports.add('default'); | |
| } | |
| return Array.from(exports); | |
| } | |
| // Get possible import paths for a file | |
| function getPossibleImportPaths(filePath) { | |
| const relativePath = path.relative(SRC_DIR, filePath); | |
| const paths = new Set(); | |
| // Remove .ts extension | |
| const basePath = relativePath.replace(/\.ts$/, ''); | |
| // Add various possible import formats | |
| paths.add(basePath.replace(/\\/g, '/')); | |
| paths.add(`src/${basePath.replace(/\\/g, '/')}`); | |
| paths.add(`./${basePath.replace(/\\/g, '/')}`); | |
| paths.add(`../${basePath.replace(/\\/g, '/')}`); | |
| // Add path without src prefix | |
| const withoutSrc = basePath.replace(/^src[\/\\]/, '').replace(/\\/g, '/'); | |
| paths.add(withoutSrc); | |
| paths.add(`./${withoutSrc}`); | |
| // Add just the filename | |
| const fileName = path.basename(filePath, '.ts'); | |
| paths.add(fileName); | |
| // Add directory/filename | |
| const dirName = path.basename(path.dirname(filePath)); | |
| paths.add(`${dirName}/${fileName}`); | |
| return Array.from(paths); | |
| } | |
| // Read all file contents for searching | |
| function getAllFileContents() { | |
| const files = getAllTsFiles(SRC_DIR); | |
| const contents = new Map(); | |
| files.forEach((filePath) => { | |
| try { | |
| contents.set(filePath, fs.readFileSync(filePath, 'utf-8')); | |
| } catch (error) { | |
| // Skip files that can't be read | |
| } | |
| }); | |
| return contents; | |
| } | |
| // Check if exports are used anywhere | |
| function isExportedUsed(filePath, exports, allContents) { | |
| if (exports.length === 0) { | |
| return false; // No exports, not isolated | |
| } | |
| const possibleImportPaths = getPossibleImportPaths(filePath); | |
| const fileName = path.basename(filePath, '.ts'); | |
| // Search through all files for imports | |
| for (const [otherFilePath, content] of allContents.entries()) { | |
| // Skip the file itself | |
| if (otherFilePath === filePath) { | |
| continue; | |
| } | |
| // Check for imports from this file using various path formats | |
| for (const importPath of possibleImportPaths) { | |
| // Escape special regex characters | |
| const escapedPath = importPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| // Match: from 'path' or from "path" | |
| const importPattern = new RegExp(`from\\s+['"]${escapedPath}['"]`, 'g'); | |
| if (importPattern.test(content)) { | |
| return true; | |
| } | |
| // Match: require('path') | |
| const requirePattern = new RegExp(`require\\(['"]${escapedPath}['"]\\)`, 'g'); | |
| if (requirePattern.test(content)) { | |
| return true; | |
| } | |
| } | |
| // Check for specific export names being imported | |
| for (const exportName of exports) { | |
| if (exportName === 'default') { | |
| // Check for default imports | |
| const defaultImportPattern = new RegExp( | |
| `import\\s+\\w+\\s+from\\s+['"][^'"]*${fileName}['"]|import\\s+\\{[^}]*\\}\\s+from\\s+['"][^'"]*${fileName}['"]`, | |
| 'g', | |
| ); | |
| if (defaultImportPattern.test(content)) { | |
| return true; | |
| } | |
| } else { | |
| // Check for named imports | |
| const namedImportPattern = new RegExp( | |
| `import\\s+.*\\b${exportName}\\b.*from|import\\s+\\{[^}]*\\b${exportName}\\b[^}]*\\}`, | |
| 'g', | |
| ); | |
| if (namedImportPattern.test(content)) { | |
| // Verify it's actually importing from this file | |
| const lines = content.split('\n'); | |
| for (let i = 0; i < lines.length; i++) { | |
| if (lines[i].includes(exportName)) { | |
| // Check if this line or nearby lines reference our file | |
| const checkRange = lines.slice(Math.max(0, i - 2), Math.min(lines.length, i + 3)).join('\n'); | |
| for (const importPath of possibleImportPaths) { | |
| const escapedPath = importPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| if (new RegExp(escapedPath).test(checkRange)) { | |
| return true; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| // Main function | |
| function findIsolatedFiles() { | |
| console.log('π Scanning for isolated files in src...\n'); | |
| const allFiles = getAllTsFiles(SRC_DIR); | |
| const allContents = getAllFileContents(); | |
| const isolatedFiles = []; | |
| console.log(`π Found ${allFiles.length} TypeScript files to analyze...\n`); | |
| for (const filePath of allFiles) { | |
| const exports = extractExports(filePath); | |
| const isUsed = isExportedUsed(filePath, exports, allContents); | |
| if (exports.length > 0 && !isUsed) { | |
| const relativePath = path.relative(SRC_DIR, filePath); | |
| isolatedFiles.push({ | |
| path: relativePath, | |
| exports: exports, | |
| }); | |
| } | |
| } | |
| if (isolatedFiles.length === 0) { | |
| console.log('β No isolated files found! All exported files are being used.\n'); | |
| return; | |
| } | |
| console.log(`β οΈ Found ${isolatedFiles.length} isolated file(s):\n`); | |
| isolatedFiles.forEach((file) => { | |
| console.log(`π ${file.path}`); | |
| console.log(` Exports: ${file.exports.join(', ') || 'default'}\n`); | |
| }); | |
| console.log('\nπ‘ Tip: These files export something but are never imported.'); | |
| console.log(' Review them to see if they should be removed or if they need to be used.\n'); | |
| } | |
| // Run the script | |
| findIsolatedFiles(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment