Add ESLint config and unused-imports cleanup script

Recommend dbaeumer.vscode-eslint in .vscode/extensions.json
This commit is contained in:
LAPTOP-O016UC3M\Qi Chen
2025-12-11 16:01:25 +08:00
parent b8c221d112
commit 647b90db75
3 changed files with 242 additions and 0 deletions

5
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"recommendations": [
"dbaeumer.vscode-eslint"
]
}

119
eslint.config.js Normal file
View File

@@ -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',
},
},
];

View File

@@ -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));