UNPKG

frontend-standards-checker

Version:

A comprehensive frontend standards validation tool with TypeScript support

356 lines (355 loc) 13.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConfigLoaderHelper = void 0; exports.readFileContent = readFileContent; exports.extractImportPaths = extractImportPaths; exports.resolveImportPath = resolveImportPath; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const file_scanner_js_1 = require("../utils/file-scanner.js"); /** * Helper functions for ConfigLoader */ class ConfigLoaderHelper { 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 Promise.resolve(`${`${configPath}?t=${Date.now()}`}`).then(s => __importStar(require(s))); 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_1.default.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 (!(0, file_scanner_js_1.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_1.default.dirname(filePath); const fileName = path_1.default.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_1.default.basename(filePath)); } checkIndexImport(currentDir, fileName, importStatement) { try { const indexContent = this.findIndexFileContent(currentDir); if (!indexContent) return false; const importedSymbols = this.extractImportedSymbols(importStatement); const dirFiles = fs_1.default.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_1.default.join(dirPath, `index.${ext}`); if (fs_1.default.existsSync(testPath)) { return fs_1.default.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_1.default.dirname(filePath); for (const importPath of importPaths) { const resolvedImport = resolveImportPath(fileDir, importPath, extensions); if (resolvedImport) { dependencyGraph[filePath] ?? (dependencyGraph[filePath] = new Set()); dependencyGraph[filePath].add(resolvedImport); visit(resolvedImport); } } }; visit(filePath); } } exports.ConfigLoaderHelper = ConfigLoaderHelper; /** * Lee el contenido de un archivo si existe. */ function readFileContent(filePath) { try { return fs_1.default.readFileSync(filePath, 'utf8'); } catch { return null; } } /** * Extrae los paths de importación relativos del contenido de un archivo. */ 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. */ function resolveImportPath(baseDir, importPath, extensions) { let resolved = path_1.default.resolve(baseDir, importPath); for (const ext of extensions) { if (fs_1.default.existsSync(resolved + ext)) { return resolved + ext; } } if (fs_1.default.existsSync(resolved)) { return resolved; } return null; }