UNPKG

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