Skip to content

Commit ad76de8

Browse files
Merge pull request #71 from mindfiredigital/dev
Release new version
2 parents 44605d0 + 59a38cd commit ad76de8

18 files changed

+1901
-1
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ This plugin provides the following rules:
122122
| `limit-reference-depth` | Restricts the depth of chained property access and enforces optional chaining to prevent runtime errors, improve null safety, and encourage safer access patterns in deeply nested data structures. |
123123
| `keep-functions-concise` | Enforces a maximum number of lines per function, with options to skip blank lines and comments, to promote readability, maintainability, and concise logic blocks. |
124124

125+
### Express Rules
126+
127+
| Rule Name | Description |
128+
| ------------------ | ------------------------------------------------------------------------------ |
129+
| `verb-consistency` | Enforces standard REST verbs (GET, POST, PUT, DELETE, PATCH) in Express routes |
130+
125131
## Usage
126132

127133
You can enable the plugin and configure the rules using either flat or legacy configurations.

bin/report-generation-cli.js

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
#!/usr/bin/env node
2+
3+
const { exec } = require('child_process');
4+
const { Parser } = require('json2csv');
5+
const pdfDocument = require('pdfkit');
6+
const fs = require('fs');
7+
const path = require('path');
8+
9+
const reportDirName = 'eslint-reports';
10+
11+
async function main() {
12+
console.log('🚀 [main] Starting Universal ESLint report generation...');
13+
14+
const targetPath = process.argv[2] || process.cwd();
15+
const resolvedTargetPath = path.resolve(targetPath);
16+
const reportDir = path.join(resolvedTargetPath, reportDirName);
17+
18+
console.log(`🔍 [main] Analyzing path: ${resolvedTargetPath}`);
19+
20+
try {
21+
if (fs.existsSync(reportDir)) {
22+
console.log(`🧹 [main] Cleaning old report directory...`);
23+
fs.rmSync(reportDir, { recursive: true, force: true });
24+
}
25+
fs.mkdirSync(reportDir, { recursive: true });
26+
console.log(`✅ [main] Report directory created at: ${reportDir}`);
27+
} catch (error) {
28+
console.error(
29+
`❌ [main] Failed to prepare report directory: ${reportDir}`,
30+
error
31+
);
32+
process.exit(1);
33+
}
34+
35+
try {
36+
const results = await runEslintInProject(resolvedTargetPath);
37+
const issues = results.flatMap(result =>
38+
result.messages.map(msg => ({
39+
filePath: path.relative(resolvedTargetPath, result.filePath),
40+
line: msg.line || 0,
41+
column: msg.column || 0,
42+
ruleId: msg.ruleId || 'Fatal',
43+
severity: msg.severity === 2 ? 'Error' : 'Warning',
44+
message: msg.message,
45+
}))
46+
);
47+
48+
if (issues.length === 0) {
49+
console.log(
50+
'🎉 [main] No linting issues found. Generating empty reports.'
51+
);
52+
await createCsvReport([], reportDir);
53+
await createPdfReport([], reportDir);
54+
return;
55+
}
56+
57+
console.log(
58+
`📄 [main] Found ${issues.length} total issues. Generating reports...`
59+
);
60+
await createCsvReport(issues, reportDir);
61+
await createPdfReport(issues, reportDir);
62+
63+
console.log(`\n✅ [main] Reports generated successfully in '${reportDir}'`);
64+
console.log(` - CSV: ${path.join(reportDir, 'report.csv')}`);
65+
console.log(` - PDF: ${path.join(reportDir, 'report.pdf')}`);
66+
} catch (error) {
67+
console.error(
68+
'\n❌ [main] An unexpected error occurred during the linting process:'
69+
);
70+
console.error(error);
71+
process.exit(1);
72+
}
73+
}
74+
75+
function runEslintInProject(projectPath) {
76+
return new Promise((resolve, reject) => {
77+
const isWindows = process.platform === 'win32';
78+
const eslintExecutable = isWindows ? 'eslint.cmd' : 'eslint';
79+
const eslintPath = path.join(
80+
projectPath,
81+
'node_modules',
82+
'.bin',
83+
eslintExecutable
84+
);
85+
86+
if (!fs.existsSync(eslintPath)) {
87+
const errorMessage = `Could not find a local ESLint installation in '${projectPath}'. Please run 'npm install eslint' in that project.`;
88+
return reject(new Error(errorMessage));
89+
}
90+
91+
const command = `"${eslintPath}" --format json --ignore-pattern "eslint.config.js" .`;
92+
exec(
93+
command,
94+
{ cwd: projectPath, maxBuffer: 1024 * 1024 * 5 },
95+
(error, stdout, stderr) => {
96+
if (stdout) {
97+
try {
98+
const results = JSON.parse(stdout);
99+
resolve(results.filter(r => r.messages.length > 0));
100+
} catch (e) {
101+
reject(
102+
`Failed to parse ESLint JSON output. Error: ${e.message}\nRaw Output:\n${stdout}`
103+
);
104+
}
105+
} else if (error && stderr) {
106+
reject(`ESLint command failed:\n${stderr}`);
107+
} else {
108+
resolve([]);
109+
}
110+
}
111+
);
112+
});
113+
}
114+
115+
async function createCsvReport(issues, reportDir) {
116+
const csvPath = path.join(reportDir, 'report.csv');
117+
if (issues.length === 0) {
118+
fs.writeFileSync(csvPath, 'No linting issues found.');
119+
return;
120+
}
121+
try {
122+
const fields = [
123+
'severity',
124+
'filePath',
125+
'line',
126+
'column',
127+
'ruleId',
128+
'message',
129+
];
130+
const json2csvParser = new Parser({ fields });
131+
const csv = json2csvParser.parse(issues);
132+
fs.writeFileSync(csvPath, csv);
133+
} catch (err) {
134+
console.error('❌ [createCsvReport] Failed to create CSV report:', err);
135+
}
136+
}
137+
138+
async function createPdfReport(issues, reportDir) {
139+
const pdfPath = path.join(reportDir, 'report.pdf');
140+
const doc = new pdfDocument({ margin: 30, size: 'A4', layout: 'landscape' });
141+
const writeStream = fs.createWriteStream(pdfPath);
142+
doc.pipe(writeStream);
143+
144+
try {
145+
const logoPath = path.join(__dirname, '..', 'static', 'img', 'logo.png');
146+
const logoWidth = 40;
147+
const logoHeight = 40;
148+
const pageWidth =
149+
doc.page.width - doc.page.margins.left - doc.page.margins.right;
150+
const logoX = doc.page.margins.left + pageWidth - logoWidth;
151+
const logoY = doc.page.margins.top;
152+
153+
if (fs.existsSync(logoPath)) {
154+
doc.image(logoPath, logoX, logoY, {
155+
width: logoWidth,
156+
height: logoHeight,
157+
});
158+
}
159+
160+
const titleText = 'ESLint Plugin Hub Issues Report';
161+
doc.fontSize(20).font('Helvetica-Bold');
162+
const titleWidth = doc.widthOfString(titleText);
163+
const titleX = doc.page.margins.left + (pageWidth - titleWidth) / 2;
164+
const titleY =
165+
doc.page.margins.top + (logoHeight - doc.heightOfString(titleText)) / 2;
166+
167+
doc.fillColor('red').text(titleText, titleX, titleY);
168+
169+
const belowHeaderY = doc.page.margins.top + logoHeight + 5;
170+
const dateText = `Generated on: ${new Date().toLocaleString()}`;
171+
doc
172+
.fontSize(10)
173+
.font('Helvetica')
174+
.fillColor('black')
175+
.text(dateText, doc.page.margins.left, belowHeaderY, {
176+
align: 'center',
177+
width: pageWidth,
178+
});
179+
doc.y = belowHeaderY + doc.heightOfString(dateText) + 10;
180+
181+
if (issues.length === 0) {
182+
doc
183+
.fontSize(12)
184+
.font('Helvetica')
185+
.text('No linting issues found.', { align: 'center' });
186+
} else {
187+
const errors = issues.filter(issue => issue.severity === 'Error');
188+
const warnings = issues.filter(issue => issue.severity === 'Warning');
189+
190+
const table = {
191+
headers: [
192+
'#',
193+
'Severity',
194+
'File Path',
195+
'Location',
196+
'Rule Name',
197+
'Message',
198+
],
199+
columns: [
200+
{ id: 'num', width: 30, align: 'center' },
201+
{ id: 'severity', width: 60, align: 'left' },
202+
{ id: 'filePath', width: 140, align: 'left' },
203+
{ id: 'location', width: 60, align: 'center' },
204+
{ id: 'ruleId', width: 120, align: 'left' },
205+
{ id: 'message', width: 290, align: 'left' },
206+
],
207+
};
208+
const cellPadding = 5;
209+
210+
const drawTableForIssues = (title, issueList) => {
211+
const totalTableWidth = table.columns.reduce(
212+
(sum, col) => sum + col.width,
213+
0
214+
);
215+
const tablePageWidth =
216+
doc.page.width - doc.page.margins.left - doc.page.margins.right;
217+
const tableStartX =
218+
doc.page.margins.left + (tablePageWidth - totalTableWidth) / 2;
219+
220+
if (doc.y + 45 > doc.page.height - doc.page.margins.bottom)
221+
doc.addPage();
222+
223+
const drawTableHeader = isFirstHeader => {
224+
if (isFirstHeader) {
225+
const titleStartY = doc.y;
226+
doc
227+
.rect(tableStartX, titleStartY, totalTableWidth, 25)
228+
.fillAndStroke('#4A5568', '#333333');
229+
doc
230+
.font('Helvetica-Bold')
231+
.fontSize(12)
232+
.fillColor('white')
233+
.text(title, tableStartX, titleStartY + 7, {
234+
width: totalTableWidth,
235+
align: 'center',
236+
});
237+
doc.y = titleStartY + 25;
238+
}
239+
const startY = doc.y;
240+
let startX = tableStartX;
241+
doc.font('Helvetica-Bold').fontSize(9);
242+
table.columns.forEach((column, i) => {
243+
doc
244+
.rect(startX, startY, column.width, 20)
245+
.fillAndStroke('#e0e0e0', '#aaaaaa');
246+
doc
247+
.fillColor('black')
248+
.text(
249+
table.headers[i],
250+
startX + cellPadding,
251+
startY + cellPadding,
252+
{ width: column.width - cellPadding * 2, align: column.align }
253+
);
254+
startX += column.width;
255+
});
256+
doc.y = startY + 20;
257+
};
258+
drawTableHeader(true);
259+
issueList.forEach((issue, index) => {
260+
doc.font('Helvetica').fontSize(8);
261+
const rowData = [
262+
index + 1,
263+
issue.severity,
264+
issue.filePath,
265+
`${issue.line}:${issue.column}`,
266+
issue.ruleId,
267+
issue.message,
268+
];
269+
let rowHeight = 0;
270+
table.columns.forEach((column, i) => {
271+
const cellHeight = doc.heightOfString(rowData[i].toString(), {
272+
width: column.width - cellPadding * 2,
273+
});
274+
rowHeight = Math.max(rowHeight, cellHeight);
275+
});
276+
rowHeight += cellPadding * 2;
277+
if (doc.y + rowHeight > doc.page.height - doc.page.margins.bottom) {
278+
doc.addPage();
279+
drawTableHeader(false);
280+
}
281+
const startY = doc.y;
282+
let startX = tableStartX;
283+
table.columns.forEach((column, i) => {
284+
doc.rect(startX, startY, column.width, rowHeight).stroke('#aaaaaa');
285+
let textColor = 'black';
286+
const cellValue = rowData[i];
287+
if (column.id === 'severity') {
288+
if (cellValue === 'Error') textColor = 'red';
289+
else if (cellValue === 'Warning') textColor = '#b45309';
290+
}
291+
doc
292+
.fillColor(textColor)
293+
.text(
294+
cellValue.toString(),
295+
startX + cellPadding,
296+
startY + cellPadding,
297+
{ width: column.width - cellPadding * 2, align: column.align }
298+
);
299+
startX += column.width;
300+
});
301+
doc.y = startY + rowHeight;
302+
});
303+
};
304+
if (errors.length > 0)
305+
drawTableForIssues(`Errors (${errors.length})`, errors);
306+
if (warnings.length > 0) {
307+
if (errors.length > 0) doc.moveDown(2);
308+
drawTableForIssues(`Warnings (${warnings.length})`, warnings);
309+
}
310+
}
311+
doc.end();
312+
} catch (err) {
313+
console.error('❌ [createPdfReport] Failed to create PDF report:', err);
314+
doc.end();
315+
}
316+
}
317+
318+
if (require.main === module) {
319+
main();
320+
}
321+
322+
module.exports = {
323+
runEslintInProject,
324+
createCsvReport,
325+
createPdfReport,
326+
};

