|
| 1 | +import stylelint from 'stylelint'; |
| 2 | +import {showInvisibles, generateDifferences} from 'prettier-linter-helpers'; |
| 3 | + |
| 4 | +const prettierPromise = import('prettier'); |
| 5 | + |
| 6 | +const {INSERT, DELETE, REPLACE} = generateDifferences; |
| 7 | + |
| 8 | +let prettier; |
| 9 | + |
| 10 | +const ruleName = 'prettier/prettier'; |
| 11 | +const messages = stylelint.utils.ruleMessages(ruleName, { |
| 12 | + insert: (code) => `Insert "${showInvisibles(code)}"`, |
| 13 | + delete: (code) => `Delete "${showInvisibles(code)}"`, |
| 14 | + replace: (deleteCode, insertCode) => |
| 15 | + `Replace "${showInvisibles(deleteCode)}" with "${showInvisibles( |
| 16 | + insertCode |
| 17 | + )}"`, |
| 18 | +}); |
| 19 | + |
| 20 | +/** @type {stylelint.Rule} */ |
| 21 | +const ruleFunction = (expectation, options, context) => { |
| 22 | + return async (root, result) => { |
| 23 | + const validOptions = stylelint.utils.validateOptions(result, ruleName, { |
| 24 | + actual: expectation, |
| 25 | + }); |
| 26 | + if (!validOptions) { |
| 27 | + return; |
| 28 | + } |
| 29 | + |
| 30 | + // Stylelint can handle css-in-js, in which it formats object literals. |
| 31 | + // We don't want to run these extracts of JS through prettier |
| 32 | + if (root.source.lang === 'object-literal') { |
| 33 | + return; |
| 34 | + } |
| 35 | + |
| 36 | + const stylelintPrettierOptions = omitStylelintSpecificOptions(options); |
| 37 | + |
| 38 | + if (!prettier) { |
| 39 | + // Prettier is expensive to load, so only load it if needed. |
| 40 | + prettier = await prettierPromise; |
| 41 | + } |
| 42 | + |
| 43 | + // Default to '<input>' if a filepath was not provided. |
| 44 | + // This mimics eslint's behaviour |
| 45 | + const filepath = root.source.input.file || '<input>'; |
| 46 | + const source = root.source.input.css; |
| 47 | + |
| 48 | + const prettierRcOptions = await prettier.resolveConfig(filepath, { |
| 49 | + editorconfig: true, |
| 50 | + }); |
| 51 | + |
| 52 | + const prettierFileInfo = await prettier.getFileInfo(filepath, { |
| 53 | + resolveConfig: true, |
| 54 | + plugins: |
| 55 | + prettierRcOptions?.plugins ?? stylelintPrettierOptions?.plugins ?? [], |
| 56 | + ignorePath: '.prettierignore', |
| 57 | + }); |
| 58 | + |
| 59 | + // Skip if file is ignored using a .prettierignore file |
| 60 | + if (prettierFileInfo.ignored) { |
| 61 | + return; |
| 62 | + } |
| 63 | + |
| 64 | + const initialOptions = {}; |
| 65 | + |
| 66 | + // If no filepath was provided then assume the CSS parser |
| 67 | + // This is added to the options first, so that |
| 68 | + // prettierRcOptions and stylelintPrettierOptions can still override |
| 69 | + // the parser. |
| 70 | + if (filepath == '<input>') { |
| 71 | + initialOptions.parser = 'css'; |
| 72 | + } |
| 73 | + |
| 74 | + // Stylelint supports languages that may contain multiple types of style |
| 75 | + // languages, thus we can't rely on guessing the parser based off the |
| 76 | + // filename. |
| 77 | + |
| 78 | + // In all of the following cases stylelint extracts a part of a file to |
| 79 | + // be formatted and there exists a prettier parser for the whole file. |
| 80 | + // If you're interested in prettier you'll want a fully formatted file so |
| 81 | + // you're about to run prettier over the whole file anyway. |
| 82 | + // Therefore running prettier over just the style section is wasteful, so |
| 83 | + // skip it. |
| 84 | + |
| 85 | + const parserBlockList = [ |
| 86 | + 'babel', |
| 87 | + 'flow', |
| 88 | + 'typescript', |
| 89 | + 'vue', |
| 90 | + 'markdown', |
| 91 | + 'html', |
| 92 | + 'angular', // .component.html files |
| 93 | + 'svelte', |
| 94 | + 'astro', |
| 95 | + ]; |
| 96 | + if (parserBlockList.indexOf(prettierFileInfo.inferredParser) !== -1) { |
| 97 | + return; |
| 98 | + } |
| 99 | + |
| 100 | + const prettierOptions = Object.assign( |
| 101 | + {}, |
| 102 | + initialOptions, |
| 103 | + prettierRcOptions, |
| 104 | + stylelintPrettierOptions, |
| 105 | + {filepath} |
| 106 | + ); |
| 107 | + |
| 108 | + let prettierSource; |
| 109 | + |
| 110 | + try { |
| 111 | + prettierSource = await prettier.format(source, prettierOptions); |
| 112 | + } catch (err) { |
| 113 | + if (!(err instanceof SyntaxError)) { |
| 114 | + throw err; |
| 115 | + } |
| 116 | + |
| 117 | + let message = 'Parsing error: ' + err.message; |
| 118 | + |
| 119 | + // Prettier's message contains a codeframe style preview of the |
| 120 | + // invalid code and the line/column at which the error occurred. |
| 121 | + // ESLint shows those pieces of information elsewhere already so |
| 122 | + // remove them from the message |
| 123 | + if (err.codeFrame) { |
| 124 | + message = message.replace(`\n${err.codeFrame}`, ''); |
| 125 | + } |
| 126 | + if (err.loc) { |
| 127 | + message = message.replace(/ \(\d+:\d+\)$/, ''); |
| 128 | + } |
| 129 | + |
| 130 | + stylelint.utils.report({ |
| 131 | + ruleName, |
| 132 | + result, |
| 133 | + message, |
| 134 | + node: root, |
| 135 | + index: getIndexFromLoc(source, err.loc.start), |
| 136 | + }); |
| 137 | + |
| 138 | + return; |
| 139 | + } |
| 140 | + |
| 141 | + // Everything is the same. Nothing to do here; |
| 142 | + if (source === prettierSource) { |
| 143 | + return; |
| 144 | + } |
| 145 | + |
| 146 | + // Otherwise let's generate some differences |
| 147 | + |
| 148 | + const differences = generateDifferences(source, prettierSource); |
| 149 | + |
| 150 | + const report = (message, index, endIndex) => { |
| 151 | + return stylelint.utils.report({ |
| 152 | + ruleName, |
| 153 | + result, |
| 154 | + message, |
| 155 | + node: root, |
| 156 | + index, |
| 157 | + endIndex, |
| 158 | + }); |
| 159 | + }; |
| 160 | + |
| 161 | + if (context.fix) { |
| 162 | + // Fixes must be processed in reverse order, as an early delete shall |
| 163 | + // change the modification offsets for anything after it |
| 164 | + const rawData = differences.reverse().reduce((rawData, difference) => { |
| 165 | + let insertText = ''; |
| 166 | + let deleteText = ''; |
| 167 | + switch (difference.operation) { |
| 168 | + case INSERT: |
| 169 | + insertText = difference.insertText; |
| 170 | + break; |
| 171 | + case DELETE: |
| 172 | + deleteText = difference.deleteText; |
| 173 | + break; |
| 174 | + case REPLACE: |
| 175 | + insertText = difference.insertText; |
| 176 | + deleteText = difference.deleteText; |
| 177 | + break; |
| 178 | + } |
| 179 | + |
| 180 | + return ( |
| 181 | + rawData.substring(0, difference.offset) + |
| 182 | + insertText + |
| 183 | + rawData.substring(difference.offset + deleteText.length) |
| 184 | + ); |
| 185 | + }, root.source.input.css); |
| 186 | + |
| 187 | + // If root.source.syntax exists then it means stylelint had to use |
| 188 | + // postcss-syntax to guess the postcss parser that it should use based |
| 189 | + // upon the input filename. |
| 190 | + // In that case we want to use the parser that postcss-syntax picked. |
| 191 | + // Otherwise use the syntax parser that was provided in the options |
| 192 | + const syntax = root.source.syntax || result.opts.syntax; |
| 193 | + const newRoot = syntax.parse(rawData); |
| 194 | + |
| 195 | + // For reasons I don't really understand, when the original input does |
| 196 | + // not have a trailing newline, newRoot generates a trailing newline but |
| 197 | + // it does not get included in the output. |
| 198 | + // Cleaning the root raws (to remove any existing whitespace), then |
| 199 | + // adding the final new line into the root raws seems to fix this |
| 200 | + root.removeAll(); |
| 201 | + root.cleanRaws(); |
| 202 | + root.append(newRoot); |
| 203 | + |
| 204 | + // Use the EOL whitespace from the rawData, as it could be \n or \r\n |
| 205 | + const trailingWhitespace = rawData.match(/[\s\uFEFF\xA0]+$/); |
| 206 | + if (trailingWhitespace) { |
| 207 | + root.raws.after = trailingWhitespace[0]; |
| 208 | + } |
| 209 | + return; |
| 210 | + } |
| 211 | + |
| 212 | + // Report in the order the differences appear in the content |
| 213 | + differences.forEach((difference) => { |
| 214 | + const {offset, deleteText = ''} = difference; |
| 215 | + switch (difference.operation) { |
| 216 | + case INSERT: |
| 217 | + report( |
| 218 | + messages.insert(difference.insertText), |
| 219 | + offset, |
| 220 | + offset + deleteText.length |
| 221 | + ); |
| 222 | + break; |
| 223 | + case DELETE: |
| 224 | + report( |
| 225 | + messages.delete(difference.deleteText), |
| 226 | + difference.offset, |
| 227 | + offset + deleteText.length |
| 228 | + ); |
| 229 | + break; |
| 230 | + case REPLACE: |
| 231 | + report( |
| 232 | + messages.replace(difference.deleteText, difference.insertText), |
| 233 | + difference.offset, |
| 234 | + offset + deleteText.length |
| 235 | + ); |
| 236 | + break; |
| 237 | + } |
| 238 | + }); |
| 239 | + }; |
| 240 | +}; |
| 241 | + |
| 242 | +ruleFunction.ruleName = ruleName; |
| 243 | +ruleFunction.messages = messages; |
| 244 | + |
| 245 | +export default stylelint.createPlugin(ruleName, ruleFunction); |
| 246 | + |
| 247 | +function omitStylelintSpecificOptions(options) { |
| 248 | + const prettierOptions = Object.assign({}, options); |
| 249 | + delete prettierOptions.message; |
| 250 | + delete prettierOptions.severity; |
| 251 | + return prettierOptions; |
| 252 | +} |
| 253 | + |
| 254 | +function getIndexFromLoc(source, {line, column}) { |
| 255 | + function nthIndex(str, searchValue, n) { |
| 256 | + let i = -1; |
| 257 | + while (n-- && i++ < str.length) { |
| 258 | + i = str.indexOf(searchValue, i); |
| 259 | + if (i < 0) { |
| 260 | + break; |
| 261 | + } |
| 262 | + } |
| 263 | + return i; |
| 264 | + } |
| 265 | + |
| 266 | + if (line === 1) { |
| 267 | + return column - 1; |
| 268 | + } |
| 269 | + |
| 270 | + return nthIndex(source, '\n', line - 1) + column; |
| 271 | +} |
0 commit comments