UNPKG

frontend-standards-checker

Version:

A comprehensive frontend standards validation tool with TypeScript support

314 lines 11.6 kB
import fs from 'fs'; import path from 'path'; import { isReactNativeProject } from '../utils/file-scanner.js'; /** * Helper functions for ConfigLoader */ export class ConfigLoaderHelper { logger; constructor(logger) { this.logger = logger; } /** * Try to load config file using both ESM and CommonJS methods */ async tryLoadConfig(configPath) { let importError = null; // Try ESM dynamic import first try { const configModule = await import(`${configPath}?t=${Date.now()}`); return configModule?.default ?? configModule; } catch (err) { importError = err; // Try CommonJS require as fallback try { // eslint-disable-next-line @typescript-eslint/no-var-requires const requiredConfig = require(configPath); return requiredConfig?.default ?? requiredConfig; } catch (requireErr) { const msg = `Failed to load config from ${configPath} with both import and require.\n` + `import error: ${importError instanceof Error ? importError.message : String(importError)}\n` + `require error: ${requireErr instanceof Error ? requireErr.message : String(requireErr)}`; this.logger.warn(msg); return undefined; } } } /** * Check if file is a config file */ isConfigFile(filePath) { const fileName = path.basename(filePath); // Common configuration file patterns const configPatterns = [ /\.config\.(js|ts|mjs|cjs|json)$/, /^(jest|vite|webpack|tailwind|next|eslint|prettier|babel|rollup|tsconfig)\.config\./, /^(vitest|nuxt|quasar)\.config\./, /^tsconfig.*\.json$/, /^\.eslintrc/, /^\.prettierrc/, /^babel\.config/, /^postcss\.config/, /^stylelint\.config/, /^cypress\.config/, /^playwright\.config/, /^storybook\.config/, /^metro\.config/, /^expo\.config/, ]; return configPatterns.some((pattern) => pattern.test(fileName)); } /** * Check for console.log statements and return their line numbers */ checkConsoleLogLines(content, filePath) { if (this.shouldSkipConsoleCheck(filePath)) { return []; } return this.getConsoleLogLineNumbers(content); } /** * Returns an array of line numbers (1-based) where console.log is used */ getConsoleLogLineNumbers(content) { const lines = content.split('\n'); let commentState = { inJSDoc: false, inMultiLineComment: false }; const violationLines = []; lines.forEach((line, idx) => { commentState = this.updateCommentState(line, commentState); if (this.isInComment(line, commentState)) { return; } if (this.hasConsoleStatement(line)) { violationLines.push(idx + 1); // 1-based line number } }); return violationLines; } shouldSkipConsoleCheck(filePath) { const isDebugTestFile = filePath.includes('debug') || filePath.includes('dev') || filePath.includes('__tests__'); // Skip check for debug/test files in all projects if (isDebugTestFile) { return true; } // No additional checks needed for non-React Native projects if (!isReactNativeProject(filePath)) { return false; } // For React Native, we already checked debug/test files above // So we just need to check if it's React Native to apply the rule return false; } // Removed unused hasConsoleInCode method updateCommentState(line, state) { // Handle JSDoc state if (/^\s*\/\*\*/.test(line)) return { ...state, inJSDoc: true }; if (state.inJSDoc && /\*\//.test(line)) return { ...state, inJSDoc: false }; // Handle multi-line comment state (non-JSDoc) if (/^\s*\/\*/.test(line) && !/^\s*\/\*\*/.test(line)) { return { ...state, inMultiLineComment: true }; } if (state.inMultiLineComment && /\*\//.test(line)) { return { ...state, inMultiLineComment: false }; } return state; } isInComment(line, state) { return (state.inJSDoc || state.inMultiLineComment || /^\s*\*/.test(line) || /^\s*\/\//.test(line)); } hasConsoleStatement(line) { // Only flag console.log for the 'No console.log' rule return /console\.log\s*\(/.test(line); } /** * Check for direct imports from sibling files */ checkDirectImports(content, filePath) { if (this.isIndexFile(filePath)) { return false; } const currentDir = path.dirname(filePath); const fileName = path.basename(filePath); const importRegex = /import\s+(?:(?:{[^}]*})|(?:[\w*]+))\s+from\s+['"`]([^'"`]+)['"`]/g; let match; let foundViolation = false; while ((match = importRegex.exec(content)) !== null && !foundViolation) { const importPath = match[1]; if (importPath === './' || importPath === '.') { foundViolation = this.checkIndexImport(currentDir, fileName, match[0]); } } return foundViolation; } isIndexFile(filePath) { const indexFilePatterns = [ 'index.ts', 'index.tsx', 'index.js', 'index.jsx', ]; return indexFilePatterns.includes(path.basename(filePath)); } checkIndexImport(currentDir, fileName, importStatement) { try { const indexContent = this.findIndexFileContent(currentDir); if (!indexContent) return false; const importedSymbols = this.extractImportedSymbols(importStatement); const dirFiles = fs.readdirSync(currentDir); return this.checkSymbolsAgainstExports(importedSymbols, dirFiles, fileName, indexContent, currentDir); } catch (error) { this.logger.warn(`Error checking index import in directory "${currentDir}": ${error instanceof Error ? error.message : String(error)}`); return false; } } findIndexFileContent(dirPath) { const extensions = ['ts', 'tsx', 'js', 'jsx']; for (const ext of extensions) { const testPath = path.join(dirPath, `index.${ext}`); if (fs.existsSync(testPath)) { return fs.readFileSync(testPath, 'utf8'); } } return null; } extractImportedSymbols(importStatement) { const importRegex = /import\s+(?:{([^}]*)}|(\w+))/; const symbolsMatch = importRegex.exec(importStatement); if (!symbolsMatch) return []; if (symbolsMatch[1]) { return symbolsMatch[1].split(',').map((s) => s.trim()); } return symbolsMatch[2] ? [symbolsMatch[2]] : []; } checkSymbolsAgainstExports(symbols, dirFiles, fileName, indexContent, _currentDir) { for (const dirFile of dirFiles) { if (this.shouldSkipFile(dirFile, fileName)) continue; const fileBase = this.getFileBaseName(dirFile); if (this.isSymbolExported(symbols, fileBase, indexContent, dirFile)) { return true; } } return false; } shouldSkipFile(dirFile, fileName) { return dirFile.startsWith('index.') || dirFile === fileName; } getFileBaseName(filePath) { return filePath.replace(/\.(ts|tsx|js|jsx)$/, ''); } isSymbolExported(symbols, fileBase, indexContent, dirFile) { const exportPatterns = this.getExportPatterns(fileBase, dirFile, indexContent); return symbols.some((symbol) => this.checkSymbolExport(symbol, fileBase, indexContent, exportPatterns)); } getExportPatterns(fileBase, _dirFile, indexContent) { const patterns = [ `export * from './${fileBase}'`, `export * from "./${fileBase}"`, `export { default } from './${fileBase}'`, `export { default } from "./${fileBase}"`, `export { default as ${fileBase} }`, `export * as ${fileBase}`, ]; const namedExports = []; const exportRegex = new RegExp(`export\\s+{([^}]*)}\\s+from\\s+['"]\\./${fileBase}['"]`, 'g'); let execResult; while ((execResult = exportRegex.exec(indexContent)) !== null) { namedExports.push(execResult[0]); } if (namedExports.length > 0) { patterns.push(...namedExports); } return patterns; } checkSymbolExport(symbol, fileBase, indexContent, exportPatterns) { // Check default exports if (symbol === fileBase || indexContent.includes(`export { default as ${symbol} } from './${fileBase}'`) || indexContent.includes(`export { default as ${symbol} } from "./${fileBase}"`)) { return true; } // Check named exports return exportPatterns.some((pattern) => pattern.includes(symbol)); } /** * Helper function for building dependency graph */ buildDependencyGraph(filePath, extensions, dependencyGraph) { const visitedFiles = new Set(); const visit = (filePath) => { if (visitedFiles.has(filePath)) return; visitedFiles.add(filePath); const content = readFileContent(filePath); if (!content) return; const importPaths = extractImportPaths(content); const fileDir = path.dirname(filePath); for (const importPath of importPaths) { const resolvedImport = resolveImportPath(fileDir, importPath, extensions); if (resolvedImport) { dependencyGraph[filePath] ??= new Set(); dependencyGraph[filePath].add(resolvedImport); visit(resolvedImport); } } }; visit(filePath); } } /** * Lee el contenido de un archivo si existe. */ export function readFileContent(filePath) { try { return fs.readFileSync(filePath, 'utf8'); } catch { return null; } } /** * Extrae los paths de importación relativos del contenido de un archivo. */ export function extractImportPaths(content) { const matches = content.match(/import.*from\s+['"]([^'"]+)['"]/g) || []; return matches .map((imp) => { const match = /from\s+['"]([^'"]+)['"]/.exec(imp); return match?.[1]; }) .filter((imp) => !!imp && (imp.startsWith('./') || imp.startsWith('../'))); } /** * Resuelve una ruta de import relativa a partir del directorio base y extensiones. */ export function resolveImportPath(baseDir, importPath, extensions) { let resolved = path.resolve(baseDir, importPath); for (const ext of extensions) { if (fs.existsSync(resolved + ext)) { return resolved + ext; } } if (fs.existsSync(resolved)) { return resolved; } return null; } //# sourceMappingURL=configLoader.helper.js.map