const stylelint = require('stylelint'); const { showInvisibles, generateDifferences, } = require('prettier-linter-helpers'); const {INSERT, DELETE, REPLACE} = generateDifferences; let prettier; const ruleName = 'prettier/prettier'; const messages = stylelint.utils.ruleMessages(ruleName, { insert: (code) => `Insert "${showInvisibles(code)}"`, delete: (code) => `Delete "${showInvisibles(code)}"`, replace: (deleteCode, insertCode) => `Replace "${showInvisibles(deleteCode)}" with "${showInvisibles( insertCode )}"`, }); module.exports = stylelint.createPlugin( ruleName, (expectation, options, context) => { return (root, result) => { const validOptions = stylelint.utils.validateOptions(result, ruleName, { actual: expectation, }); if (!validOptions) { return; } // Stylelint can handle css-in-js, in which it formats object literals. // We don't want to run these extracts of JS through prettier if (root.source.lang === 'object-literal') { return; } const stylelintPrettierOptions = omitStylelintSpecificOptions(options); if (!prettier) { // Prettier is expensive to load, so only load it if needed. prettier = require('prettier'); } // Default to '' if a filepath was not provided. // This mimics eslint's behaviour const filepath = root.source.input.file || ''; const source = root.source.input.css; const prettierRcOptions = prettier.resolveConfig.sync(filepath, { editorconfig: true, }); const prettierFileInfo = prettier.getFileInfo.sync(filepath, { resolveConfig: true, ignorePath: '.prettierignore', }); // Skip if file is ignored using a .prettierignore file if (prettierFileInfo.ignored) { return; } const initialOptions = {}; // If no filepath was provided then assume the CSS parser // This is added to the options first, so that // prettierRcOptions and stylelintPrettierOptions can still override // the parser. if (filepath == '') { initialOptions.parser = 'css'; } // Stylelint supports languages that may contain multiple types of style // languages, thus we can't rely on guessing the parser based off the // filename. // In all of the following cases stylelint extracts a part of a file to // be formatted and there exists a prettier parser for the whole file. // If you're interested in prettier you'll want a fully formatted file so // you're about to run prettier over the whole file anyway. // Therefore running prettier over just the style section is wasteful, so // skip it. const parserBlockList = [ 'babel', 'flow', 'typescript', 'vue', 'markdown', 'html', 'angular', // .component.html files 'svelte', ]; if (parserBlockList.indexOf(prettierFileInfo.inferredParser) !== -1) { return; } const prettierOptions = Object.assign( {}, initialOptions, prettierRcOptions, stylelintPrettierOptions, {filepath} ); try { prettierSource = prettier.format(source, prettierOptions); } catch (err) { if (!(err instanceof SyntaxError)) { throw err; } let message = 'Parsing error: ' + err.message; // Prettier's message contains a codeframe style preview of the // invalid code and the line/column at which the error occurred. // ESLint shows those pieces of information elsewhere already so // remove them from the message if (err.codeFrame) { message = message.replace(`\n${err.codeFrame}`, ''); } if (err.loc) { message = message.replace(/ \(\d+:\d+\)$/, ''); } stylelint.utils.report({ ruleName, result, message, node: root, index: getIndexFromLoc(source, err.loc.start), }); return; } // Everything is the same. Nothing to do here; if (source === prettierSource) { return; } // Otherwise let's generate some differences const differences = generateDifferences(source, prettierSource); const report = (message, index) => { return stylelint.utils.report({ ruleName, result, message, node: root, index, }); }; if (context.fix) { // Fixes must be processed in reverse order, as an early delete shall // change the modification offsets for anything after it const rawData = differences.reverse().reduce((rawData, difference) => { let insertText = ''; let deleteText = ''; switch (difference.operation) { case INSERT: insertText = difference.insertText; break; case DELETE: deleteText = difference.deleteText; break; case REPLACE: insertText = difference.insertText; deleteText = difference.deleteText; break; } return ( rawData.substring(0, difference.offset) + insertText + rawData.substring(difference.offset + deleteText.length) ); }, root.source.input.css); // If root.source.syntax exists then it means stylelint had to use // postcss-syntax to guess the postcss parser that it should use based // upon the input filename. // In that case we want to use the parser that postcss-syntax picked. // Otherwise use the syntax parser that was provided in the options const syntax = root.source.syntax || result.opts.syntax; const newRoot = syntax.parse(rawData); // For reasons I don't really understand, when the original input does // not have a trailing newline, newRoot generates a trailing newline but // it does not get included in the output. // Cleaning the root raws (to remove any existing whitespace), then // adding the final new line into the root raws seems to fix this root.removeAll(); root.cleanRaws(); root.append(newRoot); // Use the EOL whitespace from the rawData, as it could be \n or \r\n const trailingWhitespace = rawData.match(/[\s\uFEFF\xA0]+$/); if (trailingWhitespace) { root.raws.after = trailingWhitespace[0]; } return; } // Report in the the order the differences appear in the content differences.forEach((difference) => { switch (difference.operation) { case INSERT: report(messages.insert(difference.insertText), difference.offset); break; case DELETE: report(messages.delete(difference.deleteText), difference.offset); break; case REPLACE: report( messages.replace(difference.deleteText, difference.insertText), difference.offset ); break; } }); }; } ); function omitStylelintSpecificOptions(options) { const prettierOptions = Object.assign({}, options); delete prettierOptions.message; delete prettierOptions.severity; return prettierOptions; } function getIndexFromLoc(source, {line, column}) { function nthIndex(str, searchValue, n) { let i = -1; while (n-- && i++ < str.length) { i = str.indexOf(searchValue, i); if (i < 0) { break; } } return i; } if (line === 1) { return column - 1; } return nthIndex(source, '\n', line - 1) + column; } module.exports.ruleName = ruleName; module.exports.messages = messages;