index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const angularRules = require('./lib/rules/angular/index.js');
44
const advancedRules = require('./lib/rules/advanced/index.js');
55
const flatConfigBase = require('./configs/flat-config-base.js');
66
const legacyConfigBase = require('./configs/legacy-config-base.js');
7+
const expressRules = require('./lib/rules/node/express/open-api-spec/index.js');
78
const { name, version } = require('./package.json');
89

910
// Helper function to convert rule definitions to rule configurations for legacy config
@@ -65,6 +66,7 @@ const hub = {
6566
...reactRules.rules,
6667
...angularRules.rules,
6768
...advancedRules.rules,
69+
...expressRules.rules,
6870
},
6971
};
7072

@@ -76,6 +78,7 @@ const configs = {
7678
react: createConfig(convertRulesToLegacyConfig(reactRules.rules)),
7779
angular: createConfig(convertRulesToLegacyConfig(angularRules.rules)),
7880
advanced: createConfig(convertRulesToLegacyConfig(advancedRules.rules)),
81+
express: createConfig(convertRulesToLegacyConfig(expressRules.rules)),
7982
mern: createConfig(mernRecommendedRulesLegacy),
8083

8184
// Flat format configurations
@@ -96,6 +99,12 @@ const configs = {
9699
convertRulesToFlatConfig(advancedRules.rules),
97100
'hub/flat/advanced'
98101
),
102+
103+
'flat/express': createConfig(
104+
convertRulesToFlatConfig(expressRules.rules),
105+
'hub/flat/express'
106+
),
107+
99108
'flat/mern': createConfig(mernRecommendedRulesFlat, 'hub/flat/mern'),
100109
};
101110

0 commit comments

Comments
 (0)