baseline-lint
Version:
Check web features for Baseline compatibility
341 lines (288 loc) • 10.2 kB
JavaScript
/**
* Generate detailed PR comments for baseline-lint results
* This script parses the baseline-lint JSON output and creates
* actionable, detailed comments with file locations and fixes
*/
import { readFileSync } from 'fs';
/**
* Generate a fix suggestion based on the issue
*/
function generateFixSuggestion(issue, type) {
const suggestions = {
// CSS fix suggestions
'word-break': {
'auto-phrase': 'Use `word-break: break-word` for wider support',
},
'container-type': {
'inline-size': 'Add fallback: `width: 100%; container-type: inline-size;`',
},
'container-query-length': {
'*': 'Use media queries as fallback for older browsers',
},
'backdrop-filter': {
'*': 'Add `-webkit-backdrop-filter` prefix for Safari support',
},
'aspect-ratio': {
'*': 'Use padding-bottom hack: `padding-bottom: 56.25%; /* 16:9 */`',
},
// JavaScript fix suggestions
'Array.prototype.at': 'Use `arr[arr.length - 1]` instead of `arr.at(-1)`',
'Array.prototype.findLast': 'Use `[...arr].reverse().find()` for older browsers',
'Object.hasOwn': 'Use `Object.prototype.hasOwnProperty.call(obj, prop)`',
'String.prototype.replaceAll': 'Use `str.replace(/pattern/g, replacement)`',
'Promise.allSettled': 'Use Promise.all with .catch() for each promise',
'structuredClone': 'Use `JSON.parse(JSON.stringify())` or a deep clone library',
'AbortController': 'Add polyfill or use setTimeout/clearTimeout pattern',
'ResizeObserver': 'Add polyfill or use window resize events',
'IntersectionObserver': 'Add polyfill or use scroll event listeners',
};
if (type === 'css') {
const propertyKey = issue.property;
const valueKey = issue.value;
if (suggestions[propertyKey]?.[valueKey]) {
return suggestions[propertyKey][valueKey];
}
if (suggestions[propertyKey]?.['*']) {
return suggestions[propertyKey]['*'];
}
} else if (type === 'js') {
const apiKey = issue.api;
if (suggestions[apiKey]) {
return suggestions[apiKey];
}
}
// Generic suggestions based on baseline status
if (issue.baseline === false) {
return type === 'css'
? 'Consider using widely supported alternatives'
: 'Add polyfill or use alternative implementation';
} else if (issue.baseline === 'low') {
return 'Consider adding fallback for older browsers';
}
return 'Review browser support requirements';
}
/**
* Get browser support summary
*/
function getBrowserSupport(issue) {
if (!issue.support) return null;
const browserNames = {
chrome: 'Chrome',
firefox: 'Firefox',
safari: 'Safari',
edge: 'Edge'
};
const supported = [];
const unsupported = [];
Object.entries(issue.support).forEach(([browser, version]) => {
const name = browserNames[browser] || browser;
if (version && version !== false) {
supported.push(`${name} ${version}+`);
} else {
unsupported.push(name);
}
});
if (unsupported.length > 0) {
return `❌ Missing: ${unsupported.join(', ')}`;
} else if (supported.length > 0) {
return `✅ ${supported.join(', ')}`;
}
return null;
}
/**
* Get priority icon and level
*/
function getPriority(issue) {
if (issue.severity === 'error' || issue.baseline === false) {
return { icon: '🔴', level: 'Critical', description: 'Blocks major browsers' };
} else if (issue.severity === 'warning' || issue.baseline === 'low') {
return { icon: '🟡', level: 'Warning', description: 'Newly available feature' };
} else {
return { icon: '🔵', level: 'Info', description: 'Consider for optimization' };
}
}
/**
* Format a single issue for display
*/
function formatIssue(issue, file, type) {
const priority = getPriority(issue);
const feature = type === 'css'
? (issue.value ? `${issue.property}: ${issue.value}` : issue.property)
: issue.api;
const location = `**${file}:${issue.line || '?'}${issue.column ? `:${issue.column}` : ''}**`;
const fixSuggestion = generateFixSuggestion(issue, type);
const browserSupport = getBrowserSupport(issue);
let output = `${priority.icon} ${location}\n`;
output += `- \`${feature}\`\n`;
output += `- ${issue.message}\n`;
if (browserSupport) {
output += `- ${browserSupport}\n`;
}
output += `- **Fix:** ${fixSuggestion}\n`;
return output;
}
/**
* Generate the complete PR comment
*/
function generateComment(results, score, options = {}) {
const { previousScore = null, repoOwner = '', repoName = '' } = options;
// Calculate score change
let scoreChange = '';
if (previousScore !== null) {
const diff = score - previousScore;
if (diff > 0) scoreChange = ` (+${diff} from last PR)`;
else if (diff < 0) scoreChange = ` (${diff} from last PR)`;
}
// Count issues by severity
let totalIssues = 0;
let criticalIssues = 0;
let warningIssues = 0;
results.forEach(result => {
if (result.issues) {
result.issues.forEach(issue => {
totalIssues++;
const priority = getPriority(issue);
if (priority.level === 'Critical') criticalIssues++;
else if (priority.level === 'Warning') warningIssues++;
});
}
});
// Generate header
let comment = `## 🎯 Baseline Compatibility Report\n\n`;
comment += `**Score:** ${score}/100${scoreChange}\n\n`;
if (totalIssues === 0) {
comment += `🎉 **Perfect! No baseline compatibility issues found.**\n\n`;
comment += `All CSS and JavaScript features are widely supported across browsers.\n\n`;
return comment + generateFooter(repoOwner, repoName);
}
// Issue summary
comment += `### Issues Found (${totalIssues}):\n\n`;
// Group issues by file
const fileIssues = {};
results.forEach(result => {
if (result.issues && result.issues.length > 0) {
fileIssues[result.file] = {
issues: result.issues,
type: result.type
};
}
});
// Format each file's issues
Object.entries(fileIssues).forEach(([file, data]) => {
data.issues.forEach(issue => {
comment += formatIssue(issue, file, data.type) + '\n';
});
});
// Summary section
comment += `### Summary\n\n`;
if (criticalIssues > 0) {
comment += `- 🔴 **${criticalIssues} critical issue${criticalIssues !== 1 ? 's' : ''}** (limited browser support)\n`;
}
if (warningIssues > 0) {
comment += `- 🟡 **${warningIssues} warning${warningIssues !== 1 ? 's' : ''}** (newly available features)\n`;
}
// Recommendations
if (criticalIssues > 0) {
comment += `- **Recommendation:** Fix critical issues before merge\n`;
} else if (warningIssues > 0) {
comment += `- **Recommendation:** Consider fixes if targeting older browsers\n`;
}
comment += `\n`;
return comment + generateFooter(repoOwner, repoName);
}
/**
* Generate footer with useful links
*/
function generateFooter(repoOwner, repoName) {
let footer = `### Quick Actions\n\n`;
footer += `- 📊 [View detailed baseline guide](https://web.dev/baseline/)\n`;
footer += `- 🔧 Fix locally: \`npx baseline-lint check ./src --score\`\n`;
footer += `- 📚 [Learn about Baseline](https://github.com/web-platform-dx/web-features)\n`;
if (repoOwner && repoName) {
footer += `- 📋 [Workflow logs](https://github.com/${repoOwner}/${repoName}/actions)\n`;
}
footer += `\n---\n`;
footer += `*🤖 Generated by [baseline-lint](https://www.npmjs.com/package/baseline-lint)*`;
return footer;
}
/**
* Main function to process CLI arguments and generate comment
*/
function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error('Usage: generate-pr-comment.js <results-json-file> [options]');
console.error('Options:');
console.error(' --previous-score <score> Previous PR score for comparison');
console.error(' --repo-owner <owner> GitHub repository owner');
console.error(' --repo-name <name> GitHub repository name');
process.exit(1);
}
const resultsFile = args[0];
const options = {};
// Parse options
for (let i = 1; i < args.length; i += 2) {
const flag = args[i];
const value = args[i + 1];
switch (flag) {
case '--previous-score':
options.previousScore = parseInt(value);
break;
case '--repo-owner':
options.repoOwner = value;
break;
case '--repo-name':
options.repoName = value;
break;
}
}
try {
// Read and parse results
let resultsContent = readFileSync(resultsFile, 'utf-8');
// Clean up potential non-JSON content at the beginning
const jsonStart = resultsContent.indexOf('{');
if (jsonStart > 0) {
resultsContent = resultsContent.substring(jsonStart);
}
// Try to parse JSON
let data;
try {
data = JSON.parse(resultsContent);
} catch (parseError) {
// If JSON parsing fails, try to extract JSON from mixed content
const lines = resultsContent.split('\n');
let jsonLines = [];
let inJson = false;
for (const line of lines) {
if (line.trim().startsWith('{') || inJson) {
inJson = true;
jsonLines.push(line);
}
if (line.trim() === '}' && inJson) {
break;
}
}
if (jsonLines.length > 0) {
data = JSON.parse(jsonLines.join('\n'));
} else {
throw new Error(`Unable to parse JSON from results file: ${parseError.message}`);
}
}
if (!data.results || !Array.isArray(data.results)) {
throw new Error('Invalid results format: missing results array');
}
const score = data.score || 100;
const comment = generateComment(data.results, score, options);
console.log(comment);
} catch (error) {
console.error('Error generating PR comment:', error.message);
process.exit(1);
}
}
// Export functions for testing
export { generateComment, formatIssue, generateFixSuggestion, getBrowserSupport, getPriority };
// Run if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}