UNPKG

5.86 kBJavaScriptView Raw
1'use strict';
2
3const _ = require('lodash');
4const chalk = require('chalk');
5const path = require('path');
6const stringWidth = require('string-width');
7const symbols = require('log-symbols');
8const table = require('table');
9const utils = require('postcss-reporter/lib/util');
10
11const MARGIN_WIDTHS = 9;
12
13const levelColors = {
14 info: 'blue',
15 warning: 'yellow',
16 error: 'red',
17 success: undefined,
18};
19
20/**
21 * @param {import('stylelint').StylelintResult[]} results
22 * @returns {string}
23 */
24function deprecationsFormatter(results) {
25 const allDeprecationWarnings = _.flatMap(results, 'deprecations');
26 const uniqueDeprecationWarnings = _.uniqBy(allDeprecationWarnings, 'text');
27
28 if (!uniqueDeprecationWarnings || !uniqueDeprecationWarnings.length) {
29 return '';
30 }
31
32 return uniqueDeprecationWarnings.reduce((output, warning) => {
33 output += chalk.yellow('Deprecation Warning: ');
34 output += warning.text;
35
36 if (warning.reference) {
37 output += chalk.dim(' See: ');
38 output += chalk.dim.underline(warning.reference);
39 }
40
41 return `${output}\n`;
42 }, '\n');
43}
44
45/**
46 * @param {import('stylelint').StylelintResult[]} results
47 * @return {string}
48 */
49function invalidOptionsFormatter(results) {
50 const allInvalidOptionWarnings = _.flatMap(results, (r) =>
51 r.invalidOptionWarnings.map((w) => w.text),
52 );
53 const uniqueInvalidOptionWarnings = [...new Set(allInvalidOptionWarnings)];
54
55 return uniqueInvalidOptionWarnings.reduce((output, warning) => {
56 output += chalk.red('Invalid Option: ');
57 output += warning;
58
59 return `${output}\n`;
60 }, '\n');
61}
62
63/**
64 * @param {string} fromValue
65 * @return {string}
66 */
67function logFrom(fromValue) {
68 if (fromValue.startsWith('<')) return fromValue;
69
70 return path.relative(process.cwd(), fromValue).split(path.sep).join('/');
71}
72
73/**
74 * @param {{[k: number]: number}} columnWidths
75 * @return {number}
76 */
77function getMessageWidth(columnWidths) {
78 if (!process.stdout.isTTY) {
79 return columnWidths[3];
80 }
81
82 const availableWidth = process.stdout.columns < 80 ? 80 : process.stdout.columns;
83 const fullWidth = Object.values(columnWidths).reduce((a, b) => a + b);
84
85 // If there is no reason to wrap the text, we won't align the last column to the right
86 if (availableWidth > fullWidth + MARGIN_WIDTHS) {
87 return columnWidths[3];
88 }
89
90 return availableWidth - (fullWidth - columnWidths[3] + MARGIN_WIDTHS);
91}
92
93/**
94 * @param {import('stylelint').StylelintWarning[]} messages
95 * @param {string} source
96 * @return {string}
97 */
98function formatter(messages, source) {
99 if (!messages.length) return '';
100
101 const orderedMessages = _.sortBy(
102 messages,
103 // eslint-disable-next-line no-confusing-arrow
104 (m) => (m.line ? 2 : 1), // positionless first
105 (m) => m.line,
106 (m) => m.column,
107 );
108
109 /**
110 * Create a list of column widths, needed to calculate
111 * the size of the message column and if needed wrap it.
112 * @type {{[k: string]: number}}
113 */
114 const columnWidths = { 0: 1, 1: 1, 2: 1, 3: 1, 4: 1 };
115
116 /**
117 * @param {[string, string, string, string, string]} columns
118 * @return {[string, string, string, string, string]}
119 */
120 function calculateWidths(columns) {
121 for (const [key, value] of Object.entries(columns)) {
122 const normalisedValue = value ? value.toString() : value;
123
124 columnWidths[key] = Math.max(columnWidths[key], stringWidth(normalisedValue));
125 }
126
127 return columns;
128 }
129
130 let output = '\n';
131
132 if (source) {
133 output += `${chalk.underline(logFrom(source))}\n`;
134 }
135
136 const cleanedMessages = orderedMessages.map((message) => {
137 const location = utils.getLocation(message);
138 const severity = /** @type {keyof import('log-symbols')} */ (message.severity);
139 /**
140 * @type {[string, string, string, string, string]}
141 */
142 const row = [
143 location.line ? location.line.toString() : '',
144 location.column ? location.column.toString() : '',
145 symbols[severity]
146 ? chalk[/** @type {'blue' | 'red' | 'yellow'} */ (levelColors[severity])](symbols[severity])
147 : severity,
148 message.text
149 // Remove all control characters (newline, tab and etc)
150 .replace(/[\u0001-\u001A]+/g, ' ') // eslint-disable-line no-control-regex
151 .replace(/\.$/, '')
152 // eslint-disable-next-line prefer-template
153 .replace(new RegExp(_.escapeRegExp('(' + message.rule + ')') + '$'), ''),
154 chalk.dim(message.rule || ''),
155 ];
156
157 calculateWidths(row);
158
159 return row;
160 });
161
162 output += table
163 .table(cleanedMessages, {
164 border: table.getBorderCharacters('void'),
165 columns: {
166 0: { alignment: 'right', width: columnWidths[0], paddingRight: 0 },
167 1: { alignment: 'left', width: columnWidths[1] },
168 2: { alignment: 'center', width: columnWidths[2] },
169 3: {
170 alignment: 'left',
171 width: getMessageWidth(columnWidths),
172 wrapWord: getMessageWidth(columnWidths) > 1,
173 },
174 4: { alignment: 'left', width: columnWidths[4], paddingRight: 0 },
175 },
176 drawHorizontalLine: () => false,
177 })
178 .split('\n')
179 .map(
180 /**
181 * @param {string} el
182 * @returns {string}
183 */
184 (el) => el.replace(/(\d+)\s+(\d+)/, (m, p1, p2) => chalk.dim(`${p1}:${p2}`)),
185 )
186 .join('\n');
187
188 return output;
189}
190
191/**
192 * @param {import('stylelint').StylelintResult[]} results
193 * @returns {string}
194 */
195module.exports = function (results) {
196 let output = invalidOptionsFormatter(results);
197
198 output += deprecationsFormatter(results);
199
200 output = results.reduce((output, result) => {
201 // Treat parseErrors as warnings
202 if (result.parseErrors) {
203 result.parseErrors.forEach((error) =>
204 result.warnings.push({
205 line: error.line,
206 column: error.column,
207 rule: error.stylelintType,
208 severity: 'error',
209 text: `${error.text} (${error.stylelintType})`,
210 }),
211 );
212 }
213
214 output += formatter(result.warnings, result.source || '');
215
216 return output;
217 }, output);
218
219 // Ensure consistent padding
220 output = output.trim();
221
222 if (output !== '') {
223 output = `\n${output}\n\n`;
224 }
225
226 return output;
227};