├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .nvmrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src ├── apply-sourcemap.js ├── index.js ├── reporter-factory.js └── writer.js └── test ├── fixtures ├── basic.css ├── ignore ├── invalid.css ├── original-a.css └── original-b.css ├── index.spec.js ├── reporter-factory.spec.js ├── sourcemap.spec.js └── writer.spec.js /.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 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | node: true, 5 | es6: true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 8 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | package-lock.json 4 | 5 | # Logs 6 | logs 7 | *.log 8 | 9 | # Temporary files 10 | tmp 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | 17 | # node-waf configuration 18 | .lock-wscript 19 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.17.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - 16 5 | - 14 6 | - 12 7 | cache: npm 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Unreleased 2 | 3 | * Upgrade node to v20.17.0 4 | 5 | ### 14.1.2: 2023-10-22 6 | 7 | * Remove unknown build badge 8 | * Update postcss to 8.4.31 9 | * Update stylelint to 15.11.0 10 | * Update stylelint-config-standard to 34.0.0 11 | 12 | ### 14.1.1: 2023-02-15 13 | 14 | * Change to less strict peer dependency of stylelint 15 | 16 | ### 14.1.0: 2023-02-13 17 | 18 | * Update stylelint to 15.1.0, #5 (kudos to @movahhedi for reminding me) 19 | 20 | ### 14.0.9: 2022-11-26 21 | 22 | * Update dependencies, replace some #6 (thanks @onigoetz) 23 | * Update eslint-plugin-jest 24 | * Add .eslintrc.js 25 | 26 | ### 14.0.8: 2022-11-14 27 | 28 | * Upgrade peerDependencies to stylelint 14.15.0 29 | 30 | ### 14.0.7: 2022-11-14 31 | 32 | * Remove deprecated tape test 33 | * Update stylelint to 14.15.0 (Fixes #5) 34 | * Add .nvmrc 35 | 36 | ### 14.0.6: 2022-02-24 37 | 38 | * Allow Node 12+ #2 39 | * Remove dependency to through2 #3 40 | 41 | ### 14.0.5: 2021-11-02 42 | 43 | * Update devDependency of stylelint to 14.0.1 44 | 45 | ### 14.0.4: 2021-10-28 46 | 47 | * Release npm package @ronilaukkarinen/gulp-stylelint 48 | 49 | ### 14.0.3: 2021-10-28 50 | 51 | * Update package.json 52 | * Update only needed packages 53 | 54 | ### 14.0.2: 2021-10-28 55 | 56 | * Update stylelint and dependencies 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gulp-stylelint 2 | 3 | [![NPM version](http://img.shields.io/npm/v/@ronilaukkarinen/gulp-stylelint.svg)](https://www.npmjs.org/package/@ronilaukkarinen/gulp-stylelint) 4 | 5 | **This is a fork of [olegskl/gulp-stylelint](https://github.com/olegskl/gulp-stylelint) which seems to have been sleeping for a long time. This fork aims to have always up to date [stylelint](https://github.com/stylelint/stylelint) version.** 6 | 7 | A [Gulp](http://gulpjs.com/) plugin that runs [stylelint](https://github.com/stylelint/stylelint) results through a list of reporters. 8 | 9 | ## Installation 10 | 11 | ```bash 12 | npm install stylelint @ronilaukkarinen/gulp-stylelint --save-dev 13 | ``` 14 | 15 | ## Quick start 16 | 17 | 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. 18 | 19 | ```js 20 | const gulp = require('gulp'); 21 | 22 | gulp.task('lint-css', function lintCssTask() { 23 | const gulpStylelint = require('gulp-stylelint'); 24 | 25 | return gulp 26 | .src('src/**/*.css') 27 | .pipe(gulpStylelint({ 28 | reporters: [ 29 | {formatter: 'string', console: true} 30 | ] 31 | })); 32 | }); 33 | ``` 34 | 35 | ## Formatters 36 | 37 | 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. 38 | 39 | - `"string"` (same as `gulpStylelint.formatters.string`) – bundled with stylelint 40 | - `"verbose"` (same as `gulpStylelint.formatters.verbose`) – bundled with stylelint 41 | - `"json"` (same as `gulpStylelint.formatters.json`) – bundled with stylelint 42 | - [stylelint-checkstyle-formatter](https://github.com/davidtheclark/stylelint-checkstyle-formatter) – requires installation 43 | 44 | ## Options 45 | 46 | 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: 47 | 48 | ```js 49 | const gulp = require('gulp'); 50 | 51 | gulp.task('lint-css', function lintCssTask() { 52 | const gulpStylelint = require('gulp-stylelint'); 53 | const myStylelintFormatter = require('my-stylelint-formatter'); 54 | 55 | return gulp 56 | .src('src/**/*.css') 57 | .pipe(gulpStylelint({ 58 | failAfterError: true, 59 | reportOutputDir: 'reports/lint', 60 | reporters: [ 61 | {formatter: 'verbose', console: true}, 62 | {formatter: 'json', save: 'report.json'}, 63 | {formatter: myStylelintFormatter, save: 'my-custom-report.txt'} 64 | ], 65 | debug: true 66 | })); 67 | }); 68 | ``` 69 | 70 | #### `failAfterError` 71 | 72 | When set to `true`, the process will end with non-zero error code if any error-level warnings were raised. Defaults to `true`. 73 | 74 | #### `reportOutputDir` 75 | 76 | Base directory for lint results written to filesystem. Defaults to current working directory. 77 | 78 | #### `reporters` 79 | 80 | List of reporter configuration objects (see below). Defaults to an empty array. 81 | 82 | ```js 83 | { 84 | // stylelint results formatter (required): 85 | // - pass a function for imported, custom or exposed formatters 86 | // - pass a string ("string", "verbose", "json") for formatters bundled with stylelint 87 | formatter: myFormatter, 88 | 89 | // save the formatted result to a file (optional): 90 | save: 'text-report.txt', 91 | 92 | // log the formatted result to console (optional): 93 | console: true 94 | } 95 | ``` 96 | 97 | #### `debug` 98 | 99 | When set to `true`, the error handler will print an error stack trace. Defaults to `false`. 100 | 101 | ## Autofix 102 | 103 | 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`. 104 | 105 | ```js 106 | const gulp = require('gulp'); 107 | 108 | gulp.task('fix-css', function fixCssTask() { 109 | const gulpStylelint = require('gulp-stylelint'); 110 | 111 | return gulp 112 | .src('src/**/*.css') 113 | .pipe(gulpStylelint({ 114 | fix: true 115 | })) 116 | .pipe(gulp.dest('src')); 117 | }); 118 | ``` 119 | 120 | ## License 121 | 122 | [MIT License](http://opensource.org/licenses/MIT) 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ronilaukkarinen/gulp-stylelint", 3 | "version": "14.1.2", 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/ronilaukkarinen/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/ronilaukkarinen/gulp-stylelint/issues" 29 | }, 30 | "homepage": "https://github.com/ronilaukkarinen/gulp-stylelint", 31 | "engines": { 32 | "node": "^12.20.0 || ^14.13.1 || >=15.14.0 || >=16.0.0" 33 | }, 34 | "peerDependencies": { 35 | "stylelint": "10 - 15" 36 | }, 37 | "dependencies": { 38 | "@jridgewell/trace-mapping": "^0.3.17", 39 | "ansi-colors": "^4.1.3", 40 | "fancy-log": "^2.0.0", 41 | "plugin-error": "^2.0.1" 42 | }, 43 | "devDependencies": { 44 | "eslint": "^8.34.0", 45 | "eslint-config-stylelint": "^18.0.0", 46 | "eslint-plugin-eslint-comments": "^3.2.0", 47 | "eslint-plugin-jest": "^27.2.1", 48 | "eslint-plugin-node": "^11.1.0", 49 | "eslint-plugin-regexp": "^1.12.0", 50 | "gulp": "^4.0.2", 51 | "gulp-clean-css": "^4.3.0", 52 | "gulp-concat": "^2.6.1", 53 | "gulp-rename": "^2.0.0", 54 | "gulp-sourcemaps": "^3.0.0", 55 | "jest": "^29.4.2", 56 | "postcss": "^8.4.31", 57 | "sinon": "^15.0.1", 58 | "stylelint": "^15.11.0", 59 | "stylelint-config-standard": "^34.0.0", 60 | "tape": "^5.6.3" 61 | }, 62 | "eslintConfig": { 63 | "extends": [ 64 | "stylelint" 65 | ], 66 | "parserOptions": { 67 | "ecmaVersion": 2017 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/apply-sourcemap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {TraceMap, originalPositionFor} = require('@jridgewell/trace-mapping'); 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 = new TraceMap(sourceMap); 14 | 15 | lintResult.results = lintResult.results.reduce((memo, result) => { 16 | if (result.warnings.length) { 17 | result.warnings.forEach(warning => { 18 | const origPos = originalPositionFor(sourceMapConsumer, 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 | return lintResult; 41 | } 42 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const PluginError = require('plugin-error'); 4 | const { Transform } = require("stream"); 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 | const stream = new Transform({ 179 | objectMode: true, 180 | highWaterMark: 16, 181 | transform: onFile, 182 | flush: onStreamEnd 183 | }); 184 | 185 | return stream.resume(); 186 | }; 187 | 188 | /** 189 | * Formatters bundled with stylelint by default. 190 | * 191 | * User may want to see the list of available formatters, 192 | * proxy them or pass them as functions instead of strings. 193 | * 194 | * @see https://github.com/olegskl/gulp-stylelint/issues/3#issuecomment-197025044 195 | * @type {Object} 196 | */ 197 | module.exports.formatters = formatters; 198 | -------------------------------------------------------------------------------- /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/writer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ansiColors = require('ansi-colors'); 4 | const fs = require('fs'); 5 | const path = require('path'); 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, ansiColors.unstyle(text), fsWriteFileError => { 23 | if (fsWriteFileError) { 24 | reject(fsWriteFileError); 25 | } else { 26 | resolve(); 27 | } 28 | }); 29 | } 30 | }); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /test/fixtures/basic.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: #f00; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/ignore: -------------------------------------------------------------------------------- 1 | invalid.css 2 | -------------------------------------------------------------------------------- /test/fixtures/invalid.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: #FFF; 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | 98 | // eslint-disable-next-line no-console 99 | console.log(lintResult.map(r => ({ 100 | source: r.source, 101 | warnings: r.warnings[0] 102 | }))); 103 | 104 | t.equal( 105 | lintResult[0].warnings[0].line, 106 | 2, 107 | 'original-a.css has an error on line 2' 108 | ); 109 | t.equal( 110 | lintResult[0].warnings[0].column, 111 | 14, 112 | 'original-a.css has an error on column 14' 113 | ); 114 | t.equal( 115 | lintResult[1].warnings[0].line, 116 | 2, 117 | 'original-b.css has an error on line 2' 118 | ); 119 | t.equal( 120 | lintResult[1].warnings[0].column, 121 | 15, 122 | 'original-b.css has an error on column 15' 123 | ); 124 | } 125 | }] 126 | })) 127 | .on('error', () => t.pass('error has been emitted correctly')); 128 | }); 129 | -------------------------------------------------------------------------------- /test/writer.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ansiColors = require('ansi-colors'); 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 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(ansiColors.blue('footext'), 'foo.txt') 74 | .then(() => { 75 | t.equal( 76 | fs.readFileSync(reportFilePath, 'utf8'), 77 | 'footext', 78 | '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 | --------------------------------------------------------------------------------