UNPKG

es-guard

Version:

A tool to check JavaScript compatibility with target environments

294 lines 12.8 kB
import { codeFrameColumns } from "@babel/code-frame"; import chalk from "chalk"; import { ESLint } from "eslint"; import * as fs from "fs"; import * as path from "path"; import { SourceMapConsumer } from "source-map"; import { createESLintConfig } from "./createESLintConfig.js"; async function getSourceMapForFile(jsFile) { // Try to find a .map file next to the JS file const mapFile = jsFile + ".map"; if (fs.existsSync(mapFile)) { const raw = fs.readFileSync(mapFile, "utf-8"); const map = JSON.parse(raw); return await new SourceMapConsumer(map); } // Try to find a sourceMappingURL comment in the JS file const content = fs.readFileSync(jsFile, "utf-8"); const match = content.match(/\/\/[#@] sourceMappingURL=([^\s]+)/); if (match) { let mapPath = match[1]; if (!mapPath.endsWith(".map")) return null; // If relative, resolve from jsFile if (!path.isAbsolute(mapPath)) { mapPath = path.resolve(path.dirname(jsFile), mapPath); } if (fs.existsSync(mapPath)) { const raw = fs.readFileSync(mapPath, "utf-8"); const map = JSON.parse(raw); return await new SourceMapConsumer(map); } } return null; } /** * Recursively follows source map chains to find the original source file. * This handles cases where there are multiple layers of source maps. */ async function getOriginalSourceMap(jsFile) { let currentFile = jsFile; let consumer = null; let originalFile; // Follow the source map chain while (true) { consumer = await getSourceMapForFile(currentFile); if (!consumer) break; // Get the first source file from this source map // Note: SourceMapConsumer doesn't expose sources directly, so we'll use a different approach // We'll try to get the original position for the first line to find the source const firstPosition = consumer.originalPositionFor({ line: 1, column: 0 }); if (firstPosition && firstPosition.source) { originalFile = firstPosition.source; // If this source file is a webpack:// URL, we've reached the end if (originalFile.startsWith("webpack://")) { break; } // If this source file exists and is a JS file, continue following the chain const resolvedPath = path.isAbsolute(originalFile) ? originalFile : path.resolve(path.dirname(currentFile), originalFile); if (fs.existsSync(resolvedPath) && resolvedPath.endsWith(".js")) { // Continue following the chain if (consumer.destroy) consumer.destroy(); currentFile = resolvedPath; continue; } } // If we can't find a valid source to follow, stop here break; } return consumer ? { consumer, originalFile } : null; } export const checkCompatibility = async (config) => { // Set BROWSERSLIST env variable to override Browserslist file detection const originalBrowserslistEnv = process.env.BROWSERSLIST; if (config.browsers) { process.env.BROWSERSLIST = config.browsers; } const eslint = new ESLint(createESLintConfig(config.target, config.browsers)); const errors = []; const warnings = []; try { // Let ESLint handle directory traversal directly const results = await eslint.lintFiles([config.dir]); if (results.length === 0) { console.log(`No JavaScript files found in directory: ${config.dir}`); return { errors: [], warnings: [] }; } for (const result of results) { const errorMessages = result.messages.filter((m) => m.severity === 2); const warningMessages = result.messages.filter((m) => m.severity === 1); // Try to remap error/warning locations using sourcemap let sourceMappedErrors = undefined; let sourceMappedWarnings = undefined; let sourceMap = null; let originalFile = undefined; if (errorMessages.length > 0 || warningMessages.length > 0) { const sourceMapResult = await getOriginalSourceMap(result.filePath); if (sourceMapResult) { sourceMap = sourceMapResult.consumer; originalFile = sourceMapResult.originalFile; } } if (sourceMap) { if (errorMessages.length > 0) { sourceMappedErrors = errorMessages.map((msg) => { if (msg.line != null && msg.column != null) { const orig = sourceMap.originalPositionFor({ line: msg.line, column: msg.column, }); return { ...msg, originalFile: orig.source || originalFile || undefined, originalLine: orig.line || undefined, originalColumn: orig.column || undefined, }; } return msg; }); } if (warningMessages.length > 0) { sourceMappedWarnings = warningMessages.map((msg) => { if (msg.line != null && msg.column != null) { const orig = sourceMap.originalPositionFor({ line: msg.line, column: msg.column, }); return { ...msg, originalFile: orig.source || originalFile || undefined, originalLine: orig.line || undefined, originalColumn: orig.column || undefined, }; } return msg; }); } if (sourceMap.destroy) sourceMap.destroy(); } if (errorMessages.length > 0) { errors.push({ file: result.filePath, messages: errorMessages, sourceMappedMessages: sourceMappedErrors, }); } if (warningMessages.length > 0) { warnings.push({ file: result.filePath, messages: warningMessages, sourceMappedMessages: sourceMappedWarnings, }); } } } catch (error) { console.warn(`Warning: Could not lint directory ${config.dir}:`, error); } finally { // Restore original BROWSERSLIST env variable if (originalBrowserslistEnv !== undefined) { process.env.BROWSERSLIST = originalBrowserslistEnv; } else { delete process.env.BROWSERSLIST; } } return { errors, warnings }; }; /** * Formats a violation message with source map information and creates a link to the actual source file. * @param message The lint message * @param sourceMappedMessage Optional source mapped message with original file information * @param baseDir Base directory for resolving relative paths * @param explicitFilePath Explicit file path for code frame * @returns Formatted message string */ export const formatViolationMessage = (message, sourceMappedMessage, baseDir = process.cwd(), explicitFilePath) => { const lineCol = chalk.gray(`${message.line}:${message.column}`); const ruleInfo = message.ruleId ? chalk.dim(` (${message.ruleId})`) : ""; let codeFrame = ""; let filePath; let line; let column; let canReadSource = false; // Prefer explicit file path if provided if (explicitFilePath) { filePath = explicitFilePath; line = message.line; column = message.column; canReadSource = fs.existsSync(filePath); } else if (sourceMappedMessage?.originalFile && sourceMappedMessage?.originalLine) { const originalFile = sourceMappedMessage.originalFile; line = sourceMappedMessage.originalLine; column = sourceMappedMessage.originalColumn || 0; // Handle webpack:// URLs - try to extract the actual file path if (originalFile.startsWith("webpack://")) { const match = originalFile.match(/webpack:\/\/[^/]*\/(.+)$/); if (match) { const extractedPath = match[1]; const possiblePaths = [ path.join(baseDir, extractedPath), path.join(baseDir, "src", extractedPath), path.join(baseDir, "app", extractedPath), path.join(baseDir, "pages", extractedPath), path.join(baseDir, "components", extractedPath), ]; for (const possiblePath of possiblePaths) { if (fs.existsSync(possiblePath)) { filePath = possiblePath; canReadSource = true; break; } } } if (!filePath) { filePath = undefined; canReadSource = false; } } else { filePath = originalFile; if (!path.isAbsolute(filePath)) { const cleanPath = filePath.replace(/^\.\//, ""); filePath = path.join(baseDir, cleanPath); } canReadSource = fs.existsSync(filePath); } } else if (message.line && message.column) { filePath = undefined; line = message.line; column = message.column; } // Try to print a code frame if we have a readable file path if (filePath && canReadSource && line) { try { const raw = fs.readFileSync(filePath, "utf-8"); const lines = raw.split(/\r?\n/); const isMinified = lines.length === 1 || (line <= lines.length && lines[line - 1] && lines[line - 1].length > 300); if (!isMinified) { codeFrame = "\n" + codeFrameColumns(raw, { start: { line, column: column || 1 } }, { highlightCode: true, linesAbove: 1, linesBelow: 1 }); } // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // ignore } } // If we have source-mapped info, show the original source if (sourceMappedMessage?.originalFile && sourceMappedMessage?.originalLine) { const originalFile = sourceMappedMessage.originalFile; const originalLine = sourceMappedMessage.originalLine; const originalCol = sourceMappedMessage.originalColumn || 0; let linkIndicator; let linkPath; if (originalFile.startsWith("webpack://")) { linkIndicator = canReadSource ? "[src]" : "[map]"; if (canReadSource && filePath) linkPath = filePath; } else if (canReadSource && filePath) { linkIndicator = "[src]"; linkPath = filePath; } else { linkIndicator = "[file]"; } // Only make the path clickable if it points to a real file let originalDisplay; if (linkPath && fs.existsSync(linkPath)) { const fileUri = `file://${linkPath.replace(/\\/g, "/")}`; originalDisplay = `${chalk.magenta(linkIndicator)} ${chalk.bold("Original:")} ${chalk.cyan(fileUri)}:${chalk.yellow(originalLine)}:${chalk.yellow(originalCol)}`; } else { originalDisplay = `${chalk.magenta(linkIndicator)} ${chalk.bold("Original:")} ${chalk.cyan(originalFile)}:${chalk.yellow(originalLine)}:${chalk.yellow(originalCol)}`; } const isError = message.severity === 2; const label = isError ? chalk.red.bold("ERROR") : chalk.yellow.bold("WARNING"); return `${lineCol} ${label}: ${chalk.bold(message.message)}${ruleInfo}\n ${originalDisplay}\n ${codeFrame}`; } // No source map info const isError = message.severity === 2; const label = isError ? chalk.red.bold("ERROR") : chalk.yellow.bold("WARNING"); return `${lineCol} ${label}: ${chalk.bold(message.message)}${ruleInfo}\n ${codeFrame}`; }; //# sourceMappingURL=checkCompatiblity.js.map