├── test ├── fixtures │ ├── ignore │ ├── basic.css │ ├── invalid.css │ ├── original-a.css │ └── original-b.css ├── reporter-factory.spec.js ├── writer.spec.js ├── index.spec.js └── sourcemap.spec.js ├── .gitattributes ├── .travis.yml ├── .gitignore ├── .editorconfig ├── src ├── writer.js ├── reporter-factory.js ├── apply-sourcemap.js └── index.js ├── LICENSE ├── package.json └── README.md /test/fixtures/ignore: -------------------------------------------------------------------------------- 1 | invalid.css 2 | -------------------------------------------------------------------------------- /test/fixtures/basic.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: #f00; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/invalid.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: #FFF; 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /test/fixtures/original-a.css: -------------------------------------------------------------------------------- 1 | .red { 2 | color: red !important; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/original-b.css: -------------------------------------------------------------------------------- 1 | .blue { 2 | color: blue !important; 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "12" 5 | - "10.12.0" 6 | cache: npm 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Temporary files 9 | tmp 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # node-waf configuration 17 | .lock-wscript 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | indent_size = 2 10 | indent_style = space 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | -------------------------------------------------------------------------------- /src/writer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const stripAnsi = require('strip-ansi'); 6 | 7 | /** 8 | * Creates the output folder and writes formatted text to a file. 9 | * @param {String} text - Text to write (may be color-coded). 10 | * @param {String} dest - Destination path relative to destRoot. 11 | * @param {String} [destRoot] - Destination root folder, defaults to cwd. 12 | * @return {Promise} Resolved when folder is created and file is written. 13 | */ 14 | module.exports = function writer(text, dest, destRoot = process.cwd()) { 15 | const fullpath = path.resolve(destRoot, dest); 16 | 17 | return new Promise((resolve, reject) => { 18 | fs.mkdir(path.dirname(fullpath), { recursive: true }, mkdirpError => { 19 | if (mkdirpError) { 20 | reject(mkdirpError); 21 | } else { 22 | fs.writeFile(fullpath, stripAnsi(text), fsWriteFileError => { 23 | if (fsWriteFileError) { 24 | reject(fsWriteFileError); 25 | } else { 26 | resolve(); 27 | } 28 | }); 29 | } 30 | }); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Oleg Sklyanchuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/reporter-factory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fancyLog = require('fancy-log'); 4 | const {formatters} = require('stylelint'); 5 | 6 | const writer = require('./writer'); 7 | 8 | /** 9 | * Creates a reporter from the given config. 10 | * @param {Object} [config] - Reporter config. 11 | * @param {Object} [options] - Plugin options. 12 | * @return {Function} Reporter. 13 | */ 14 | module.exports = function reporterFactory(config = {}, options = {}) { 15 | 16 | /** 17 | * Formatter for stylelint results. 18 | * 19 | * User has a choice of passing a custom formatter function, 20 | * or a name of formatter bundled with stylelint by default. 21 | * 22 | * @type {Function} 23 | */ 24 | const formatter = typeof config.formatter === 'string' ? 25 | formatters[config.formatter] : 26 | config.formatter; 27 | 28 | /** 29 | * Reporter. 30 | * @param {[Object]} results - Array of stylelint results. 31 | * @return {Promise} Resolved when writer and logger are done. 32 | */ 33 | return function reporter(results) { 34 | 35 | /** 36 | * Async tasks performed by the reporter. 37 | * @type [Promise] 38 | */ 39 | const asyncTasks = []; 40 | 41 | /** 42 | * Formatter output. 43 | * @type String 44 | */ 45 | const formattedText = formatter(results); 46 | 47 | if (config.console && formattedText.trim()) { 48 | asyncTasks.push( 49 | fancyLog.info(`\n${formattedText}\n`) 50 | ); 51 | } 52 | 53 | if (config.save) { 54 | asyncTasks.push( 55 | writer(formattedText, config.save, options.reportOutputDir) 56 | ); 57 | } 58 | 59 | return Promise.all(asyncTasks); 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /src/apply-sourcemap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {SourceMapConsumer} = require('source-map'); 4 | 5 | /** 6 | * Applies a sourcemap to Stylelint result. 7 | * 8 | * @param {Object} lintResult - Result of StyleLint. 9 | * @param {Object} sourceMap - Sourcemap object. 10 | * @return {Object} Rewritten Stylelint result. 11 | */ 12 | module.exports = async function applySourcemap(lintResult, sourceMap) { 13 | const sourceMapConsumer = await new SourceMapConsumer(sourceMap); 14 | 15 | lintResult.results = lintResult.results.reduce((memo, result) => { 16 | if (result.warnings.length) { 17 | result.warnings.forEach(warning => { 18 | const origPos = sourceMapConsumer.originalPositionFor(warning); 19 | const sameSourceResultIndex = memo.findIndex(r => r.source === origPos.source); 20 | 21 | warning.line = origPos.line; 22 | warning.column = origPos.column; 23 | 24 | if (sameSourceResultIndex === -1) { 25 | memo.push(Object.assign({}, result, { 26 | source: origPos.source, 27 | warnings: [warning] 28 | })); 29 | } else { 30 | memo[sameSourceResultIndex].warnings.push(warning); 31 | } 32 | }); 33 | } else { 34 | memo.push(result); 35 | } 36 | 37 | return memo; 38 | }, []); 39 | 40 | // The consumer in versions ^0.7.0 of SourceMap need to be `destroy`ed after 41 | // usage, but the older don't, so we wrap it in a typeof for backwards compatibility: 42 | if (typeof sourceMapConsumer.destroy === 'function') { 43 | // Free this source map consumer's associated wasm data that is manually-managed: 44 | sourceMapConsumer.destroy(); 45 | } 46 | 47 | return lintResult; 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-stylelint", 3 | "version": "13.0.0", 4 | "description": "Gulp plugin for running Stylelint results through various reporters.", 5 | "main": "src/index.js", 6 | "files": [ 7 | "/src/*.js" 8 | ], 9 | "scripts": { 10 | "lint": "eslint \"{src,test}/**/*.js\"", 11 | "tape": "tape \"test/*.spec.js\"", 12 | "test": "npm run lint && npm run tape", 13 | "prepublishOnly": "npm test" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/olegskl/gulp-stylelint.git" 18 | }, 19 | "keywords": [ 20 | "gulpplugin", 21 | "stylelint", 22 | "postcss", 23 | "css" 24 | ], 25 | "author": "Oleg Sklyanchuk (http://olegskl.com)", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/olegskl/gulp-stylelint/issues" 29 | }, 30 | "homepage": "https://github.com/olegskl/gulp-stylelint", 31 | "engines": { 32 | "node": ">=10.12.0" 33 | }, 34 | "peerDependencies": { 35 | "stylelint": "^13.0.0" 36 | }, 37 | "dependencies": { 38 | "chalk": "^3.0.0", 39 | "fancy-log": "^1.3.3", 40 | "plugin-error": "^1.0.1", 41 | "source-map": "^0.7.3", 42 | "strip-ansi": "^6.0.0", 43 | "through2": "^3.0.1" 44 | }, 45 | "devDependencies": { 46 | "eslint": "^6.8.0", 47 | "eslint-config-stylelint": "^11.1.0", 48 | "gulp": "^4.0.2", 49 | "gulp-clean-css": "^4.2.0", 50 | "gulp-concat": "^2.6.1", 51 | "gulp-rename": "^2.0.0", 52 | "gulp-sourcemaps": "^2.6.5", 53 | "sinon": "^8.1.1", 54 | "stylelint": "^13.0.0", 55 | "tape": "^4.13.0" 56 | }, 57 | "eslintConfig": { 58 | "extends": [ 59 | "stylelint" 60 | ], 61 | "parserOptions": { 62 | "ecmaVersion": 2017 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/reporter-factory.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fancyLog = require('fancy-log'); 4 | const test = require('tape'); 5 | const {stub} = require('sinon'); 6 | 7 | const reporterFactory = require('../src/reporter-factory'); 8 | 9 | test('reporter factory should return a function', t => { 10 | t.plan(1); 11 | t.equal( 12 | typeof reporterFactory(), 13 | 'function', 14 | 'reporter factory has returned a function' 15 | ); 16 | }); 17 | 18 | test('reporter should return a promise', t => { 19 | t.plan(1); 20 | 21 | const reporter = reporterFactory({formatter() { 22 | // empty formatter 23 | }}); 24 | 25 | t.equal( 26 | typeof reporter({}).then, 27 | 'function', 28 | 'reporter is then-able' 29 | ); 30 | }); 31 | 32 | test('reporter should write to console if console param is true', t => { 33 | t.plan(1); 34 | stub(fancyLog, 'info'); 35 | const reporter = reporterFactory({ 36 | formatter() { return 'foo'; }, 37 | console: true 38 | }); 39 | 40 | reporter({}); 41 | 42 | t.true( 43 | fancyLog.info.calledWith('\nfoo\n'), 44 | 'reporter has written padded formatter output to console' 45 | ); 46 | fancyLog.info.restore(); 47 | }); 48 | 49 | test('reporter should NOT write to console if console param is false', t => { 50 | t.plan(1); 51 | stub(fancyLog, 'info'); 52 | const reporter = reporterFactory({ 53 | formatter() { return 'foo'; }, 54 | console: false 55 | }); 56 | 57 | reporter({}); 58 | 59 | t.false( 60 | fancyLog.info.called, 61 | 'reporter has NOT written anything to console' 62 | ); 63 | fancyLog.info.restore(); 64 | }); 65 | 66 | test('reporter should NOT write to console if formatter returned only whitespace', t => { 67 | t.plan(1); 68 | stub(fancyLog, 'info'); 69 | const reporter = reporterFactory({ 70 | formatter() { return ' \n'; }, 71 | console: true 72 | }); 73 | 74 | reporter({}); 75 | 76 | t.false( 77 | fancyLog.info.called, 78 | 'reporter has NOT written anything to console' 79 | ); 80 | fancyLog.info.restore(); 81 | }); 82 | 83 | test('reporter should NOT write to console by default', t => { 84 | t.plan(1); 85 | stub(fancyLog, 'info'); 86 | const reporter = reporterFactory({ 87 | formatter() { return 'foo'; } 88 | }); 89 | 90 | reporter({}); 91 | 92 | t.false( 93 | fancyLog.info.called, 94 | 'reporter has NOT written anything to console' 95 | ); 96 | fancyLog.info.restore(); 97 | }); 98 | -------------------------------------------------------------------------------- /test/writer.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chalk = require('chalk'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const test = require('tape'); 7 | const {stub} = require('sinon'); 8 | 9 | const writer = require('../src/writer'); 10 | 11 | const tmpDir = path.resolve(__dirname, '../tmp'); 12 | 13 | test('writer should write to cwd if base dir is not specified', t => { 14 | stub(process, 'cwd').returns(tmpDir); 15 | const reportFilePath = path.join(process.cwd(), 'foo.txt'); 16 | 17 | t.plan(2); 18 | 19 | writer('footext', 'foo.txt') 20 | .then(() => { 21 | t.true( 22 | fs.statSync(reportFilePath).isFile(), 23 | 'report file has been created in the current working directory' 24 | ); 25 | t.equal( 26 | fs.readFileSync(reportFilePath, 'utf8'), 27 | 'footext', 28 | 'report file has correct contents' 29 | ); 30 | }) 31 | .catch(e => t.fail(`failed to create report file: ${e.message}`)) 32 | .then(() => { 33 | process.cwd.restore(); 34 | fs.unlinkSync(reportFilePath); 35 | }); 36 | }); 37 | 38 | test('writer should write to a base folder if it is specified', t => { 39 | stub(process, 'cwd').returns(tmpDir); 40 | const reportDirPath = path.join(process.cwd(), 'foodir'); 41 | const reportSubdirPath = path.join(reportDirPath, '/subdir'); 42 | const reportFilePath = path.join(reportSubdirPath, 'foo.txt'); 43 | 44 | t.plan(2); 45 | 46 | writer('footext', 'foo.txt', 'foodir/subdir') 47 | .then(() => { 48 | t.true( 49 | fs.statSync(reportFilePath).isFile(), 50 | 'report file has been created in the specified base folder' 51 | ); 52 | t.equal( 53 | fs.readFileSync(reportFilePath, 'utf8'), 54 | 'footext', 55 | 'report file has correct contents' 56 | ); 57 | }) 58 | .catch(e => t.fail(`failed to create report file: ${e.message}`)) 59 | .then(() => { 60 | process.cwd.restore(); 61 | fs.unlinkSync(reportFilePath); 62 | fs.rmdirSync(reportSubdirPath); 63 | fs.rmdirSync(reportDirPath); 64 | }); 65 | }); 66 | 67 | test('writer should strip chalk colors from formatted output', t => { 68 | stub(process, 'cwd').returns(tmpDir); 69 | const reportFilePath = path.join(process.cwd(), 'foo.txt'); 70 | 71 | t.plan(1); 72 | 73 | writer(chalk.blue('footext'), 'foo.txt') 74 | .then(() => { 75 | t.equal( 76 | fs.readFileSync(reportFilePath, 'utf8'), 77 | 'footext', 78 | 'chalk colors have been stripped in report file' 79 | ); 80 | }) 81 | .catch(e => t.fail(`failed to create report file: ${e.message}`)) 82 | .then(() => { 83 | process.cwd.restore(); 84 | fs.unlinkSync(reportFilePath); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const gulp = require('gulp'); 5 | const gulpSourcemaps = require('gulp-sourcemaps'); 6 | const path = require('path'); 7 | const test = require('tape'); 8 | 9 | const gulpStylelint = require('../src/index'); 10 | 11 | /** 12 | * Creates a full path to the fixtures glob. 13 | * @param {String} glob - Src glob. 14 | * @return {String} Full path. 15 | */ 16 | function fixtures(glob) { 17 | return path.join(__dirname, 'fixtures', glob); 18 | } 19 | 20 | test('should not throw when no arguments are passed', t => { 21 | t.plan(1); 22 | t.doesNotThrow(gulpStylelint); 23 | }); 24 | 25 | test('should emit an error on streamed file', t => { 26 | t.plan(1); 27 | gulp 28 | .src(fixtures('basic.css'), {buffer: false}) 29 | .pipe(gulpStylelint()) 30 | .on('error', error => t.equal( 31 | error.message, 32 | 'Streaming is not supported', 33 | 'error has been emitted on streamed file' 34 | )); 35 | }); 36 | 37 | test('should NOT emit an error when configuration is set', t => { 38 | t.plan(1); 39 | gulp 40 | .src(fixtures('basic.css')) 41 | .pipe(gulpStylelint({config: {rules: []}})) 42 | .on('error', () => t.fail('error has been emitted')) 43 | .on('finish', () => t.pass('no error emitted')); 44 | }); 45 | 46 | test('should emit an error when linter complains', t => { 47 | t.plan(1); 48 | gulp 49 | .src(fixtures('invalid.css')) 50 | .pipe(gulpStylelint({config: {rules: { 51 | 'color-hex-case': 'lower' 52 | }}})) 53 | .on('error', () => t.pass('error has been emitted correctly')); 54 | }); 55 | 56 | test('should ignore file', t => { 57 | t.plan(1); 58 | gulp 59 | .src([fixtures('basic.css'), fixtures('invalid.css')]) 60 | .pipe(gulpStylelint({ 61 | config: {rules: {'color-hex-case': 'lower'}}, 62 | ignorePath: fixtures('ignore') 63 | })) 64 | .on('finish', () => t.pass('no error emitted')); 65 | }); 66 | 67 | test('should fix the file without emitting errors', t => { 68 | t.plan(2); 69 | gulp 70 | .src(fixtures('invalid.css')) 71 | .pipe(gulpSourcemaps.init()) 72 | .pipe(gulpStylelint({ 73 | fix: true, 74 | config: {rules: {'color-hex-case': 'lower'}} 75 | })) 76 | .pipe(gulp.dest(path.resolve(__dirname, '../tmp'))) 77 | .on('error', error => t.fail(`error ${error} has been emitted`)) 78 | .on('finish', () => { 79 | t.equal( 80 | fs.readFileSync(path.resolve(__dirname, '../tmp/invalid.css'), 'utf8'), 81 | '.foo {\n color: #fff;\n}\n', 82 | 'report file has fixed contents' 83 | ); 84 | t.pass('no error emitted'); 85 | }); 86 | }); 87 | 88 | test('should expose an object with stylelint formatter functions', t => { 89 | t.plan(2); 90 | t.equal(typeof gulpStylelint.formatters, 'object', 'formatters property is an object'); 91 | 92 | const formatters = Object 93 | .keys(gulpStylelint.formatters) 94 | .map(fName => gulpStylelint.formatters[fName]); 95 | 96 | t.true(formatters.every(f => typeof f === 'function'), 'all formatters are functions'); 97 | }); 98 | -------------------------------------------------------------------------------- /test/sourcemap.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const gulp = require('gulp'); 4 | const gulpCleanCss = require('gulp-clean-css'); 5 | const gulpConcat = require('gulp-concat'); 6 | const gulpRename = require('gulp-rename'); 7 | const gulpSourcemaps = require('gulp-sourcemaps'); 8 | const path = require('path'); 9 | const test = require('tape'); 10 | 11 | const gulpStylelint = require('../src/index'); 12 | 13 | /** 14 | * Creates a full path to the fixtures glob. 15 | * @param {String} glob - Src glob. 16 | * @return {String} Full path. 17 | */ 18 | function fixtures(glob) { 19 | return path.join(__dirname, 'fixtures', glob); 20 | } 21 | 22 | test('should emit no errors when stylelint rules are satisfied', t => { 23 | t.plan(1); 24 | gulp 25 | .src(fixtures('original-*.css')) 26 | .pipe(gulpSourcemaps.init()) 27 | .pipe(gulpStylelint({ 28 | config: {rules: {}} 29 | })) 30 | .on('finish', () => t.pass('no error emitted')); 31 | }); 32 | 33 | test('should apply sourcemaps correctly', t => { 34 | t.plan(6); 35 | gulp 36 | .src(fixtures('original-*.css')) 37 | .pipe(gulpSourcemaps.init()) 38 | .pipe(gulpCleanCss()) 39 | .pipe(gulpConcat('concatenated.css')) 40 | .pipe(gulpRename({prefix: 'renamed-'})) 41 | .pipe(gulpStylelint({ 42 | config: {rules: { 43 | 'declaration-no-important': true 44 | }}, 45 | reporters: [{ 46 | formatter(lintResult) { 47 | t.deepEqual( 48 | lintResult.map(r => r.source), 49 | ['original-a.css', 'original-b.css'], 50 | 'there are two files' 51 | ); 52 | t.equal( 53 | lintResult[0].warnings[0].line, 54 | 2, 55 | 'original-a.css has an error on line 2' 56 | ); 57 | t.equal( 58 | lintResult[0].warnings[0].column, 59 | 9, 60 | 'original-a.css has an error on column 9' 61 | ); 62 | t.equal( 63 | lintResult[1].warnings[0].line, 64 | 2, 65 | 'original-b.css has an error on line 2' 66 | ); 67 | t.equal( 68 | lintResult[1].warnings[0].column, 69 | 9, 70 | 'original-b.css has an error on column 9' 71 | ); 72 | } 73 | }] 74 | })) 75 | .on('error', () => t.pass('error has been emitted correctly')); 76 | }); 77 | 78 | test('should ignore empty sourcemaps', t => { 79 | t.plan(6); 80 | gulp 81 | .src(fixtures('original-*.css')) 82 | .pipe(gulpSourcemaps.init()) // empty sourcemaps here 83 | .pipe(gulpStylelint({ 84 | config: {rules: { 85 | 'declaration-no-important': true 86 | }}, 87 | reporters: [{ 88 | formatter(lintResult) { 89 | t.deepEqual( 90 | lintResult.map(r => r.source), 91 | [ 92 | path.join(__dirname, 'fixtures', 'original-a.css'), 93 | path.join(__dirname, 'fixtures', 'original-b.css') 94 | ], 95 | 'there are two files' 96 | ); 97 | t.equal( 98 | lintResult[0].warnings[0].line, 99 | 2, 100 | 'original-a.css has an error on line 2' 101 | ); 102 | t.equal( 103 | lintResult[0].warnings[0].column, 104 | 15, 105 | 'original-a.css has an error on column 15' 106 | ); 107 | t.equal( 108 | lintResult[1].warnings[0].line, 109 | 2, 110 | 'original-b.css has an error on line 2' 111 | ); 112 | t.equal( 113 | lintResult[1].warnings[0].column, 114 | 16, 115 | 'original-b.css has an error on column 16' 116 | ); 117 | } 118 | }] 119 | })) 120 | .on('error', () => t.pass('error has been emitted correctly')); 121 | }); 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gulp-stylelint 2 | 3 | [![NPM version](http://img.shields.io/npm/v/gulp-stylelint.svg)](https://www.npmjs.org/package/gulp-stylelint) 4 | [![Build Status](https://travis-ci.org/olegskl/gulp-stylelint.svg?branch=master)](https://travis-ci.org/olegskl/gulp-stylelint) 5 | [![Dependency Status](https://david-dm.org/olegskl/gulp-stylelint.svg)](https://david-dm.org/olegskl/gulp-stylelint) 6 | [![Join the chat at https://gitter.im/olegskl/gulp-stylelint](https://badges.gitter.im/olegskl/gulp-stylelint.svg)](https://gitter.im/olegskl/gulp-stylelint?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 7 | 8 | A [Gulp](http://gulpjs.com/) plugin that runs [stylelint](https://github.com/stylelint/stylelint) results through a list of reporters. 9 | 10 | ## Installation 11 | 12 | ```bash 13 | npm install stylelint gulp-stylelint --save-dev 14 | ``` 15 | 16 | ## Quick start 17 | 18 | Once you have [configured stylelint](http://stylelint.io/user-guide/configuration/) (e.g. you have a *.stylelintrc* file), start with the following code. You will find additional configuration [options](#options) below. 19 | 20 | ```js 21 | const gulp = require('gulp'); 22 | 23 | gulp.task('lint-css', function lintCssTask() { 24 | const gulpStylelint = require('gulp-stylelint'); 25 | 26 | return gulp 27 | .src('src/**/*.css') 28 | .pipe(gulpStylelint({ 29 | reporters: [ 30 | {formatter: 'string', console: true} 31 | ] 32 | })); 33 | }); 34 | ``` 35 | 36 | ## Formatters 37 | 38 | Below is the list of currently available stylelint formatters. Some of them are bundled with stylelint by default and exposed on `gulpStylelint.formatters` object. Others need to be installed. You can [write a custom formatter](http://stylelint.io/developer-guide/formatters/) to tailor the reporting to your needs. 39 | 40 | - `"string"` (same as `gulpStylelint.formatters.string`) – bundled with stylelint 41 | - `"verbose"` (same as `gulpStylelint.formatters.verbose`) – bundled with stylelint 42 | - `"json"` (same as `gulpStylelint.formatters.json`) – bundled with stylelint 43 | - [stylelint-checkstyle-formatter](https://github.com/davidtheclark/stylelint-checkstyle-formatter) – requires installation 44 | 45 | ## Options 46 | 47 | gulp-stylelint supports all [stylelint options](http://stylelint.io/user-guide/node-api/#options) except [`files`](http://stylelint.io/user-guide/node-api/#files) and [`formatter`](http://stylelint.io/user-guide/node-api/#formatter) and accepts a custom set of options listed below: 48 | 49 | ```js 50 | const gulp = require('gulp'); 51 | 52 | gulp.task('lint-css', function lintCssTask() { 53 | const gulpStylelint = require('gulp-stylelint'); 54 | const myStylelintFormatter = require('my-stylelint-formatter'); 55 | 56 | return gulp 57 | .src('src/**/*.css') 58 | .pipe(gulpStylelint({ 59 | failAfterError: true, 60 | reportOutputDir: 'reports/lint', 61 | reporters: [ 62 | {formatter: 'verbose', console: true}, 63 | {formatter: 'json', save: 'report.json'}, 64 | {formatter: myStylelintFormatter, save: 'my-custom-report.txt'} 65 | ], 66 | debug: true 67 | })); 68 | }); 69 | ``` 70 | 71 | #### `failAfterError` 72 | 73 | When set to `true`, the process will end with non-zero error code if any error-level warnings were raised. Defaults to `true`. 74 | 75 | #### `reportOutputDir` 76 | 77 | Base directory for lint results written to filesystem. Defaults to current working directory. 78 | 79 | #### `reporters` 80 | 81 | List of reporter configuration objects (see below). Defaults to an empty array. 82 | 83 | ```js 84 | { 85 | // stylelint results formatter (required): 86 | // - pass a function for imported, custom or exposed formatters 87 | // - pass a string ("string", "verbose", "json") for formatters bundled with stylelint 88 | formatter: myFormatter, 89 | 90 | // save the formatted result to a file (optional): 91 | save: 'text-report.txt', 92 | 93 | // log the formatted result to console (optional): 94 | console: true 95 | } 96 | ``` 97 | 98 | #### `debug` 99 | 100 | When set to `true`, the error handler will print an error stack trace. Defaults to `false`. 101 | 102 | ## Autofix 103 | 104 | The `fix: true` option instructs stylelint to try to fix as many issues as possible. The fixes are applied to the gulp stream. The fixed content can be saved to file using `gulp.dest`. 105 | 106 | ```js 107 | const gulp = require('gulp'); 108 | 109 | gulp.task('fix-css', function fixCssTask() { 110 | const gulpStylelint = require('gulp-stylelint'); 111 | 112 | return gulp 113 | .src('src/**/*.css') 114 | .pipe(gulpStylelint({ 115 | fix: true 116 | })) 117 | .pipe(gulp.dest('src')); 118 | }); 119 | ``` 120 | 121 | ## License 122 | 123 | [MIT License](http://opensource.org/licenses/MIT) 124 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const PluginError = require('plugin-error'); 4 | const through = require('through2'); 5 | const {formatters, lint} = require('stylelint'); 6 | 7 | const applySourcemap = require('./apply-sourcemap'); 8 | const reporterFactory = require('./reporter-factory'); 9 | 10 | /** 11 | * Name of this plugin for reporting purposes. 12 | * @type {String} 13 | */ 14 | const pluginName = 'gulp-stylelint'; 15 | 16 | /** 17 | * Stylelint results processor. 18 | * @param {Object} [options] - Plugin options. 19 | * @param {String} [options.reportOutputDir] - Common path for all reporters. 20 | * @param {[Object]} [options.reporters] - Reporter configurations. 21 | * @param {Boolean} [options.failAfterError] - If true, the process will end with non-zero error code if any error was raised. 22 | * @param {Boolean} [options.debug] - If true, error stack will be printed. 23 | * @return {Stream} Object stream usable in Gulp pipes. 24 | */ 25 | module.exports = function gulpStylelint(options) { 26 | 27 | /** 28 | * Plugin options with defaults applied. 29 | * @type Object 30 | */ 31 | const pluginOptions = Object.assign({ 32 | failAfterError: true, 33 | debug: false 34 | }, options); 35 | 36 | /** 37 | * Lint options for stylelint's `lint` function. 38 | * @type Object 39 | */ 40 | const lintOptions = Object.assign({}, options); 41 | 42 | /** 43 | * List of gulp-stylelint reporters. 44 | * @type [Function] 45 | */ 46 | const reporters = (pluginOptions.reporters || []) 47 | .map(config => reporterFactory(config, pluginOptions)); 48 | 49 | /** 50 | * List of stylelint's lint result promises. 51 | * @type [Promise] 52 | */ 53 | const lintPromiseList = []; 54 | 55 | // Remove the stylelint options that cannot be used: 56 | delete lintOptions.files; // css code will be provided by gulp instead 57 | delete lintOptions.formatter; // formatters are defined in the `reporters` option 58 | delete lintOptions.cache; // gulp caching should be used instead 59 | 60 | // Remove gulp-stylelint options so that they don't interfere with stylelint options: 61 | delete lintOptions.reportOutputDir; 62 | delete lintOptions.reporters; 63 | delete lintOptions.debug; 64 | 65 | /** 66 | * Launches linting of a given file, pushes promises to the promise list. 67 | * 68 | * Note that the files are not modified and are pushed 69 | * back to their pipes to allow usage of other plugins. 70 | * 71 | * @param {File} file - Piped file. 72 | * @param {String} encoding - File encoding. 73 | * @param {Function} done - File pipe completion callback. 74 | * @return {undefined} Nothing is returned (done callback is used instead). 75 | */ 76 | function onFile(file, encoding, done) { 77 | 78 | if (file.isNull()) { 79 | done(null, file); 80 | 81 | return; 82 | } 83 | 84 | if (file.isStream()) { 85 | this.emit('error', new PluginError(pluginName, 'Streaming is not supported')); 86 | done(); 87 | 88 | return; 89 | } 90 | 91 | const localLintOptions = Object.assign({}, lintOptions, { 92 | code: file.contents.toString(), 93 | codeFilename: file.path 94 | }); 95 | 96 | const lintPromise = lint(localLintOptions) 97 | .then(lintResult => 98 | // Checking for the presence of sourceMap.mappings 99 | // in case sourcemaps are initialized, but still empty: 100 | file.sourceMap && file.sourceMap.mappings ? 101 | applySourcemap(lintResult, file.sourceMap) : 102 | lintResult 103 | ) 104 | .then(lintResult => { 105 | if (lintOptions.fix && lintResult.output) { 106 | file.contents = Buffer.from(lintResult.output); 107 | } 108 | 109 | done(null, file); 110 | 111 | return lintResult; 112 | }) 113 | .catch(error => { 114 | done(null, file); 115 | 116 | return Promise.reject(error); 117 | }); 118 | 119 | lintPromiseList.push(lintPromise); 120 | } 121 | 122 | /** 123 | * Provides Stylelint result to reporters. 124 | * @param {[Object]} lintResults - Stylelint results. 125 | * @return {Promise} Resolved with original lint results. 126 | */ 127 | function passLintResultsThroughReporters(lintResults) { 128 | const warnings = lintResults 129 | .reduce((accumulated, res) => accumulated.concat(res.results), []); 130 | 131 | return Promise 132 | .all(reporters.map(reporter => reporter(warnings))) 133 | .then(() => lintResults); 134 | } 135 | 136 | /** 137 | * Determines if the severity of a stylelint warning is "error". 138 | * @param {Object} warning - Stylelint results warning. 139 | * @return {Boolean} True if warning's severity is "error", false otherwise. 140 | */ 141 | function isErrorSeverity(warning) { 142 | return warning.severity === 'error'; 143 | } 144 | 145 | /** 146 | * Resolves promises and provides accumulated report to reporters. 147 | * @param {Function} done - Stream completion callback. 148 | * @return {undefined} Nothing is returned (done callback is used instead). 149 | */ 150 | function onStreamEnd(done) { 151 | Promise 152 | .all(lintPromiseList) 153 | .then(passLintResultsThroughReporters) 154 | .then(lintResults => { 155 | process.nextTick(() => { 156 | // if the file was skipped, for example, by .stylelintignore, then res.results will be [] 157 | const errorCount = lintResults.filter(res => res.results.length).reduce((sum, res) => { 158 | return sum + res.results[0].warnings.filter(isErrorSeverity).length; 159 | }, 0); 160 | 161 | if (pluginOptions.failAfterError && errorCount > 0) { 162 | this.emit('error', new PluginError(pluginName, `Failed with ${errorCount} ${errorCount === 1 ? 'error' : 'errors'}`)); 163 | } 164 | 165 | done(); 166 | }); 167 | }) 168 | .catch(error => { 169 | process.nextTick(() => { 170 | this.emit('error', new PluginError(pluginName, error, { 171 | showStack: Boolean(pluginOptions.debug) 172 | })); 173 | done(); 174 | }); 175 | }); 176 | } 177 | 178 | return through.obj(onFile, onStreamEnd).resume(); 179 | }; 180 | 181 | /** 182 | * Formatters bundled with stylelint by default. 183 | * 184 | * User may want to see the list of available formatters, 185 | * proxy them or pass them as functions instead of strings. 186 | * 187 | * @see https://github.com/olegskl/gulp-stylelint/issues/3#issuecomment-197025044 188 | * @type {Object} 189 | */ 190 | module.exports.formatters = formatters; 191 | --------------------------------------------------------------------------------