123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- 'use strict';
- const path = require('path');
- const stringWidth = require('string-width');
- const table = require('table');
- const { yellow, dim, underline, blue, red, green } = require('picocolors');
- const terminalLink = require('./terminalLink');
- const MARGIN_WIDTHS = 9;
- /**
- * @param {string} s
- * @returns {string}
- */
- function nope(s) {
- return s;
- }
- const levelColors = {
- info: blue,
- warning: yellow,
- error: red,
- success: nope,
- };
- const symbols = {
- info: blue('ℹ'),
- warning: yellow('⚠'),
- error: red('✖'),
- success: green('✔'),
- };
- /**
- * @param {import('stylelint').LintResult[]} results
- * @returns {string}
- */
- function deprecationsFormatter(results) {
- const allDeprecationWarnings = results.flatMap((result) => result.deprecations);
- if (allDeprecationWarnings.length === 0) {
- return '';
- }
- const seenText = new Set();
- return allDeprecationWarnings.reduce((output, warning) => {
- if (seenText.has(warning.text)) return output;
- seenText.add(warning.text);
- output += yellow('Deprecation Warning: ');
- output += warning.text;
- if (warning.reference) {
- output += dim(' See: ');
- output += dim(underline(warning.reference));
- }
- return `${output}\n`;
- }, '\n');
- }
- /**
- * @param {import('stylelint').LintResult[]} results
- * @return {string}
- */
- function invalidOptionsFormatter(results) {
- const allInvalidOptionWarnings = results.flatMap((result) =>
- result.invalidOptionWarnings.map((warning) => warning.text),
- );
- const uniqueInvalidOptionWarnings = [...new Set(allInvalidOptionWarnings)];
- return uniqueInvalidOptionWarnings.reduce((output, warning) => {
- output += red('Invalid Option: ');
- output += warning;
- return `${output}\n`;
- }, '\n');
- }
- /**
- * @param {string} fromValue
- * @param {string} cwd
- * @return {string}
- */
- function logFrom(fromValue, cwd) {
- if (fromValue.startsWith('<')) {
- return underline(fromValue);
- }
- const filePath = path.relative(cwd, fromValue).split(path.sep).join('/');
- return terminalLink(filePath, `file://${fromValue}`);
- }
- /**
- * @param {{[k: number]: number}} columnWidths
- * @return {number}
- */
- function getMessageWidth(columnWidths) {
- if (!process.stdout.isTTY) {
- return columnWidths[3];
- }
- const availableWidth = process.stdout.columns < 80 ? 80 : process.stdout.columns;
- const fullWidth = Object.values(columnWidths).reduce((a, b) => a + b);
- // If there is no reason to wrap the text, we won't align the last column to the right
- if (availableWidth > fullWidth + MARGIN_WIDTHS) {
- return columnWidths[3];
- }
- return availableWidth - (fullWidth - columnWidths[3] + MARGIN_WIDTHS);
- }
- /**
- * @param {import('stylelint').Warning[]} messages
- * @param {string} source
- * @param {string} cwd
- * @return {string}
- */
- function formatter(messages, source, cwd) {
- if (!messages.length) return '';
- const orderedMessages = [...messages].sort((a, b) => {
- // positionless first
- if (!a.line && b.line) return -1;
- // positionless first
- if (a.line && !b.line) return 1;
- if (a.line < b.line) return -1;
- if (a.line > b.line) return 1;
- if (a.column < b.column) return -1;
- if (a.column > b.column) return 1;
- return 0;
- });
- /**
- * Create a list of column widths, needed to calculate
- * the size of the message column and if needed wrap it.
- * @type {{[k: string]: number}}
- */
- const columnWidths = { 0: 1, 1: 1, 2: 1, 3: 1, 4: 1 };
- /**
- * @param {[string, string, string, string, string]} columns
- * @return {[string, string, string, string, string]}
- */
- function calculateWidths(columns) {
- for (const [key, value] of Object.entries(columns)) {
- const normalisedValue = value ? value.toString() : value;
- columnWidths[key] = Math.max(columnWidths[key], stringWidth(normalisedValue));
- }
- return columns;
- }
- let output = '\n';
- if (source) {
- output += `${logFrom(source, cwd)}\n`;
- }
- /**
- * @param {import('stylelint').Warning} message
- * @return {string}
- */
- function formatMessageText(message) {
- let result = message.text;
- result = result
- // Remove all control characters (newline, tab and etc)
- .replace(/[\u0001-\u001A]+/g, ' ') // eslint-disable-line no-control-regex
- .replace(/\.$/, '');
- const ruleString = ` (${message.rule})`;
- if (result.endsWith(ruleString)) {
- result = result.slice(0, result.lastIndexOf(ruleString));
- }
- return result;
- }
- const cleanedMessages = orderedMessages.map((message) => {
- const { line, column, severity } = message;
- /**
- * @type {[string, string, string, string, string]}
- */
- const row = [
- line ? line.toString() : '',
- column ? column.toString() : '',
- symbols[severity] ? levelColors[severity](symbols[severity]) : severity,
- formatMessageText(message),
- dim(message.rule || ''),
- ];
- calculateWidths(row);
- return row;
- });
- output += table
- .table(cleanedMessages, {
- border: table.getBorderCharacters('void'),
- columns: {
- 0: { alignment: 'right', width: columnWidths[0], paddingRight: 0 },
- 1: { alignment: 'left', width: columnWidths[1] },
- 2: { alignment: 'center', width: columnWidths[2] },
- 3: {
- alignment: 'left',
- width: getMessageWidth(columnWidths),
- wrapWord: getMessageWidth(columnWidths) > 1,
- },
- 4: { alignment: 'left', width: columnWidths[4], paddingRight: 0 },
- },
- drawHorizontalLine: () => false,
- })
- .split('\n')
- .map(
- /**
- * @param {string} el
- * @returns {string}
- */
- (el) => el.replace(/(\d+)\s+(\d+)/, (_m, p1, p2) => dim(`${p1}:${p2}`)),
- )
- .join('\n');
- return output;
- }
- /**
- * @type {import('stylelint').Formatter}
- */
- module.exports = function (results, returnValue) {
- let output = invalidOptionsFormatter(results);
- output += deprecationsFormatter(results);
- output = results.reduce((accum, result) => {
- // Treat parseErrors as warnings
- if (result.parseErrors) {
- for (const error of result.parseErrors)
- result.warnings.push({
- line: error.line,
- column: error.column,
- rule: error.stylelintType,
- severity: 'error',
- text: `${error.text} (${error.stylelintType})`,
- });
- }
- accum += formatter(
- result.warnings,
- result.source || '',
- (returnValue && returnValue.cwd) || process.cwd(),
- );
- return accum;
- }, output);
- // Ensure consistent padding
- output = output.trim();
- if (output !== '') {
- output = `\n${output}\n\n`;
- }
- return output;
- };
|