UNPKG

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