From 43511f58103b4335f298d657549656bcfb93f317 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 16 Jul 2022 17:10:56 +0000 Subject: [PATCH 1/5] chore(deps): add renovate.json --- renovate.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..39a2b6e --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ] +} From 8c616f7518c2225075e9a175e57cbb53e3533f49 Mon Sep 17 00:00:00 2001 From: Victor Soares Date: Sun, 17 Jul 2022 21:53:03 -0300 Subject: [PATCH 2/5] feat: add xml output format --- .eslintignore | 1 + bin/intl-report | 2 +- package.json | 8 +- src/cli.ts | 37 +++ src/generateReport.ts | 92 +++--- src/generateResult.ts | 88 +++++- src/index.ts | 40 +-- src/intlrc-default.json | 1 + src/saveReport.ts | 38 +++ src/templates/details/result.html | 2 +- src/templates/formatSourceCode.ts | 3 + src/templates/getSeverity.ts | 12 + src/templates/isFullyInternationalized.ts | 3 + src/templates/main-page.html | 6 - src/templates/occurrence.template.ts | 21 ++ src/templates/results.template.ts | 71 +++++ src/templates/resultsDetails.template.ts | 42 +++ src/templates/sourceCode.template.ts | 64 +++++ src/templates/summary.template.ts | 39 +++ src/templates/template-generator.ts | 329 ---------------------- src/types.ts | 80 ++++++ yarn.lock | 10 + 22 files changed, 550 insertions(+), 439 deletions(-) create mode 100644 src/cli.ts create mode 100644 src/saveReport.ts create mode 100644 src/templates/formatSourceCode.ts create mode 100644 src/templates/getSeverity.ts create mode 100644 src/templates/isFullyInternationalized.ts create mode 100644 src/templates/occurrence.template.ts create mode 100644 src/templates/results.template.ts create mode 100644 src/templates/resultsDetails.template.ts create mode 100644 src/templates/sourceCode.template.ts create mode 100644 src/templates/summary.template.ts delete mode 100644 src/templates/template-generator.ts diff --git a/.eslintignore b/.eslintignore index c9ab804..7408370 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,3 +3,4 @@ *.test.js *.test.ts dist +bin diff --git a/bin/intl-report b/bin/intl-report index c533b16..c688b4a 100755 --- a/bin/intl-report +++ b/bin/intl-report @@ -1,2 +1,2 @@ #!/usr/bin/env node -require('../dist/cjs/index.js') +require('../dist/cjs/cli.js') diff --git a/package.json b/package.json index 90b7032..5a7562f 100644 --- a/package.json +++ b/package.json @@ -68,17 +68,18 @@ ] }, "peerDependencies": { - "eslint": "^8.0.0", - "@typescript-eslint/parser": "^5.0.0" + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^8.0.0" }, "dependencies": { + "@typescript-eslint/parser": "^5.28.0", "eslint-plugin-react-intl-universal": "^1.0.4", "glob-promise": "^4.2.2", "gulp": "^4.0.2", "gulp-eslint-new": "^1.5.1", + "jstoxml": "^1.0.1", "lodash": "^4.17.21", "ora": "5.4.1", - "@typescript-eslint/parser": "^5.28.0", "typescript": "4.4.4" }, "devDependencies": { @@ -86,6 +87,7 @@ "@commitlint/config-conventional": "^17.0.3", "@types/gulp": "^4.0.9", "@types/jest": "^28.1.3", + "@types/jstoxml": "^2.0.2", "@types/lodash": "^4.14.182", "@typescript-eslint/eslint-plugin": "^5.28.0", "commitizen": "^4.2.4", diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..1f268dd --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,37 @@ +import { generateReport } from './generateReport'; +import { generateResult } from './generateResult'; +import { getArguments } from './getArguments'; +import { getSettings } from './getSettings'; +import { saveReport } from './saveReport'; + +(async () => { + try { + const args = getArguments(process.argv.slice(2)); + + const source = args['--source']; + const configFile = args['--config-file']; + + const settings = await getSettings(configFile); + + const results = await generateResult( + source, + settings, + // ! The parameter in this function has been changed to json if the user wants to have the output in xml or js(json) if he uses this function outside the cli + settings.outputFormat === 'html' ? 'json' : 'xml' + ); + + let report: string; + + if (settings.outputFormat === 'html') { + report = await generateReport(results, settings); + await saveReport(settings.outputDir, report, settings.outputFormat); + } + + if (settings.outputFormat === 'xml') { + report = String(results); + await saveReport(settings.outputDir, report, settings.outputFormat); + } + } catch (error: any) { + throw new Error(error); + } +})(); diff --git a/src/generateReport.ts b/src/generateReport.ts index ca0df23..3a75822 100644 --- a/src/generateReport.ts +++ b/src/generateReport.ts @@ -1,58 +1,42 @@ -import gulp from 'gulp'; -import gulpESLintNew from 'gulp-eslint-new'; -import path from 'path'; import fs from 'fs'; -import ora from 'ora'; -import generateTemplate from './templates/template-generator'; -import eslintConfig from './eslint_config'; -import { Analyzer, ReporterSettings } from './types'; +import path from 'path'; +import _ from 'lodash'; + +import { renderSummaryTemplate } from './templates/summary.template'; + +import { ReporterSettings, Results } from './types'; +import { renderResultsTemplate } from './templates/results.template'; + +const pageTemplate = _.template( + fs.readFileSync(path.join(__dirname, 'templates', 'main-page.html'), 'utf-8') +); + +const styles = _.template( + fs.readFileSync(path.join(__dirname, 'helpers/styles.html'), 'utf-8') +); + +const scripts = _.template( + fs.readFileSync(path.join(__dirname, 'helpers/scripts.html'), 'utf-8') +); export async function generateReport( - files: string[], - output: string, - debug: boolean, - analyzer: Analyzer, - reporterSettings: ReporterSettings -): Promise { - let filesProcessed = 0; - const totalFiles = files.length; - - const spinner = ora('Generating report').start(); - return gulp - .src(files) - .pipe( - gulpESLintNew({ - useEslintrc: false, - allowInlineConfig: false, - baseConfig: { - ...eslintConfig, - rules: { - ...eslintConfig.rules, - 'react-intl-universal/no-literal-string': ['error', analyzer], - }, - }, - }) - ) - .pipe( - gulpESLintNew - .format( - (results) => generateTemplate(results, reporterSettings), - (results) => { - const reportName = `intl-report-${new Date().getTime()}.html`; - if (!fs.existsSync(output)) fs.mkdirSync(output); - fs.writeFileSync(path.join(output, reportName), results); - spinner.succeed( - `Generated report for ${totalFiles} files. See ${output}/${reportName}` - ); - } - ) - .on('data', (file) => { - filesProcessed += 1; - spinner.text = `Generating report (${filesProcessed}/${totalFiles})`; - - if (debug) { - console.log(`\nFile processed: ${file.path}`); - } - }) - ); + result: Results, + config: ReporterSettings +) { + const { template } = config; + + const { internationalizedCount, notInternationalizedCount, results } = result; + + const currentDir = process.cwd() || ''; + + return pageTemplate({ + title: template.title, + reportSummary: renderSummaryTemplate( + notInternationalizedCount, + internationalizedCount + ), + results: renderResultsTemplate(results, currentDir), + styles: styles(), + scripts: scripts(), + }); } diff --git a/src/generateResult.ts b/src/generateResult.ts index 5758908..62129f3 100644 --- a/src/generateResult.ts +++ b/src/generateResult.ts @@ -1,18 +1,33 @@ import gulp from 'gulp'; import gulpESLintNew from 'gulp-eslint-new'; import ora from 'ora'; +import _ from 'lodash'; +import { toXML } from 'jstoxml'; import eslintConfig from './eslint_config'; -import { Result } from './types'; +import { Occurrence, ReporterSettings, Results } from './types'; import { findFiles } from './findFiles'; import { getSettings } from './getSettings'; export async function generateResult( source: string, - configFile?: string -): Promise { + config?: string | ReporterSettings, + outputFormat?: 'json' | 'xml' +): Promise { let filesProcessed = 0; - const reporterSettings = await getSettings(configFile); + let reporterSettings: ReporterSettings; + + if (config) { + if (typeof config === 'string') { + reporterSettings = await getSettings(config); + } else { + reporterSettings = config; + } + } else { + reporterSettings = await getSettings(); + } + + const spinner = ora('Finding files').start(); const files = await findFiles( source, @@ -21,9 +36,16 @@ export async function generateResult( reporterSettings.debug ); + if (_.isEmpty(files)) { + spinner.fail('No files found!'); + process.exit(0); + } + const totalFiles = files.length; - const spinner = ora('Generating results').start(); + spinner.succeed(`Found ${totalFiles} files`); + + spinner.start('Analyzing files'); return new Promise((resolve, reject) => { gulp @@ -62,14 +84,64 @@ export async function generateResult( 100 ); - return { + const result: Results = { notInternationalizedCount, internationalizedCount, percentage, - } as any; + results: results.map( + ({ + filePath, + messages, + errorCount, + warningCount, + source: fileSource, + }) => { + const filePercentage = Math.round( + (warningCount / (errorCount + warningCount)) * 100 + ); + return { + filePath, + occurrences: messages.map( + ({ + ruleId, + line, + endLine, + column, + endColumn, + message, + }) => + ({ + type: + ruleId === + 'react-intl-universal/no-literal-string' + ? 'not-internationalized' + : 'internationalized', + line, + endLine, + column, + endColumn, + message, + } as Occurrence) + ), + notInternationalizedCount: errorCount, + internationalizedCount: warningCount, + percentage: filePercentage, + source: fileSource, + }; + } + ), + }; + return result as any; }, (results) => { - resolve(results as unknown as Result); + if (outputFormat === 'xml') { + resolve( + toXML( + { intl: { results } }, + { indent: ' ', header: true } + ) as any + ); + } else resolve(results as unknown as Results); spinner.succeed(`Finish! ${totalFiles} files processed`); } ) diff --git a/src/index.ts b/src/index.ts index ec72bf9..7b60006 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,39 +1,5 @@ -import _ from 'lodash'; -import ora from 'ora'; -import { findFiles } from './findFiles'; -import { generateReport } from './generateReport'; -import { getArguments } from './getArguments'; -import { getSettings } from './getSettings'; +import { generateResult } from './generateResult'; -(async () => { - const spinner = ora('Finding files').start(); - try { - const args = getArguments(process.argv.slice(2)); +export * from './types'; - const reporterSettings = await getSettings(args['--config-file']); - - const files = await findFiles( - args['--source'], - reporterSettings.ignorePatterns, - reporterSettings.extensions, - reporterSettings.debug - ); - spinner.succeed(`Found ${files.length} files`); - - if (_.isEmpty(files)) { - spinner.fail('No files found!'); - process.exit(0); - } - - generateReport( - files, - reporterSettings.outputDir, - reporterSettings.debug, - reporterSettings.analyzer, - reporterSettings - ); - } catch (error: any) { - spinner.stop(); - throw new Error(error); - } -})(); +export { generateResult }; diff --git a/src/intlrc-default.json b/src/intlrc-default.json index 1c1ac9a..54bf658 100644 --- a/src/intlrc-default.json +++ b/src/intlrc-default.json @@ -11,6 +11,7 @@ ], "extensions": ["js", "jsx", "ts", "tsx"], "outputDir": "out", + "outputFormat": "html", "debug": false, "analyzer": { "mode": "jsx-text-only", diff --git a/src/saveReport.ts b/src/saveReport.ts new file mode 100644 index 0000000..e690305 --- /dev/null +++ b/src/saveReport.ts @@ -0,0 +1,38 @@ +import fs from 'fs'; +import path from 'path'; +import ora from 'ora'; + +export async function saveReport( + outputDir: string, + report: string, + outputFormat: 'html' | 'xml' +) { + const spinner = ora('Saving report').start(); + + try { + let reportName = `intl-report-${new Date().getTime()}`; + + if (outputFormat === 'xml') { + reportName += '.xml'; + } + + if (outputFormat === 'html') { + reportName += '.html'; + } + + if (!fs.existsSync(outputDir)) await fs.promises.mkdir(outputDir); + + const outputPath = path.join(outputDir, reportName); + + await fs.promises.writeFile( + path.join(outputDir, reportName), + report, + 'utf-8' + ); + + spinner.succeed(`Save report on ${outputPath}`); + } catch (error: any) { + spinner.fail(error); + throw new Error(error); + } +} diff --git a/src/templates/details/result.html b/src/templates/details/result.html index 03d83c2..3802520 100644 --- a/src/templates/details/result.html +++ b/src/templates/details/result.html @@ -2,7 +2,7 @@ id="<%- fileId %>" class="lint-result bg-<%- progress.toString() !== 'NaN' ? rowColor : 'gray' %>" data-group="f-<%- index %>" - data-problem-count="<%- problemCount %>" + data-problem-count="<%- occurrencesCount %>" tabindex="0" > diff --git a/src/templates/formatSourceCode.ts b/src/templates/formatSourceCode.ts new file mode 100644 index 0000000..971b3fe --- /dev/null +++ b/src/templates/formatSourceCode.ts @@ -0,0 +1,3 @@ +export function formatSourceCode(sourceCode: string) { + return sourceCode.replace(//g, '>'); +} diff --git a/src/templates/getSeverity.ts b/src/templates/getSeverity.ts new file mode 100644 index 0000000..7065ac6 --- /dev/null +++ b/src/templates/getSeverity.ts @@ -0,0 +1,12 @@ +export function getSeverity( + notInternationalizedCount: number, + internationalizedCount: number +) { + if (notInternationalizedCount !== 0) { + return 'error'; + } + if (internationalizedCount !== 0) { + return 'warning'; + } + return 'success'; +} diff --git a/src/templates/isFullyInternationalized.ts b/src/templates/isFullyInternationalized.ts new file mode 100644 index 0000000..dcbfc3f --- /dev/null +++ b/src/templates/isFullyInternationalized.ts @@ -0,0 +1,3 @@ +export function isFullyInternationalized(notInternationalizedCount: number) { + return notInternationalizedCount === 0; +} diff --git a/src/templates/main-page.html b/src/templates/main-page.html index e994f3b..caba1fb 100644 --- a/src/templates/main-page.html +++ b/src/templates/main-page.html @@ -18,12 +18,6 @@ <%= reportSummary %> - -
diff --git a/src/templates/occurrence.template.ts b/src/templates/occurrence.template.ts new file mode 100644 index 0000000..bc464a1 --- /dev/null +++ b/src/templates/occurrence.template.ts @@ -0,0 +1,21 @@ +import fs from 'fs'; +import path from 'path'; +import _ from 'lodash'; +import { Occurrence } from '../types'; + +const occurrenceTemplate = _.template( + fs.readFileSync(path.join(__dirname, 'details/code/occurrence.html'), 'utf-8') +); + +export function renderOccurrenceTemplate({ + line, + column, + type, + message, +}: Occurrence) { + return occurrenceTemplate({ + lineNumber: line, + column, + message: type === 'not-internationalized' ? message : 'Intl Function', + }); +} diff --git a/src/templates/results.template.ts b/src/templates/results.template.ts new file mode 100644 index 0000000..7e40a07 --- /dev/null +++ b/src/templates/results.template.ts @@ -0,0 +1,71 @@ +import fs from 'fs'; +import path from 'path'; +import _ from 'lodash'; +import { Result } from '../types'; +import { getSeverity } from './getSeverity'; +import { renderSummaryTemplate } from './summary.template'; +import { isFullyInternationalized } from './isFullyInternationalized'; +import { formatSourceCode } from './formatSourceCode'; +import { renderResultDetailsTemplate } from './resultsDetails.template'; + +const resultTemplate = _.template( + fs.readFileSync(path.join(__dirname, 'details/result.html'), 'utf-8') +); + +function renderResultTemplate( + index: number, + currentDir: string, + result: Result +) { + const { + filePath, + notInternationalizedCount, + internationalizedCount, + percentage, + occurrences, + source, + } = result; + + let template = resultTemplate({ + index, + fileId: _.camelCase(filePath), + filePath: result.filePath.replace(currentDir, ''), + color: getSeverity(notInternationalizedCount, internationalizedCount), + summary: renderSummaryTemplate( + notInternationalizedCount, + internationalizedCount + ), + occurrencesCount: notInternationalizedCount + internationalizedCount, + progress: percentage, + rowColor: isFullyInternationalized(notInternationalizedCount) + ? 'success' + : 'error', + internationalizedCount, + notInternationalizedCount, + zeroOccurrences: notInternationalizedCount + internationalizedCount === 0, + }); + + // only renders the source code if there are occurrences present in the file + if (!_.isEmpty(occurrences)) { + // reads the file to get the source code if the source is not provided + const sourceCode = formatSourceCode( + source || fs.readFileSync(filePath, 'utf8') + ); + + template += renderResultDetailsTemplate( + sourceCode, + occurrences, + index, + internationalizedCount, + notInternationalizedCount + ); + } + + return template; +} + +export function renderResultsTemplate(results: Result[], currentDir: string) { + return _.map(results, (result, index) => + renderResultTemplate(index, currentDir, result) + ).join('\n'); +} diff --git a/src/templates/resultsDetails.template.ts b/src/templates/resultsDetails.template.ts new file mode 100644 index 0000000..7f32c26 --- /dev/null +++ b/src/templates/resultsDetails.template.ts @@ -0,0 +1,42 @@ +import fs from 'fs'; +import path from 'path'; +import _ from 'lodash'; + +import { Occurrence } from '../types'; +import { renderSourceCodeTemplate } from './sourceCode.template'; +import { renderOccurrenceTemplate } from './occurrence.template'; + +const resultDetailsTemplate = _.template( + fs.readFileSync(path.join(__dirname, 'details/details.html'), 'utf-8') +); + +const resultSummaryTemplate = _.template( + fs.readFileSync(path.join(__dirname, 'details/summary.html'), 'utf-8') +); + +export function renderResultDetailsTemplate( + sourceCode: string, + occurrences: Array, + parentIndex: number, + internationalizedCount: number, + notInternationalizedCount: number +) { + return resultDetailsTemplate({ + parentIndex, + sourceCode: renderSourceCodeTemplate(sourceCode, occurrences, parentIndex), + detailSummary: resultSummaryTemplate({ + topIssues: '', + internationalizedOccurrences: _.map( + occurrences.filter(({ type }) => type === 'internationalized'), + renderOccurrenceTemplate + ).join(''), + notInternationalizedOccurrences: _.map( + occurrences.filter(({ type }) => type === 'not-internationalized'), + renderOccurrenceTemplate + ).join(''), + internationalizedCount, + notInternationalizedCount, + totalCount: internationalizedCount + notInternationalizedCount, + }), + }); +} diff --git a/src/templates/sourceCode.template.ts b/src/templates/sourceCode.template.ts new file mode 100644 index 0000000..135c9ca --- /dev/null +++ b/src/templates/sourceCode.template.ts @@ -0,0 +1,64 @@ +import fs from 'fs'; +import path from 'path'; +import _ from 'lodash'; + +import { Occurrence } from '../types'; +import { renderOccurrenceTemplate } from './occurrence.template'; + +const codeWrapperTemplate = _.template( + fs.readFileSync( + path.join(__dirname, 'details/code/code-wrapper.html'), + 'utf-8' + ) +); + +const codeTemplate = _.template( + fs.readFileSync(path.join(__dirname, 'details/code/code.html'), 'utf-8') +); + +export function renderSourceCodeTemplate( + sourceCode: string, + occurrences: Array, + parentIndex: number +) { + return codeWrapperTemplate({ + parentIndex, + sourceCode: _.map(sourceCode.split('\n'), (code, lineNumber) => { + const lineMessages = _.filter(occurrences, { line: lineNumber + 1 }); + const type = _.get(lineMessages[0], 'type'); + + let template = ''; + + // checks if there is a problem on the current line and renders it + if (!_.isEmpty(lineMessages)) { + template += _.map(lineMessages, renderOccurrenceTemplate).join(''); + } + + if (type === 'internationalized') { + template += codeTemplate({ + lineNumber: lineNumber + 1, + code, + status: 'warning', + }); + } + + if (type === 'not-internationalized') { + template += codeTemplate({ + lineNumber: lineNumber + 1, + code, + status: 'error', + }); + } + + if (!type) { + template += codeTemplate({ + lineNumber: lineNumber + 1, + code, + status: 'success', + }); + } + + return template; + }).join('\n'), + }); +} diff --git a/src/templates/summary.template.ts b/src/templates/summary.template.ts new file mode 100644 index 0000000..35b4cec --- /dev/null +++ b/src/templates/summary.template.ts @@ -0,0 +1,39 @@ +import fs from 'fs'; +import path from 'path'; +import _ from 'lodash'; + +const summaryTemplate = _.template( + fs.readFileSync(path.join(__dirname, 'summary/summary-details.html'), 'utf-8') +); + +export function renderSummaryTemplate( + notInternationalizedCount: number, + internationalizedCount: number +) { + const percentage = Math.round( + (internationalizedCount / + (notInternationalizedCount + internationalizedCount)) * + 100 + ); + + const percentageColor = () => { + if (percentage >= 75) { + return '#157F1F'; + } + if (percentage >= 50) { + return '#FBB13C'; + } + return '#9E2B25'; + }; + + const strokeDashoffset = Math.round((629 * percentage) / 100); + + return summaryTemplate({ + notInternationalizedCount, + internationalizedCount, + totalCount: notInternationalizedCount + internationalizedCount, + percentage, + percentageColor: percentageColor(), + strokeDashoffset, + }); +} diff --git a/src/templates/template-generator.ts b/src/templates/template-generator.ts deleted file mode 100644 index 81cf7e8..0000000 --- a/src/templates/template-generator.ts +++ /dev/null @@ -1,329 +0,0 @@ -import _ from 'lodash'; -import fs from 'fs'; -import path from 'path'; -import { ESLint } from 'eslint'; -import { ReporterSettings } from '../types'; - -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -const styles = _.template( - fs.readFileSync(path.join(__dirname, '../helpers/styles.html'), 'utf-8') -); -const scripts = _.template( - fs.readFileSync(path.join(__dirname, '../helpers/scripts.html'), 'utf-8') -); -const pageTemplate = _.template( - fs.readFileSync(path.join(__dirname, 'main-page.html'), 'utf-8') -); -const resultTemplate = _.template( - fs.readFileSync(path.join(__dirname, 'details/result.html'), 'utf-8') -); -const resultDetailsTemplate = _.template( - fs.readFileSync(path.join(__dirname, 'details/details.html'), 'utf-8') -); -const resultSummaryTemplate = _.template( - fs.readFileSync(path.join(__dirname, 'details/summary.html'), 'utf-8') -); -const codeWrapperTemplate = _.template( - fs.readFileSync( - path.join(__dirname, 'details/code/code-wrapper.html'), - 'utf-8' - ) -); -const codeTemplate = _.template( - fs.readFileSync(path.join(__dirname, 'details/code/code.html'), 'utf-8') -); -const occurrenceTemplate = _.template( - fs.readFileSync(path.join(__dirname, 'details/code/occurrence.html'), 'utf-8') -); -const summaryTemplate = _.template( - fs.readFileSync(path.join(__dirname, 'summary/summary-details.html'), 'utf-8') -); -const filesTemplate = _.template( - fs.readFileSync(path.join(__dirname, 'summary/files.html'), 'utf-8') -); - -/** - * Renders text along the template of x problems (x errors, x warnings) - * @param {int} totalErrors Total errors - * @param {int} totalWarnings Total warnings - * @returns {string} The formatted string, pluralized where necessary - */ -function renderSummary( - notInternationalizedCount: number, - internationalizedCount: number -) { - const percentage = Math.round( - (internationalizedCount / - (notInternationalizedCount + internationalizedCount)) * - 100 - ); - const percentageColor = () => { - if (percentage >= 75) { - return '#157F1F'; - } - if (percentage >= 50) { - return '#FBB13C'; - } - return '#9E2B25'; - }; - const strokeDashoffset = Math.round((629 * percentage) / 100); - return summaryTemplate({ - notInternationalizedCount, - internationalizedCount, - totalCount: notInternationalizedCount + internationalizedCount, - percentage, - percentageColor: percentageColor(), - strokeDashoffset, - }); -} - -/** - * Converts the severity number to a string - * @param {int} severity severity number - * @returns {string} The color string based on severity number (0 = success, 1 = warning, 2 = error) - */ -function severityString(severity: number) { - const colors = ['success', 'warning', 'error']; - - return colors[severity]; -} - -/** - * Get the color based on whether there are errors/warnings... - * @param {int} totalErrors Total errors - * @param {int} totalWarnings Total warnings - * @returns {string} The color code (success = green, warning = yellow, error = red) - */ -function renderColor(totalErrors: number, totalWarnings: number) { - if (totalErrors !== 0) { - return severityString(2); - } - if (totalWarnings !== 0) { - return severityString(1); - } - return severityString(0); -} - -/** - * Renders an issue - * @param {object} message a message object with an issue - * @returns {string} HTML string of an issue - */ -function renderOccurrence(message: { - line: number; - column: number; - message: string; -}) { - return occurrenceTemplate({ - lineNumber: message.line, - column: message.column, - message: - message.message !== "Don't use intl" ? message.message : 'Intl Function', - }); -} - -/** - * Renders the source code for the files that have issues and marks the lines that have problems - * @param {string} sourceCode source code string - * @param {array} messages array of messages with the problems in a file - * @param {int} parentIndex file index - * @returns {string} HTML string of the code file that is being linted - */ -function renderSourceCode( - sourceCode: any, - messages: any[], - parentIndex: number -) { - return codeWrapperTemplate({ - parentIndex, - sourceCode: _.map(sourceCode.split('\n'), (code, lineNumber) => { - const lineMessages = _.filter(messages, { line: lineNumber + 1 }); - const severity = _.get(lineMessages[0], 'severity') || 0; - - let template = ''; - - // checks if there is a problem on the current line and renders it - if (!_.isEmpty(lineMessages)) { - template += _.map(lineMessages, renderOccurrence).join(''); - } - - // adds a line of code to the template (with line number and severity color if appropriate - template += codeTemplate({ - lineNumber: lineNumber + 1, - code, - status: severityString(severity), - }); - - return template; - }).join('\n'), - }); -} - -/** - * Renders the result details with tabs for source code and a summary - * @param {string} sourceCode source code string - * @param {array} messages array of messages with the problems in a file - * @param {int} parentIndex file index - * @returns {string} HTML string of result details - */ -function renderResultDetails( - sourceCode: string, - messages: any[], - parentIndex: number, - internationalizedCount: number, - notInternationalizedCount: number -) { - // const topIssues = messages.length < 10 ? '' : _.groupBy(messages, 'severity'); - - /* console.log({ - sourceCode, messages, parentIndex, internationalizedCount, notInternationalizedCount, - }); */ - return resultDetailsTemplate({ - parentIndex, - sourceCode: renderSourceCode(sourceCode, messages, parentIndex), - detailSummary: resultSummaryTemplate({ - topIssues: '', - internationalizedOccurrences: _.map( - messages.filter(({ message }) => message === "Don't use intl"), - renderOccurrence - ).join(''), - notInternationalizedOccurrences: _.map( - messages.filter(({ message }) => message !== "Don't use intl"), - renderOccurrence - ).join(''), - internationalizedCount, - notInternationalizedCount, - totalCount: internationalizedCount + notInternationalizedCount, - }), - }); -} - -/** - * Formats the source code before adding it to the HTML - * @param {string} sourceCode Source code string - * @returns {string} Source code string which will not cause issues in the HTML - */ -function formatSourceCode(sourceCode: string) { - return sourceCode.replace(//g, '>'); -} - -/** - * Verify that the file is a fully internationalized - * @param {number} internationalizedCount Count of internationalized occurrences. - * @param {number} notInternationalizedCount Count of non-internationalized occurrences. - * @returns {boolean} True if the file is fully internationalized. - */ -function isFullyInternationalized(notInternationalizedCount: number) { - return notInternationalizedCount === 0; -} - -/** - * Creates the test results HTML - * @param {Array} results Test results. - * @param {String} currDir Current working directory - * @returns {string} HTML string describing the results. - */ -function renderResults(results: any[], currDir: string) { - return _.map(results, (result, index) => { - const internationalizedCount = result.warningCount; - const notInternationalizedCount = result.errorCount; - const progress = Math.round( - (internationalizedCount / - (notInternationalizedCount + internationalizedCount)) * - 100 - ); - - let template = resultTemplate({ - index, - fileId: _.camelCase(result.filePath), - filePath: result.filePath.replace(currDir, ''), - color: renderColor(result.errorCount, result.warningCount), - summary: renderSummary(result.errorCount, result.warningCount), - problemCount: result.errorCount + result.warningCount, - progress, - rowColor: isFullyInternationalized(notInternationalizedCount) - ? 'success' - : 'error', - internationalizedCount, - notInternationalizedCount, - zeroOccurrences: result.errorCount + result.warningCount === 0, - totalCount: internationalizedCount + notInternationalizedCount, - }); - - // only renders the source code if there are issues present in the file - if (!_.isEmpty(result.messages)) { - // reads the file to get the source code if the source is not provided - const sourceCode = formatSourceCode( - result.source || fs.readFileSync(result.filePath, 'utf8') - ); - - template += renderResultDetails( - sourceCode, - result.messages, - index, - result.warningCount, - result.errorCount - ); - } - - return template; - }).join('\n'); -} - -/** - * Renders list of problem files - * @param {array} files - * @param {String} currDir Current working directory - * @return {string} HTML string describing the files. - */ -function renderProblemFiles(files: any[], currDir: string) { - return _.map(files, (fileDetails) => - filesTemplate({ - fileId: _.camelCase(fileDetails.filePath), - filePath: fileDetails.filePath.replace(currDir, ''), - errorCount: fileDetails.errorCount, - warningCount: fileDetails.warningCount, - }) - ).join('\n'); -} - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -export default async function generateTemplate( - results: ESLint.LintResult[], - reporterSettings: ReporterSettings -) { - const currWorkingDir = process.cwd() || ''; - const problemFiles = _(results) - .reject({ - errorCount: 0, - warningCount: 0, - }) - .orderBy(['errorCount', 'warningCount'], ['desc', 'desc']) - .take(5) - .value(); // top five files with most problems - - let totalErrors = 0; - let totalWarnings = 0; - - // Iterate over results to get totals - results.forEach((result) => { - totalErrors += result.errorCount; - totalWarnings += result.warningCount; - }); - - return pageTemplate({ - title: reporterSettings.template.title, - reportColor: renderColor(totalErrors, totalWarnings), - reportSummary: renderSummary(totalErrors, totalWarnings), - mostProblems: renderProblemFiles(problemFiles, currWorkingDir), - results: renderResults(results, currWorkingDir), - styles: styles(), - scripts: scripts(), - }); -} diff --git a/src/types.ts b/src/types.ts index 6a90cb3..644edfc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,69 @@ +export type OccurrenceType = 'internationalized' | 'not-internationalized'; + +/** + * The occurrence can have two types internationalized and non-internationalized. + * The non-internationalized occurrence represents what is identified as wrong by the [no-literal-string](https://github.com/victorsoares96/eslint-plugin-react-intl-universal) rule. + * The internationalized occurrence represents the pieces of code that match `intl.get` or `intl.getHTML` + */ +export type Occurrence = { + /** + * Indicates if the occurrence type is internationalized or not. + * @see {@link OccurrenceType} + */ + type: OccurrenceType; + /** + * The message returned by the rule. + */ + message: string; + /** + * The line number of the occurrence. + */ + line: number; + /** + * The end line number of the occurrence. + */ + endLine: number; + /** + * The column number of the occurrence. + */ + column: number; + /** + * The end column number of the occurrence. + */ + endColumn: number; +}; + export type Result = { + /** + * Path of the processed file. + * @example 'src/components/App.tsx' + */ + filePath: string; + /** + * List of internationalized(or not) occurrences found by the analysis. + * @see {@link Occurrence} + */ + occurrences: Array; + /** + * Number of occurrences of non-internationalized strings. + */ + notInternationalizedCount: number; + /** + * Number of occurrences of internationalized strings. + */ + internationalizedCount: number; + /** + * Percentage of occurrences of internationalized strings. + */ + percentage: number; + /** + * Source code of the processed file. + * @example 'import React from \'react\';\nimport { IntlProvider } from \'react-intl\';\nimport messages from \'./messages.json\';\n\nconst App = () => (\n \n
Hello World
\n
\n);\n\nexport default App;' + */ + source?: string; +}; + +export type Results = { /** * Number of occurrences of non-internationalized strings. */ @@ -11,6 +76,10 @@ export type Result = { * Percentage of occurrences of internationalized strings. */ percentage: number; + /** + * Detailed result of the analysis performed on each file. + */ + results: Array; }; export type Analyzer = { @@ -52,6 +121,10 @@ export type ReporterSettings = { * @type {string} */ outputDir: string; + /** + * The output format. + */ + outputFormat: 'html' | 'xml'; /** * Whether to print debug information. * @default false @@ -80,3 +153,10 @@ export type CLIArguments = { '--config-file'?: string; '--extensions'?: string[]; }; + +export type ReportOptions = { + /** + * The report title + */ + title: string; +}; diff --git a/yarn.lock b/yarn.lock index a31b6d0..e79939a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -929,6 +929,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/jstoxml@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/jstoxml/-/jstoxml-2.0.2.tgz#7ccfe12412857cc541a737ae56ce2559bd9b8ef4" + integrity sha512-60VaXPlZbd7tEhloAkE2E0lg+QoWpnGusdy+2pGMGFFpdsyxm/1GKs0o/nLJJKWXci92cnq2utmqaV5L7Zjqxw== + "@types/lodash@^4.14.182": version "4.14.182" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" @@ -4407,6 +4412,11 @@ jsonparse@^1.2.0: resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== +jstoxml@^1.0.1: + version "1.6.11" + resolved "https://registry.yarnpkg.com/jstoxml/-/jstoxml-1.6.11.tgz#295f3aec62b845310026c702173fab0c65fc8f6c" + integrity sha512-7X6qWhnru9dtSLoGilRFycpMgxX0LA+7cZORi2ObYTiwbPN0Byl3v9aaGnb0XrWWloub9W6z5zM5fW9HOoWctA== + just-debounce@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.1.0.tgz#2f81a3ad4121a76bc7cb45dbf704c0d76a8e5ddf" From 2c237265442fc1ed227e3f03b085756e02cc512c Mon Sep 17 00:00:00 2001 From: Victor Soares Date: Sun, 17 Jul 2022 22:04:09 -0300 Subject: [PATCH 3/5] docs: update readme --- README.md | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index defee3c..db03668 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ This file will be used by the generator, it represents a set of instructions tha ], "extensions": ["js", "jsx", "ts", "tsx"], "outputDir": "out", + "outputFormat": "json", // can be xml to "debug": false, "analyzer": { "mode": "jsx-text-only", @@ -148,26 +149,7 @@ That asynchronous function above mentioned receives as argument: That asynchronous function above mentioned returns: -- `result`: The result of the analysis. [See the result structure](#result-structure) - -### Result Structure - -```ts -type Result = { - /** - * Number of occurrences of non-internationalized strings. - */ - notInternationalizedCount: number; - /** - * Number of occurrences of internationalized strings. - */ - internationalizedCount: number; - /** - * Percentage of occurrences of internationalized strings. - */ - percentage: number; -}; -``` +- `result`: The result of the analysis. [See the result structure](src/types.ts#L36) ## Help to improve this project From d16ed010b3e28ba99e1548663ad079050e27e95e Mon Sep 17 00:00:00 2001 From: Victor Soares Date: Sun, 17 Jul 2022 22:04:50 -0300 Subject: [PATCH 4/5] chore(types): remove unused --extensions CLIArguments type --- src/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 644edfc..bbdefe2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -151,7 +151,6 @@ export type ReporterSettings = { export type CLIArguments = { '--source': string; '--config-file'?: string; - '--extensions'?: string[]; }; export type ReportOptions = { From 6fe96cea5d4f62031fd28db230248943e6fcfc35 Mon Sep 17 00:00:00 2001 From: Victor Soares Date: Sun, 17 Jul 2022 22:09:56 -0300 Subject: [PATCH 5/5] chore: update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5a7562f..4385b38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-intl-universal-reporter", - "version": "0.2.1", + "version": "0.3.1", "description": "An report generator to measure the number of internationalized and non-internationalized occurrences of a project.", "main": "dist/cjs/index.js", "bin": {