Add ESLint config and unused-imports cleanup script
Recommend dbaeumer.vscode-eslint in .vscode/extensions.json
This commit is contained in:
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"dbaeumer.vscode-eslint"
|
||||||
|
]
|
||||||
|
}
|
||||||
119
eslint.config.js
Normal file
119
eslint.config.js
Normal 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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
118
scripts/remove-unused-imports.mjs
Normal file
118
scripts/remove-unused-imports.mjs
Normal 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));
|
||||||
Reference in New Issue
Block a user