diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..b308e589 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint" + ] +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..02e59647 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,119 @@ +import js from '@eslint/js'; +import tsParser from '@typescript-eslint/parser'; +import tsPlugin from '@typescript-eslint/eslint-plugin'; +import unusedImports from 'eslint-plugin-unused-imports'; +import reactHooks from 'eslint-plugin-react-hooks'; + +export default [ + js.configs.recommended, + { + ignores: [ + 'node_modules/**', + 'dist/**', + 'electron/**', + 'scripts/**', + ], + }, + { + files: ['**/*.{ts,tsx}'], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + globals: { + // Browser globals + window: 'readonly', + document: 'readonly', + navigator: 'readonly', + console: 'readonly', + setTimeout: 'readonly', + clearTimeout: 'readonly', + setInterval: 'readonly', + clearInterval: 'readonly', + requestAnimationFrame: 'readonly', + cancelAnimationFrame: 'readonly', + fetch: 'readonly', + URL: 'readonly', + URLSearchParams: 'readonly', + FormData: 'readonly', + Blob: 'readonly', + File: 'readonly', + FileReader: 'readonly', + AbortController: 'readonly', + AbortSignal: 'readonly', + Event: 'readonly', + EventTarget: 'readonly', + CustomEvent: 'readonly', + MouseEvent: 'readonly', + KeyboardEvent: 'readonly', + DragEvent: 'readonly', + ClipboardEvent: 'readonly', + HTMLElement: 'readonly', + HTMLInputElement: 'readonly', + HTMLTextAreaElement: 'readonly', + HTMLButtonElement: 'readonly', + HTMLDivElement: 'readonly', + HTMLFormElement: 'readonly', + Element: 'readonly', + Node: 'readonly', + NodeList: 'readonly', + MutationObserver: 'readonly', + ResizeObserver: 'readonly', + IntersectionObserver: 'readonly', + localStorage: 'readonly', + sessionStorage: 'readonly', + crypto: 'readonly', + performance: 'readonly', + confirm: 'readonly', + alert: 'readonly', + prompt: 'readonly', + getComputedStyle: 'readonly', + atob: 'readonly', + btoa: 'readonly', + // Node.js globals (for Electron) + process: 'readonly', + global: 'readonly', + Buffer: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + module: 'readonly', + require: 'readonly', + exports: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': tsPlugin, + 'unused-imports': unusedImports, + 'react-hooks': reactHooks, + }, + rules: { + // Disable base rules that conflict with TypeScript + 'no-unused-vars': 'off', + 'no-undef': 'off', // TypeScript handles this + + // TypeScript unused vars (disabled in favor of unused-imports) + '@typescript-eslint/no-unused-vars': 'off', + + // Unused imports plugin - this is the main feature you want + 'unused-imports/no-unused-imports': 'warn', + 'unused-imports/no-unused-vars': [ + 'warn', + { + vars: 'all', + varsIgnorePattern: '^_', + args: 'after-used', + argsIgnorePattern: '^_', + }, + ], + + // React Hooks rules + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + }, + }, +]; diff --git a/scripts/remove-unused-imports.mjs b/scripts/remove-unused-imports.mjs new file mode 100644 index 00000000..498bb1fa --- /dev/null +++ b/scripts/remove-unused-imports.mjs @@ -0,0 +1,118 @@ +import ts from 'typescript'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const rootDir = path.resolve(__dirname, '..'); + +// Find all TSX files recursively +function findTsxFiles(dir, files = []) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== 'dist') { + findTsxFiles(fullPath, files); + } else if (entry.isFile() && (entry.name.endsWith('.tsx') || entry.name.endsWith('.ts'))) { + files.push(fullPath); + } + } + return files; +} + +// Organize imports using TypeScript Language Service +function organizeImports(filePath) { + const fileContent = fs.readFileSync(filePath, 'utf-8'); + + // Create a simple in-memory host + const servicesHost = { + getScriptFileNames: () => [filePath], + getScriptVersion: () => '1', + getScriptSnapshot: (fileName) => { + if (fileName === filePath) { + return ts.ScriptSnapshot.fromString(fileContent); + } + if (!fs.existsSync(fileName)) { + return undefined; + } + return ts.ScriptSnapshot.fromString(fs.readFileSync(fileName, 'utf-8')); + }, + getCurrentDirectory: () => rootDir, + getCompilationSettings: () => ({ + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.ESNext, + jsx: ts.JsxEmit.ReactJSX, + moduleResolution: ts.ModuleResolutionKind.Bundler, + allowJs: true, + skipLibCheck: true, + noEmit: true, + }), + getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options), + fileExists: ts.sys.fileExists, + readFile: ts.sys.readFile, + readDirectory: ts.sys.readDirectory, + directoryExists: ts.sys.directoryExists, + getDirectories: ts.sys.getDirectories, + }; + + const services = ts.createLanguageService(servicesHost, ts.createDocumentRegistry()); + + // Get organize imports edits + const edits = services.organizeImports( + { type: 'file', fileName: filePath }, + {}, + {} + ); + + if (edits.length === 0) { + return { changed: false, content: fileContent }; + } + + // Apply edits + let newContent = fileContent; + // Apply edits in reverse order to preserve positions + for (const fileEdit of edits) { + const textChanges = [...fileEdit.textChanges].sort((a, b) => b.span.start - a.span.start); + for (const change of textChanges) { + newContent = + newContent.slice(0, change.span.start) + + change.newText + + newContent.slice(change.span.start + change.span.length); + } + } + + return { changed: newContent !== fileContent, content: newContent }; +} + +// Main +console.log('🔍 Searching for TSX/TS files...\n'); +const files = findTsxFiles(rootDir); +console.log(`Found ${files.length} files to process.\n`); + +let changedCount = 0; +let errorCount = 0; + +for (const file of files) { + const relativePath = path.relative(rootDir, file); + try { + const result = organizeImports(file); + if (result.changed) { + fs.writeFileSync(file, result.content, 'utf-8'); + console.log(`✅ Fixed: ${relativePath}`); + changedCount++; + } else { + console.log(`⏭️ Skipped (no changes): ${relativePath}`); + } + } catch (error) { + console.error(`❌ Error processing ${relativePath}: ${error.message}`); + errorCount++; + } +} + +console.log('\n' + '='.repeat(50)); +console.log(`📊 Summary:`); +console.log(` Files processed: ${files.length}`); +console.log(` Files modified: ${changedCount}`); +console.log(` Errors: ${errorCount}`); +console.log('='.repeat(50));