├── .nvmrc ├── .ruby-version ├── .gitignore ├── test ├── fixtures │ ├── invalid.scss │ ├── invalid-error.scss │ ├── default.yml │ ├── valid.scss │ ├── invalid-default.yml │ └── file with spaces.scss ├── reporters.js └── main.js ├── Gemfile ├── .travis.yml ├── Gemfile.lock ├── src ├── checkstyle.js ├── reporters.js ├── index.js ├── command.js └── scss-lint.js ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v8.3.0 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.0.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules -------------------------------------------------------------------------------- /test/fixtures/invalid.scss: -------------------------------------------------------------------------------- 1 | #xx { 2 | .fail {} 3 | } -------------------------------------------------------------------------------- /test/fixtures/invalid-error.scss: -------------------------------------------------------------------------------- 1 | #xx { 2 | .fail { 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/default.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | Indentation: 3 | enabled: true 4 | width: 4 -------------------------------------------------------------------------------- /test/fixtures/valid.scss: -------------------------------------------------------------------------------- 1 | .xx { 2 | .fail { 3 | margin-left: 10px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/invalid-default.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | Indentation 3 | enabled: true 4 | width: 4 -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'scss_lint' 3 | gem 'scss_lint_reporter_checkstyle' 4 | -------------------------------------------------------------------------------- /test/fixtures/file with spaces.scss: -------------------------------------------------------------------------------- 1 | .xx { 2 | .fail { 3 | margin-left: 10px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: gem install bundler; bundle install; 2 | language: node_js 3 | node_js: 4 | - "8" 5 | - "9" 6 | - "10" 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | rake (12.0.0) 5 | sass (3.4.25) 6 | scss_lint (0.54.0) 7 | rake (>= 0.9, < 13) 8 | sass (~> 3.4.20) 9 | scss_lint_reporter_checkstyle (0.2.0) 10 | 11 | PLATFORMS 12 | ruby 13 | 14 | DEPENDENCIES 15 | scss_lint 16 | scss_lint_reporter_checkstyle 17 | 18 | BUNDLED WITH 19 | 1.13.1 20 | -------------------------------------------------------------------------------- /src/checkstyle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pd = require('pretty-data').pd, 4 | xml2js = require('xml2js').parseString; 5 | 6 | exports.toJSON = function (report, cb) { 7 | var obj = {}; 8 | var xmlReport = pd.xml(report); 9 | var error = []; 10 | 11 | xml2js(xmlReport, function(err, report) { 12 | report.checkstyle.file = report.checkstyle.file || []; 13 | 14 | report.checkstyle.file.forEach(function(file) { 15 | obj[file.$.name] = []; 16 | 17 | file.error.forEach(function(error) { 18 | error.$.linter = error.$.source; 19 | error.$.reason = error.$.message; 20 | 21 | obj[file.$.name].push(error.$); 22 | }); 23 | }); 24 | 25 | cb([obj, xmlReport]); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-scss-lint", 3 | "description": "Validate `.scss` files with `scss-lint`", 4 | "version": "1.0.0", 5 | "homepage": "http://github.com/juanfran/gulp-scss-lint", 6 | "repository": "git://github.com/juanfran/gulp-scss-lint.git", 7 | "main": "./src/index.js", 8 | "author": { 9 | "name": "Juanfran Alcántara" 10 | }, 11 | "keywords": [ 12 | "gulpplugin", 13 | "scss-lint", 14 | "scsslint", 15 | "sass-lint", 16 | "scss", 17 | "lint", 18 | "gulp" 19 | ], 20 | "engines": { 21 | "node": ">= 0.10" 22 | }, 23 | "license": "MIT", 24 | "scripts": { 25 | "test": "./node_modules/.bin/mocha" 26 | }, 27 | "devDependencies": { 28 | "chai": "~4.1.2", 29 | "mocha": "~5.2.0", 30 | "proxyquire": "^2.0.1", 31 | "sinon": "^6.1.5" 32 | }, 33 | "dependencies": { 34 | "bluebird": "^3.3.5", 35 | "chalk": "^2.4.1", 36 | "dargs": "~6.0.0", 37 | "event-stream": "3.3.4", 38 | "fancy-log": "^1.3.2", 39 | "plugin-error": "^1.0.1", 40 | "pretty-data": "^0.40.0", 41 | "slash": "^2.0.0", 42 | "vinyl": "^2.2.0", 43 | "vinyl-fs": "^3.0.3", 44 | "xml2js": "^0.4.16" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/reporters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var es = require('event-stream'), 4 | chalk = require('chalk'), 5 | PluginError = require('plugin-error'), 6 | fancyLog = require('fancy-log'); 7 | 8 | exports.failReporter = function (severity) { 9 | return es.map(function(file, cb) { 10 | var error; 11 | 12 | if (!file.scsslint.success) { 13 | if (!severity || severity === 'E' && file.scsslint.errors > 0) { 14 | error = new PluginError('gulp-scss-lint', { 15 | message: 'ScssLint failed for: ' + file.relative, 16 | showStack: false 17 | }); 18 | } 19 | } 20 | 21 | cb(error, file); 22 | }); 23 | }; 24 | 25 | exports.defaultReporter = function (file) { 26 | if (!file.scsslint.success) { 27 | fancyLog(chalk.cyan(file.scsslint.issues.length) + ' issues found in ' + chalk.magenta(file.path)); 28 | 29 | file.scsslint.issues.forEach(function (issue) { 30 | var severity = issue.severity === 'warning' ? chalk.yellow(' [W] ') : chalk.red(' [E] '); 31 | var linter = issue.linter ? (issue.linter + ': ') : ''; 32 | var logMsg = 33 | chalk.cyan(file.relative) + ':' + chalk.magenta(issue.line) + severity + chalk.green(linter) + issue.reason; 34 | 35 | fancyLog(logMsg); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var es = require('event-stream'), 4 | readline = require('readline'), 5 | PluginError = require('plugin-error'), 6 | reporters = require('./reporters'), 7 | scssLint = require('./scss-lint'), 8 | Readable = require('stream').Readable; 9 | 10 | var PLUGIN_NAME = 'gulp-scss-lint'; 11 | 12 | var gulpScssLint = function (options) { 13 | options = options || {}; 14 | 15 | options.format = 'JSON'; 16 | 17 | if (options.reporterOutputFormat === 'Checkstyle') { 18 | options.format = 'Checkstyle'; 19 | options.require = 'scss_lint_reporter_checkstyle'; 20 | } 21 | 22 | if (options.exclude) { 23 | throw new PluginError(PLUGIN_NAME, "You must use gulp src to exclude"); 24 | } 25 | 26 | var lint = function(stream, files) { 27 | scssLint(stream, files, options) 28 | .then(function() { 29 | if (!options.endless) { 30 | stream.emit('end'); 31 | } 32 | }, function(e) { 33 | var existingError = new Error(e); 34 | var err = new PluginError(PLUGIN_NAME, existingError); 35 | 36 | stream.emit('error', err); 37 | stream.emit('end'); 38 | }) 39 | .done(function(e) {}); 40 | }; 41 | 42 | var getStream = function() { 43 | var files = []; 44 | 45 | var writeStream = function(currentFile){ 46 | if (options.endless) { 47 | lint(stream, [currentFile]); 48 | } else { 49 | files.push(currentFile); 50 | } 51 | }; 52 | 53 | var endStream = function() { 54 | if (options.endless) { 55 | return; 56 | } 57 | 58 | if (!files.length) { 59 | stream.emit('end'); 60 | return; 61 | } 62 | 63 | lint(stream, files); 64 | }; 65 | 66 | var stream = es.through(writeStream, endStream); 67 | 68 | return stream; 69 | }; 70 | 71 | var getNewStream = function() { 72 | var stream = new Readable({objectMode: true}); 73 | stream._read = function () {}; 74 | 75 | lint(stream, [options.src]); 76 | 77 | return stream; 78 | }; 79 | 80 | if (options.src) { 81 | return getNewStream(); 82 | } 83 | 84 | return getStream(); 85 | }; 86 | 87 | gulpScssLint.failReporter = reporters.failReporter; 88 | gulpScssLint.defaultReporter = reporters.defaultReporter; 89 | 90 | module.exports = gulpScssLint; 91 | -------------------------------------------------------------------------------- /src/command.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Promise = require('bluebird'); 4 | var checkstyle = require('./checkstyle'); 5 | var dargs = require('dargs'); 6 | var child_process = require('child_process'); 7 | 8 | var scssLintCodes = { 9 | '64': 'Command line usage error', 10 | '66': 'Input file did not exist or was not readable', 11 | '69': 'You need to have the scss_lint_reporter_checkstyle gem installed', 12 | '70': 'Internal software error', 13 | '78': 'Configuration error', 14 | '127': 'You need to have Ruby and scss-lint gem installed' 15 | }; 16 | 17 | function generateCommand(filePaths, options) { 18 | var commandParts = ['scss-lint'], 19 | excludes = ['bundleExec', 20 | 'filePipeOutput', 21 | 'reporterOutput', 22 | 'endlessReporter', 23 | 'src', 24 | 'shell', 25 | 'reporterOutputFormat', 26 | 'customReport', 27 | 'maxBuffer', 28 | 'endless', 29 | 'verbose', 30 | 'sync']; 31 | 32 | if (options.bundleExec) { 33 | commandParts.unshift('bundle', 'exec'); 34 | excludes.push('bundleExec'); 35 | } 36 | 37 | var optionsArgs = dargs(options, {excludes: excludes}); 38 | 39 | return commandParts.concat(filePaths, optionsArgs); 40 | } 41 | 42 | function execCommand(command, options) { 43 | return new Promise(function(resolve, reject) { 44 | var commandOptions = { 45 | env: process.env, 46 | cwd: process.cwd(), 47 | maxBuffer: options.maxBuffer || 300 * 1024, 48 | shell: options.shell 49 | }; 50 | 51 | if (options.sync || options.endless) { 52 | var commandResult = child_process.execFileSync(command[0], command.slice(1)); 53 | var error = null; 54 | 55 | if (commandResult.status) { 56 | error = {code: commandResult.status}; 57 | } 58 | 59 | resolve({error: error, report: commandResult.stdout}); 60 | } else { 61 | child_process.execFile(command[0], command.slice(1), commandOptions, function(error, report) { 62 | resolve({error: error, report: report}); 63 | }); 64 | } 65 | }); 66 | } 67 | 68 | function configFileReadError(report, options) { 69 | var re = new RegExp('No such file or directory(.*)' + options.config, 'g'); 70 | return re.test(report); 71 | } 72 | 73 | function execLintCommand(command, options) { 74 | return new Promise(function(resolve, reject) { 75 | execCommand(command, options).then(function(result) { 76 | var error = result.error; 77 | var report = result.report; 78 | 79 | if (error && error.code !== 1 && error.code !== 2 && error.code !== 65) { 80 | if (scssLintCodes[error.code]) { 81 | if (error.code === 66 && configFileReadError(report, options)) { 82 | reject('Config file did not exist or was not readable'); 83 | } else { 84 | reject(scssLintCodes[error.code]); 85 | } 86 | } else if (error.code) { 87 | reject('Error code ' + error.code + '\n' + error); 88 | } else { 89 | reject(error); 90 | } 91 | } else if (error && error.code === 1 && report.length === 0) { 92 | reject('Error code ' + error.code + '\n' + error); 93 | } else { 94 | if (options.format === 'JSON'){ 95 | resolve([JSON.parse(report)]); 96 | } else { 97 | checkstyle.toJSON(report, resolve); 98 | } 99 | } 100 | }); 101 | }); 102 | } 103 | 104 | module.exports = function(filePaths, options) { 105 | var command = generateCommand(filePaths, options); 106 | 107 | if (options.verbose) { 108 | console.log(command.join(' ')); 109 | } 110 | 111 | return execLintCommand(command, options); 112 | }; 113 | -------------------------------------------------------------------------------- /src/scss-lint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Promise = require('bluebird'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var Vinyl = require('vinyl'); 7 | var vinylFs = require('vinyl-fs'); 8 | var es = require('event-stream'); 9 | var slash = require('slash'); 10 | 11 | var lintCommand = require('./command'); 12 | var reporters = require('./reporters'); 13 | 14 | function getRelativePath(filePath) { 15 | return slash(path.relative(process.cwd(), filePath)); 16 | } 17 | 18 | function getFilePaths(files) { 19 | return files.map(function (file) { 20 | return getRelativePath(file.path); 21 | }); 22 | } 23 | 24 | function defaultLintResult() { 25 | return { 26 | success: true, 27 | errors: 0, 28 | warnings: 0, 29 | issues: [] 30 | }; 31 | } 32 | 33 | function reportLint(stream, files, options, lintReport, xmlReport) { 34 | var report = {}; 35 | 36 | // normalize scss-lint urls 37 | Object.keys(lintReport).forEach(function(key) { 38 | var newKey = slash(key); 39 | report[newKey] = lintReport[key]; 40 | }); 41 | 42 | if (options.reporterOutput || options.endlessReporter) { 43 | var output = null; 44 | var reporterOutput = null; 45 | 46 | if (xmlReport) { 47 | output = xmlReport; 48 | } else { 49 | output = JSON.stringify(report); 50 | } 51 | 52 | if (options.reporterOutput) { 53 | reporterOutput = options.reporterOutput || ''; 54 | } else if(options.endlessReporter) { 55 | reporterOutput = ''; 56 | 57 | if (typeof options.endlessReporter === 'string' || options.endlessReporter instanceof String) { 58 | reporterOutput = options.endlessReporter; 59 | } 60 | 61 | reporterOutput = path.join(reporterOutput, 'report-' + path.basename(files[0].path)); 62 | 63 | if (xmlReport) reporterOutput += '.xml'; 64 | else reporterOutput += '.json'; 65 | } 66 | 67 | fs.writeFileSync(reporterOutput, output); 68 | } 69 | 70 | var fileReport; 71 | var lintResult = {}; 72 | 73 | for (var i = 0; i < files.length; i++) { 74 | lintResult = defaultLintResult(); 75 | 76 | //relative or absolute path 77 | fileReport = report[slash(files[i].path)]; 78 | 79 | if (!fileReport) { 80 | fileReport = report[getRelativePath(files[i].path)]; 81 | } 82 | 83 | if (fileReport) { 84 | lintResult.success = false; 85 | 86 | fileReport.forEach(function (issue) { 87 | var severity = issue.severity === 'warning' ? 'W' : 'E'; 88 | 89 | if (severity === 'W') { 90 | lintResult.warnings++; 91 | } else { 92 | lintResult.errors++; 93 | } 94 | 95 | lintResult.issues.push(issue); 96 | }); 97 | } 98 | 99 | files[i].scsslint = lintResult; 100 | 101 | if (options.customReport) { 102 | options.customReport(files[i], stream); 103 | } else { 104 | reporters.defaultReporter(files[i]); 105 | } 106 | 107 | if (!options.filePipeOutput) { 108 | if (options.src) { 109 | stream.push(files[i]); 110 | } else { 111 | stream.emit('data', files[i]); 112 | } 113 | } 114 | } 115 | 116 | //TODO: endless support 117 | if (options.filePipeOutput) { 118 | var contentFile = ""; 119 | 120 | if (xmlReport) { 121 | contentFile = xmlReport; 122 | } else { 123 | contentFile = JSON.stringify(report); 124 | } 125 | 126 | var pipeFile = new Vinyl({ 127 | cwd: files[0].cwd, 128 | base: files[0].base, 129 | path: path.join(files[0].base, options.filePipeOutput), 130 | contents: new Buffer(contentFile) 131 | }); 132 | 133 | pipeFile.scsslint = lintResult; 134 | 135 | if (options.src) { 136 | stream.push(files[i]); 137 | } else { 138 | stream.emit('data', pipeFile); 139 | } 140 | } 141 | } 142 | 143 | function getVinylFiles(paths) { 144 | return new Promise(function(resolve, reject){ 145 | var files = []; 146 | 147 | var stream = es.through(function(currentFile) { 148 | files.push(currentFile); 149 | }, function() { 150 | resolve(files); 151 | }); 152 | 153 | vinylFs.src(paths).pipe(stream); 154 | }); 155 | } 156 | 157 | module.exports = function(stream, files, options) { 158 | return new Promise(function(resolve, reject){ 159 | var filesPaths = []; 160 | 161 | if (options.src) { 162 | filesPaths = options.src; 163 | } else { 164 | filesPaths = getFilePaths(files); 165 | } 166 | 167 | lintCommand(filesPaths, options) 168 | .spread(function(report, xmlReport) { 169 | if (options.src) { 170 | var paths = Object.keys(report); 171 | 172 | if (paths.length) { 173 | getVinylFiles(paths).then(function(vinylFiles) { 174 | reportLint(stream, vinylFiles, options, report, xmlReport); 175 | resolve(); 176 | }); 177 | } else { 178 | getVinylFiles(files).then(function(vinylFiles) { 179 | reportLint(stream, vinylFiles, options, report, xmlReport); 180 | resolve(); 181 | }); 182 | } 183 | } else { 184 | try { 185 | reportLint(stream, files, options, report, xmlReport); 186 | } catch(err) { 187 | // if the user run scss-lint from node instead of gulp, stream.emit('data', null); becomes syncronous and this will handle the failReporter #58 188 | reject(err); 189 | } 190 | 191 | resolve(); 192 | } 193 | }) 194 | .catch(function (e) { 195 | reject(e); 196 | }); 197 | }); 198 | }; 199 | -------------------------------------------------------------------------------- /test/reporters.js: -------------------------------------------------------------------------------- 1 | var proxyquire = require('proxyquire'); 2 | var chalk = require('chalk'); 3 | var Vinyl = require('vinyl') 4 | var chai = require('chai'); 5 | var sinon = require('sinon'); 6 | var expect = chai.expect; 7 | var fs = require('fs'); 8 | 9 | var getFixtureFile = function (path) { 10 | return new Vinyl({ 11 | path: './test/fixtures/' + path, 12 | cwd: './test/', 13 | base: './test/fixtures/', 14 | contents: fs.readFileSync('./test/fixtures/' + path) 15 | }); 16 | } 17 | 18 | var fakeFile = getFixtureFile('invalid.scss'); 19 | 20 | var getReporters = function (logMock) { 21 | return proxyquire('../src/reporters', { 22 | "fancy-log": logMock 23 | }); 24 | } 25 | 26 | describe('reporters', function() { 27 | it('fail reporter, success true', function (done) { 28 | var fileCount = 0; 29 | var error = false; 30 | 31 | fakeFile.scsslint = {}; 32 | fakeFile.scsslint.success = true; 33 | fakeFile.scsslint.issues = []; 34 | 35 | var log = sinon.spy(); 36 | var failReporter = getReporters(log).failReporter(); 37 | 38 | failReporter 39 | .on('data', function (file) { 40 | fileCount++; 41 | expect(file.relative).to.be.equal('invalid.scss'); 42 | }) 43 | .on('error', function () { 44 | error = true; 45 | }) 46 | .once('end', function () { 47 | expect(fileCount).to.be.equal(1); 48 | expect(error).to.be.false; 49 | done(); 50 | }); 51 | 52 | failReporter.write(fakeFile); 53 | failReporter.emit('end'); 54 | }); 55 | 56 | it('fail reporter, success false', function (done) { 57 | var error = false; 58 | 59 | fakeFile.scsslint = {}; 60 | fakeFile.scsslint.success = false; 61 | fakeFile.scsslint.issues = []; 62 | 63 | var log = sinon.spy(); 64 | var failReporter = getReporters(log).failReporter(); 65 | 66 | failReporter 67 | .on('error', function (err) { 68 | expect(err.message).to.be.equal('ScssLint failed for: invalid.scss'); 69 | error = true; 70 | }) 71 | .on('end', function () { 72 | expect(error).to.be.true; 73 | done(); 74 | }); 75 | 76 | failReporter.write(fakeFile); 77 | failReporter.emit('end'); 78 | }); 79 | 80 | describe('fail reporter, only errors', function () { 81 | it('the scss has errors', function (done) { 82 | fakeFile.scsslint = {}; 83 | fakeFile.scsslint.success = false; 84 | fakeFile.scsslint.errors = 1; 85 | fakeFile.scsslint.issues = []; 86 | 87 | var error = false; 88 | var log = sinon.spy(); 89 | var failReporter = getReporters(log).failReporter("E"); 90 | 91 | failReporter 92 | .on('error', function (err) { 93 | expect(err.message).to.be.equal('ScssLint failed for: invalid.scss'); 94 | error = true; 95 | }) 96 | .once('end', function () { 97 | expect(error).to.be.true; 98 | done(); 99 | }); 100 | 101 | failReporter.write(fakeFile); 102 | failReporter.emit('end'); 103 | }); 104 | 105 | it('the scss does not have errors', function (done) { 106 | fakeFile.scsslint = {}; 107 | fakeFile.scsslint.success = false; 108 | fakeFile.scsslint.errors = 0; 109 | fakeFile.scsslint.issues = []; 110 | 111 | var fileCount = 0; 112 | var error = false; 113 | var log = sinon.spy(); 114 | var failReporter = getReporters(log).failReporter("E"); 115 | 116 | failReporter 117 | .on('data', function (file) { 118 | fileCount++; 119 | expect(file.relative).to.be.equal('invalid.scss'); 120 | }) 121 | .on('error', function () { 122 | error = true; 123 | }) 124 | .once('end', function () { 125 | expect(fileCount).to.be.equal(1); 126 | expect(error).to.be.false; 127 | done(); 128 | }); 129 | 130 | failReporter.write(fakeFile); 131 | failReporter.emit('end'); 132 | }); 133 | }); 134 | 135 | it('default reporter, success true', function () { 136 | fakeFile.scsslint = {}; 137 | fakeFile.scsslint.success = true; 138 | fakeFile.scsslint.issues = []; 139 | 140 | var log = sinon.spy(); 141 | var defaultReporter = getReporters(log).defaultReporter; 142 | 143 | expect(log.called).to.be.false; 144 | }); 145 | 146 | it('default reporter, success false', function () { 147 | fakeFile.scsslint = {}; 148 | fakeFile.scsslint.success = false; 149 | fakeFile.scsslint.issues = [ 150 | {"severity": "warning", 151 | "line": 10, 152 | "linter": "some linter", 153 | "reason": "some reasone"}, 154 | {"severity": "error", 155 | "line": 13, 156 | "reason": "some reasone 2"} 157 | ]; 158 | 159 | var log = sinon.spy(); 160 | var defaultReporter = getReporters(log).defaultReporter; 161 | 162 | defaultReporter(fakeFile); 163 | 164 | var firstCall = log.withArgs(chalk.cyan(fakeFile.scsslint.issues.length) + ' issues found in ' + chalk.magenta(fakeFile.path)).calledOnce; 165 | 166 | var secondCall = log.withArgs(chalk.cyan(fakeFile.relative) + ':' + chalk.magenta(fakeFile.scsslint.issues[0].line) + chalk.yellow(' [W] ') + chalk.green(fakeFile.scsslint.issues[0].linter + ': ') + fakeFile.scsslint.issues[0].reason).calledOnce; 167 | 168 | 169 | var thirdCall = log.withArgs(chalk.cyan(fakeFile.relative) + ':' + chalk.magenta(fakeFile.scsslint.issues[1].line) + chalk.red(' [E] ') + fakeFile.scsslint.issues[1].reason).calledOnce; 170 | 171 | expect(firstCall).to.be.ok; 172 | expect(secondCall).to.be.ok; 173 | expect(thirdCall).to.be.ok; 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gulp-scss-lint 2 | [![Build Status](https://travis-ci.org/juanfran/gulp-scss-lint.svg?branch=master)](https://travis-ci.org/juanfran/gulp-scss-lint) 3 | > Lint your `.scss` files 4 | 5 | ## Install 6 | 7 | ```shell 8 | npm install gulp-scss-lint --save-dev 9 | ``` 10 | 11 | This plugin requires Ruby and [scss-lint](https://github.com/brigade/scss-lint) 12 | ```shell 13 | gem install scss_lint 14 | ``` 15 | 16 | ## Usage 17 | 18 | `gulpfile.js` 19 | ```js 20 | var scsslint = require('gulp-scss-lint'); 21 | 22 | gulp.task('scss-lint', function() { 23 | return gulp.src('/scss/*.scss') 24 | .pipe(scsslint()); 25 | }); 26 | ``` 27 | 28 | ## Api 29 | 30 | #### config 31 | 32 | - Type: `String` 33 | - Default: [default scss-lint config file](https://github.com/brigade/scss-lint/blob/master/config/default.yml). 34 | 35 | ```js 36 | scsslint({ 37 | 'config': 'lint.yml' 38 | }); 39 | ``` 40 | 41 | #### bundleExec 42 | 43 | - Type: `Boolean` 44 | - Default: `false` 45 | 46 | If your gem is installed via [bundler](http://bundler.io), then set this option to `true` 47 | 48 | ```js 49 | scsslint({ 50 | 'bundleExec': true 51 | }); 52 | ``` 53 | 54 | #### reporterOutput 55 | 56 | - Type: `String` 57 | - Default: `null` 58 | 59 | If you want to save the report to a file then set reporterOutput with a file name 60 | 61 | ```js 62 | scsslint({ 63 | 'reporterOutput': 'scssReport.json' 64 | }); 65 | ``` 66 | 67 | #### reporterOutputFormat 68 | 69 | - Type: `String` 70 | - Default: `JSON` 71 | - Values: `JSON` or `Checkstyle` 72 | 73 | ```js 74 | gulp.src(['**/*.scss']) 75 | .pipe(scsslint({ 76 | 'reporterOutputFormat': 'Checkstyle', 77 | })) 78 | ``` 79 | 80 | #### filePipeOutput 81 | 82 | - Type: `String` 83 | - Default: `null` 84 | 85 | If you want the pipe return a report file instead of the `.scss` file then set filePipeOutput with a filename 86 | 87 | ```js 88 | //xml 89 | gulp.src(['**/*.scss']) 90 | .pipe(scsslint({ 91 | 'reporterOutputFormat': 'Checkstyle', 92 | 'filePipeOutput': 'scssReport.xml' 93 | })) 94 | .pipe(gulp.dest('./reports')) 95 | 96 | //json 97 | gulp.src(['**/*.scss']) 98 | .pipe(scsslint({ 99 | 'filePipeOutput': 'scssReport.json' 100 | })) 101 | .pipe(gulp.dest('./reports')) 102 | ``` 103 | 104 | #### maxBuffer 105 | - Type: Number or Boolean 106 | - Default: 300 * 1024 107 | 108 | Set maxBuffer for the child_process.exec process. If you get a `maxBuffer exceeded` error, set it with a higher number. maxBuffer specifies the largest amount of data allowed on stdout or stderr. 109 | 110 | ```js 111 | gulp.src(['**/*.scss']) 112 | .pipe(scsslint({ 113 | 'maxBuffer': 307200 114 | })) 115 | .pipe(gulp.dest('./reports')) 116 | ``` 117 | 118 | #### endless 119 | 120 | - Type: Boolean 121 | - Default: false 122 | 123 | If you use gulp-watch set endless to true. 124 | 125 | #### sync 126 | 127 | - Type: Boolean 128 | - Default: sync 129 | 130 | `scss-lint` will be executed in sequence. 131 | 132 | #### verbose 133 | 134 | - Type: Boolean 135 | - Default: false 136 | 137 | If you want to see the executed scss-lint command for debugging purposes, set this to true. 138 | 139 | ## Glob pattern without gulp.src 140 | ```js 141 | var scsslint = require('gulp-scss-lint'); 142 | 143 | gulp.task('scss-lint', function() { 144 | return scsslint({ 145 | shell: 'bash', // your shell must support glob 146 | src: '**/*.scss' 147 | }); 148 | }); 149 | ``` 150 | 151 | 152 | ## Excluding 153 | 154 | To exclude files you should use the gulp.src ignore format '!filePath'' 155 | 156 | ```js 157 | gulp.src(['/scss/*.scss', '!/scss/vendor/**/*.scss']) 158 | .pipe(scsslint({'config': 'lint.yml'})); 159 | ``` 160 | 161 | Or you should use [gulp-filter](https://github.com/sindresorhus/gulp-filter) 162 | 163 | ```js 164 | var scsslint = require('gulp-scss-lint'); 165 | var gulpFilter = require('gulp-filter'); 166 | 167 | gulp.task('scss-lint', function() { 168 | var scssFilter = gulpFilter('/scss/vendor/**/*.scss'); 169 | 170 | return gulp.src('/scss/*.scss') 171 | .pipe(scssFilter) 172 | .pipe(scsslint()) 173 | .pipe(scssFilter.restore()); 174 | }); 175 | 176 | ``` 177 | 178 | ## Lint only modified files 179 | You should use [gulp-cached](https://github.com/wearefractal/gulp-cached) 180 | 181 | In this example, without the gulp-cached plugin, every time you save a `.scss` file the scss-lint plugin will check all your files. In case you have gulp-cached plugin, it will only check the modified files. 182 | 183 | ```js 184 | var scsslint = require('gulp-scss-lint'); 185 | var cache = require('gulp-cached'); 186 | 187 | gulp.task('scss-lint', function() { 188 | return gulp.src('/scss/*.scss') 189 | .pipe(cache('scsslint')) 190 | .pipe(scsslint()); 191 | }); 192 | 193 | gulp.task('watch', function() { 194 | gulp.watch('/scss/*.scss', ['scss-lint']); 195 | }); 196 | ``` 197 | 198 | ## Results 199 | 200 | Adds the following properties to the file object: 201 | 202 | ```js 203 | file.scsslint = { 204 | 'success': false, 205 | 'errors': 0, 206 | 'warnings': 1, 207 | 'issues': [ 208 | { 209 | 'line': 123, 210 | 'column': 10, 211 | 'severity': 'warning', // or `error` 212 | 'reason': 'a description of the error' 213 | } 214 | ] 215 | }; 216 | ``` 217 | 218 | The issues have the same parameters that [scss-lint](https://github.com/brigade/scss-lint#checkstyle) 219 | 220 | ## Custom reporter 221 | 222 | You can replace the default console log by a custom output with `customReport`. customReport function will be called for each file that includes the lint results [See result params](#results) 223 | 224 | ```js 225 | var scsslint = require('gulp-scss-lint'); 226 | 227 | var myCustomReporter = function(file) { 228 | if (!file.scsslint.success) { 229 | gutil.log(file.scsslint.issues.length + ' issues found in ' + file.path); 230 | } 231 | }; 232 | 233 | gulp.task('scss-lint', function() { 234 | return gulp.src('/scss/*.scss') 235 | .pipe(scsslint({ 236 | customReport: myCustomReporter 237 | })) 238 | }); 239 | ``` 240 | 241 | You can even throw an exception 242 | 243 | ```js 244 | var scsslint = require('gulp-scss-lint'); 245 | 246 | var myCustomReporter = function(file, stream) { 247 | if (!file.scsslint.success) { 248 | stream.emit('error', new gutil.PluginError("scss-lint", "some error")); 249 | } 250 | }; 251 | 252 | gulp.task('scss-lint', function() { 253 | return gulp.src('/scss/*.scss') 254 | .pipe(scsslint({ 255 | customReport: myCustomReporter 256 | })) 257 | }); 258 | ``` 259 | 260 | ## Default reporter 261 | 262 | This is an example from the default reporter output 263 | 264 | ```shell 265 | [20:55:10] 3 issues found in test/fixtures/invalid.scss 266 | [20:55:10] test/fixtures/invalid.scss:1 [W] IdSelector: Avoid using id selectors 267 | [20:55:10] test/fixtures/invalid.scss:2 [W] Indentation: Line should be indented 2 spaces, but was indented 0 spaces 268 | [20:55:10] test/fixtures/invalid.scss:2 [W] EmptyRule: Empty rule 269 | ``` 270 | 271 | ## Fail reporter 272 | 273 | If you want the task to fail when "scss-lint" was not a success then call `failReporter` after the scsslint call. 274 | 275 | This example will log the issues as usual and then fails if there is any issue. 276 | 277 | ```js 278 | var scsslint = require('gulp-scss-lint'); 279 | 280 | gulp.task('scss-lint', function() { 281 | return gulp.src('/scss/*.scss') 282 | .pipe(scsslint()) 283 | .pipe(scsslint.failReporter()) 284 | }); 285 | ``` 286 | 287 | if you just want `failReporter` to fail just with errors pass the 'E' string 288 | 289 | ```js 290 | var scsslint = require('gulp-scss-lint'); 291 | 292 | gulp.task('scss-lint', function() { 293 | return gulp.src('/scss/*.scss') 294 | .pipe(scsslint()) 295 | .pipe(scsslint.failReporter('E')) 296 | }); 297 | ``` 298 | 299 | ## Testing 300 | 301 | To test you must first have `scss-lint` installed globally using 302 | `gem install scss_lint` as well as via bundler using `bundle install`. 303 | -------------------------------------------------------------------------------- /test/main.js: -------------------------------------------------------------------------------- 1 | var pluginPath = '../src/index'; 2 | var scssLintPlugin = require(pluginPath); 3 | var chai = require('chai'); 4 | var Vinyl = require('vinyl'); 5 | var PluginError = require('plugin-error'); 6 | var fs = require('fs'); 7 | var expect = chai.expect; 8 | var proxyquire = require('proxyquire'); 9 | var sinon = require('sinon'); 10 | 11 | var getFixtureFile = function (path) { 12 | return new Vinyl({ 13 | path: './test/fixtures/' + path, 14 | cwd: './test/', 15 | base: './test/fixtures/', 16 | contents: fs.readFileSync('./test/fixtures/' + path) 17 | }); 18 | } 19 | 20 | describe('gulp-scss-lint', function() { 21 | it('invalid scss file', function(done) { 22 | var fakeFile = getFixtureFile('invalid.scss'); 23 | 24 | var stream = scssLintPlugin(); 25 | 26 | stream 27 | .on('data', function (file) { 28 | expect(file.scsslint.success).to.be.false; 29 | expect(file.scsslint.issues).to.have.length(4); 30 | expect(file.scsslint.warnings).to.equal(4); 31 | expect(file.scsslint.errors).to.equal(0); 32 | 33 | expect(file.scsslint.issues[0].line).to.exist; 34 | expect(file.scsslint.issues[0].column).to.exist; 35 | expect(file.scsslint.issues[0].length).to.exist; 36 | expect(file.scsslint.issues[0].severity).to.exist; 37 | expect(file.scsslint.issues[0].reason).to.exist; 38 | }) 39 | .once('end', function() { 40 | done(); 41 | }); 42 | 43 | stream.write(fakeFile); 44 | stream.end(); 45 | }); 46 | 47 | it('if scss-lint is not available throw an error', function(done) { 48 | var execStub = sinon.stub(); 49 | execStub.callsArgWith(3, {error: true, code: 127}); 50 | 51 | var scssLintPluginWithProxy = proxyquire(pluginPath, { 52 | 'child_process': { 53 | execFile: execStub, 54 | '@global': true 55 | } 56 | }); 57 | 58 | var fakeFile = getFixtureFile('invalid.scss'); 59 | var fileCount = 0; 60 | var stream = scssLintPluginWithProxy(); 61 | var error = false; 62 | 63 | stream 64 | .on('data', function (file) { 65 | fileCount++; 66 | }) 67 | .on('error', function (issue) { 68 | expect(issue.message).to.equal('You need to have Ruby and scss-lint gem installed'); 69 | error = true; 70 | }) 71 | .once('end', function() { 72 | expect(fileCount).to.equal(0); 73 | expect(error).to.be.true; 74 | done(); 75 | }); 76 | 77 | stream.write(fakeFile); 78 | stream.end(); 79 | }); 80 | 81 | it('if scss_lint_reporter_checkstyle is not available throw an error', function(done) { 82 | var execStub = sinon.stub(); 83 | execStub.callsArgWith(3, {error: true, code: 69}); 84 | 85 | var childProcessStub = {execFile: execStub, '@global': true}; 86 | 87 | var scssLintPluginWithProxy = proxyquire(pluginPath, {'child_process': childProcessStub}); 88 | var fakeFile = getFixtureFile('invalid.scss'); 89 | var fileCount = 0; 90 | var stream = scssLintPluginWithProxy(); 91 | var error = false; 92 | 93 | stream 94 | .on('data', function (file) { 95 | fileCount++; 96 | }) 97 | .on('error', function (issue) { 98 | expect(issue.message).to.equal('You need to have the scss_lint_reporter_checkstyle gem installed'); 99 | error = true; 100 | }) 101 | .once('end', function() { 102 | expect(fileCount).to.equal(0); 103 | expect(error).to.be.true; 104 | done(); 105 | }); 106 | 107 | stream.write(fakeFile); 108 | stream.end(); 109 | }); 110 | 111 | it('validate multi scss files', function(done) { 112 | var fakeFile = getFixtureFile('invalid.scss'); 113 | var fakeFile2 = getFixtureFile('invalid-error.scss'); 114 | 115 | var stream = scssLintPlugin(); 116 | 117 | var results = [ 118 | {'issues': 4, 'warnings': 4, 'errors': 0}, 119 | {'issues': 1, 'warnings': 0, 'errors': 1}, 120 | ]; 121 | 122 | stream 123 | .on('data', function (file) { 124 | var result = results.shift(); 125 | 126 | expect(file.scsslint.success).to.be.false; 127 | expect(file.scsslint.issues).to.have.length(result.issues); 128 | expect(file.scsslint.warnings).to.equal(result.warnings); 129 | expect(file.scsslint.errors).to.equal(result.errors); 130 | }) 131 | .once('end', function() { 132 | done(); 133 | }); 134 | 135 | stream.write(fakeFile); 136 | stream.write(fakeFile2); 137 | stream.end(); 138 | }); 139 | 140 | it('valid scss file', function(done) { 141 | var fakeFile = getFixtureFile('valid.scss'); 142 | var stream = scssLintPlugin(); 143 | 144 | stream 145 | .on('data', function (file) { 146 | expect(file.scsslint.success).to.be.true; 147 | expect(file.scsslint.issues).to.have.length(0); 148 | expect(file.scsslint.warnings).to.equal(0); 149 | expect(file.scsslint.errors).to.equal(0); 150 | }) 151 | .once('end', function() { 152 | done(); 153 | }); 154 | 155 | stream.write(fakeFile); 156 | stream.end(); 157 | }); 158 | 159 | it('default report call', function(done) { 160 | var fakeFile = getFixtureFile('invalid.scss'); 161 | var defaultReportSpy = sinon.spy(); 162 | 163 | var defaultReport = function (file) { 164 | expect(file.scsslint.success).to.be.false; 165 | expect(file.relative).to.be.equal('invalid.scss'); 166 | defaultReportSpy(); 167 | }; 168 | 169 | var scssLintPluginWithProxy = proxyquire(pluginPath, { 170 | './reporters': { 171 | "defaultReporter": defaultReport, 172 | '@global': true 173 | } 174 | }); 175 | var stream = scssLintPluginWithProxy(); 176 | 177 | stream 178 | .once('end', function() { 179 | expect(defaultReportSpy.calledOnce).to.be.true; 180 | done(); 181 | }); 182 | 183 | stream.write(fakeFile); 184 | stream.end(); 185 | }); 186 | 187 | it('custom report call', function(done) { 188 | var fakeFile = getFixtureFile('invalid.scss'); 189 | var customReportSpy = sinon.spy(); 190 | 191 | var customReport = function (file, stream) { 192 | expect(stream.end).to.exist; 193 | expect(file.scsslint.success).to.be.false; 194 | expect(file.relative).to.be.equal('invalid.scss'); 195 | customReportSpy(); 196 | }; 197 | 198 | var stream = scssLintPlugin({"customReport": customReport}); 199 | 200 | stream 201 | .once('end', function() { 202 | expect(customReportSpy.calledOnce).to.be.true; 203 | done(); 204 | }); 205 | 206 | stream.write(fakeFile); 207 | stream.end(); 208 | }); 209 | 210 | it('custom report throw an exception', function(done) { 211 | var fakeFile = getFixtureFile('invalid.scss'); 212 | var error = false; 213 | 214 | var customReport = function (file, stream) { 215 | stream.emit('error', new PluginError("scss-lint", "some error")); 216 | }; 217 | 218 | var stream = scssLintPlugin({"customReport": customReport}); 219 | 220 | stream 221 | .on('error', function (issue) { 222 | expect(issue.message).to.be.equal("some error"); 223 | error = true; 224 | }) 225 | .once('end', function() { 226 | expect(error).to.be.true; 227 | done(); 228 | }); 229 | 230 | stream.write(fakeFile); 231 | stream.end(); 232 | }); 233 | 234 | it('file pipe output', function(done) { 235 | var fakeFile = getFixtureFile('invalid.scss'); 236 | var stream = scssLintPlugin({"filePipeOutput": "test.json"}); 237 | 238 | stream 239 | .on('data', function (data) { 240 | expect(data.contents.toString('utf-8')).to.have.length.above(20); 241 | expect(data.path).to.be.equal('test/fixtures/test.json'); 242 | }) 243 | .once('end', function() { 244 | done(); 245 | }); 246 | 247 | stream.write(fakeFile); 248 | stream.end(); 249 | }); 250 | 251 | it('xml pipe output', function(done) { 252 | var fakeFile = getFixtureFile('invalid.scss'); 253 | var stream = scssLintPlugin({"filePipeOutput": "test.xml", 'reporterOutputFormat': 'Checkstyle'}); 254 | 255 | stream 256 | .on('data', function (data) { 257 | expect(data.contents.toString('utf-8')).to.have.string(''); 259 | expect(data.path).to.be.equal('test/fixtures/test.xml'); 260 | }) 261 | .once('end', function() { 262 | done(); 263 | }); 264 | 265 | stream.write(fakeFile); 266 | stream.end(); 267 | }); 268 | 269 | it('scss-lint src success', function(done) { 270 | var stream = scssLintPlugin({src: 'test/fixtures/valid.scss'}); 271 | 272 | stream 273 | .on('data', function (file) { 274 | expect(file.scsslint.success).to.be.true; 275 | }) 276 | .once('end', function() { 277 | done(); 278 | }); 279 | }); 280 | 281 | it('valid xml pipe output', function(done) { 282 | var fakeFile = getFixtureFile('valid.scss'); 283 | var stream = scssLintPlugin({"filePipeOutput": "test.xml", 'reporterOutputFormat': 'Checkstyle'}); 284 | 285 | stream 286 | .on('data', function (data) { 287 | expect(data.contents.toString('utf-8')).to.have.string(''); 289 | expect(data.path).to.be.equal('test/fixtures/test.xml'); 290 | }) 291 | .once('end', function() { 292 | done(); 293 | }); 294 | 295 | stream.write(fakeFile); 296 | stream.end(); 297 | }); 298 | 299 | it('should not fail without files', function(done) { 300 | var stream = scssLintPlugin(); 301 | var fileCount = 0; 302 | 303 | stream 304 | .on('data', function (file) { 305 | fileCount++; 306 | }) 307 | .on('error', function(error){ 308 | expect(error).to.equal(null); 309 | }) 310 | .once('end', function() { 311 | expect(fileCount).to.equal(0); 312 | done(); 313 | }); 314 | 315 | stream.end(); 316 | }); 317 | 318 | it('should not fail with files with spaces', function(done) { 319 | var fakeFile = getFixtureFile('file with spaces.scss'); 320 | var stream = scssLintPlugin(); 321 | 322 | stream 323 | .on('data', function (file) { 324 | expect(file.scsslint.success).to.be.true; 325 | expect(file.scsslint.issues).to.have.length(0); 326 | expect(file.scsslint.warnings).to.equal(0); 327 | expect(file.scsslint.errors).to.equal(0); 328 | }) 329 | .on('error', function(error){ 330 | expect(error).to.equal(null); 331 | }) 332 | .once('end', function() { 333 | done(); 334 | }); 335 | 336 | stream.write(fakeFile); 337 | stream.end(); 338 | }); 339 | 340 | it('config file param', function (done) { 341 | var fakeFile = getFixtureFile('valid.scss'); 342 | var stream = scssLintPlugin({'config': './test/fixtures/default.yml'}); 343 | 344 | stream 345 | .on('data', function (file) { 346 | expect(file.scsslint.success).to.be.false; 347 | }) 348 | .once('end', function() { 349 | done(); 350 | }); 351 | 352 | stream.write(fakeFile); 353 | stream.end(); 354 | }); 355 | 356 | it('invalid config file', function (done) { 357 | var fakeFile = getFixtureFile('valid.scss'); 358 | var stream = scssLintPlugin({'config': './test/fixtures/invalid-default.yml'}); 359 | var error = false; 360 | 361 | stream 362 | .on('error', function (issue) { 363 | expect(issue.message).to.have.length.above(1); 364 | error = true; 365 | }) 366 | .once('end', function() { 367 | expect(error).to.be.true; 368 | done(); 369 | }); 370 | 371 | stream.write(fakeFile); 372 | stream.end(); 373 | }); 374 | 375 | it('confil file does not exist', function (done) { 376 | var fakeFile = getFixtureFile('valid.scss'); 377 | var stream = scssLintPlugin({'config': './test/fixtures/no-exist.yml'}); 378 | var error = false; 379 | 380 | stream 381 | .on('error', function (issue) { 382 | expect(issue.message).to.be.equal('Config file did not exist or was not readable'); 383 | error = true; 384 | }) 385 | .once('end', function() { 386 | expect(error).to.be.true; 387 | done(); 388 | }); 389 | 390 | stream.write(fakeFile); 391 | stream.end(); 392 | }); 393 | 394 | it('write the json output', function(done) { 395 | var fakeFile = getFixtureFile('invalid.scss'); 396 | 397 | var stream = scssLintPlugin({reporterOutput: 'test.json'}); 398 | 399 | stream 400 | .once('end', function() { 401 | var fileContent = fs.readFileSync('test.json', 'utf8'); 402 | 403 | expect(fileContent).to.have.length.above(1); 404 | 405 | fs.unlinkSync('test.json'); 406 | 407 | done(); 408 | }); 409 | 410 | stream.write(fakeFile); 411 | stream.end(); 412 | }); 413 | 414 | it('write the xml output', function(done) { 415 | var fakeFile = getFixtureFile('invalid.scss'); 416 | 417 | var stream = scssLintPlugin({reporterOutput: 'test.xml', reporterOutputFormat: 'Checkstyle'}); 418 | 419 | stream 420 | .once('end', function() { 421 | var fileContent = fs.readFileSync('test.xml', 'utf8'); 422 | 423 | expect(fileContent).to.have.length.above(1); 424 | 425 | fs.unlinkSync('test.xml'); 426 | 427 | done(); 428 | }); 429 | 430 | stream.write(fakeFile); 431 | stream.end(); 432 | }); 433 | 434 | it('scss-lint src', function(done) { 435 | var fakeFile = getFixtureFile('invalid.scss'); 436 | 437 | var stream = scssLintPlugin({src: 'test/fixtures/invalid.scss'}); 438 | 439 | stream 440 | .on('data', function (file) { 441 | expect(file.scsslint.success).to.be.false; 442 | }) 443 | .once('end', function() { 444 | done(); 445 | }); 446 | }); 447 | 448 | 449 | it('should create correct bundle exec command', function (done) { 450 | var fakeFile = getFixtureFile('valid.scss'); 451 | var stream = scssLintPlugin({'bundleExec': true}); 452 | 453 | stream 454 | .on('data', function (file) { 455 | expect(file.scsslint.success).to.be.true; 456 | }) 457 | .once('end', function() { 458 | done(); 459 | }); 460 | 461 | stream.write(fakeFile); 462 | stream.end(); 463 | }); 464 | }); 465 | --------------------------------------------------------------------------------