├── .editorconfig ├── .gitattributes ├── .gitignore ├── .npmrc ├── .travis.yml ├── contributing.md ├── index.js ├── lib ├── logger.js └── utils.js ├── license ├── package.json ├── readme.md └── test ├── gulpfile.js ├── result ├── directory with spaces │ └── file with spaces.css ├── directory │ └── file.css ├── file.css └── warnings.css ├── source ├── _partial.scss ├── directory with spaces │ └── file with spaces.scss ├── directory │ ├── _nested-partial.scss │ └── file.scss ├── file.scss └── warnings.scss ├── special ├── computational.scss └── error.scss └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | .sass-cache 4 | fixture/maps 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | - '6' 5 | - '4' 6 | before_install: 7 | - gem update --system 8 | - gem install sass 9 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | Issues with the output should be reported on the Sass [issue tracker](https://github.com/sass/sass/issues). 2 | 3 | Before posting an issue: 4 | 5 | - Update to the latest version. 6 | - Check the example tasks and available options in the readme. 7 | - Create a reduced test case with only the gulp-ruby-sass task. 8 | - Try running a similar command with the Sass gem directly. To see the command gulp-ruby-sass is running, add `verbose: true` to your task options. 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const os = require('os'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const Readable = require('stream').Readable; 6 | const convert = require('convert-source-map'); 7 | const dargs = require('dargs'); 8 | const eachAsync = require('each-async'); 9 | const glob = require('glob'); 10 | const pathExists = require('path-exists'); 11 | const rimraf = require('rimraf'); 12 | const spawn = require('cross-spawn'); 13 | const PluginError = require('plugin-error'); 14 | const fancyLog = require('fancy-log'); 15 | const Vinyl = require('vinyl'); 16 | const chalk = require('chalk'); 17 | const replaceExt = require('replace-ext'); 18 | const logger = require('./lib/logger'); 19 | const utils = require('./lib/utils'); 20 | 21 | const emitErr = utils.emitErr; 22 | const replaceLocation = utils.replaceLocation; 23 | const createIntermediatePath = utils.createIntermediatePath; 24 | 25 | const defaults = { 26 | tempDir: path.join(os.tmpdir(), 'gulp-ruby-sass'), 27 | verbose: false, 28 | sourcemap: false, 29 | emitCompileError: false 30 | }; 31 | 32 | if (typeof process.getuid === 'function') { 33 | defaults.tempDir += `-${process.getuid()}`; 34 | } 35 | 36 | function gulpRubySass(sources, options) { 37 | const stream = new Readable({objectMode: true}); 38 | 39 | // Redundant but necessary 40 | stream._read = () => {}; 41 | 42 | options = Object.assign({}, defaults, options); 43 | 44 | // Alert user that `container` is deprecated 45 | if (options.container) { 46 | fancyLog(chalk.yellow('The container option has been deprecated. Simultaneous tasks work automatically now!')); 47 | } 48 | 49 | // Error if user tries to watch their files with the Sass gem 50 | if (options.watch || options.poll) { 51 | emitErr(stream, '`watch` and `poll` are not valid options for gulp-ruby-sass. Use `gulp.watch` to rebuild your files on change.'); 52 | } 53 | 54 | // Error if user tries to pass a Sass option to sourcemap 55 | if (typeof options.sourcemap !== 'boolean') { 56 | emitErr(stream, 'The sourcemap option must be true or false. See the readme for instructions on using Sass sourcemaps with gulp.'); 57 | } 58 | 59 | options.sourcemap = options.sourcemap === true ? 'file' : 'none'; 60 | options.update = true; 61 | 62 | // Simplified handling of array sources, like gulp.src 63 | if (!Array.isArray(sources)) { 64 | sources = [sources]; 65 | } 66 | 67 | const matches = []; 68 | const bases = []; 69 | 70 | for (const source of sources) { 71 | matches.push(glob.sync(source)); 72 | bases.push(options.base || utils.calculateBase(source)); 73 | } 74 | 75 | // Log and return stream if there are no file matches 76 | if (matches[0].length < 1) { 77 | fancyLog('No files matched your Sass source.'); 78 | stream.push(null); 79 | return stream; 80 | } 81 | 82 | const intermediateDir = createIntermediatePath(sources, matches, options); 83 | const compileMappings = []; 84 | const baseMappings = {}; 85 | 86 | matches.forEach((matchArray, i) => { 87 | const base = bases[i]; 88 | 89 | matchArray.filter(match => { 90 | // Remove _partials 91 | return path.basename(match).indexOf('_') !== 0; 92 | }) 93 | .forEach(match => { 94 | const dest = replaceExt( 95 | replaceLocation(match, base, intermediateDir), 96 | '.css' 97 | ); 98 | const relative = path.relative(intermediateDir, dest); 99 | 100 | // Source:dest mappings for the Sass CLI 101 | compileMappings.push(`${match}:${dest}`); 102 | 103 | // Store base values by relative file path 104 | baseMappings[relative] = base; 105 | }); 106 | }); 107 | 108 | const args = dargs(options, [ 109 | 'bundleExec', 110 | 'watch', 111 | 'poll', 112 | 'tempDir', 113 | 'verbose', 114 | 'emitCompileError', 115 | 'base', 116 | 'container' 117 | ]).concat(compileMappings); 118 | 119 | let command; 120 | 121 | if (options.bundleExec) { 122 | command = 'bundle'; 123 | args.unshift('exec', 'sass'); 124 | } else { 125 | command = 'sass'; 126 | } 127 | 128 | // Plugin logging 129 | if (options.verbose) { 130 | logger.verbose(command, args); 131 | } 132 | 133 | const sass = spawn(command, args); 134 | 135 | sass.stdout.setEncoding('utf8'); 136 | sass.stderr.setEncoding('utf8'); 137 | 138 | sass.stdout.on('data', data => { 139 | logger.stdout(stream, intermediateDir, data); 140 | }); 141 | 142 | sass.stderr.on('data', data => { 143 | logger.stderr(stream, intermediateDir, data); 144 | }); 145 | 146 | sass.on('error', err => { 147 | logger.error(stream, err); 148 | }); 149 | 150 | sass.on('close', code => { 151 | if (options.emitCompileError && code !== 0) { 152 | emitErr(stream, 'Sass compilation failed. See console output for more information.'); 153 | } 154 | 155 | glob(path.join(intermediateDir, '**/*'), (err, files) => { 156 | if (err) { 157 | emitErr(stream, err); 158 | } 159 | 160 | eachAsync(files, (file, i, next) => { 161 | if (fs.statSync(file).isDirectory() || path.extname(file) === '.map') { 162 | next(); 163 | return; 164 | } 165 | 166 | const relative = path.relative(intermediateDir, file); 167 | const base = baseMappings[relative]; 168 | 169 | fs.readFile(file, (err, data) => { 170 | if (err) { 171 | emitErr(stream, err); 172 | next(); 173 | return; 174 | } 175 | 176 | // Rewrite file paths so gulp thinks the file came from cwd, not the 177 | // intermediate directory 178 | const vinylFile = new Vinyl({ 179 | cwd: process.cwd(), 180 | base, 181 | path: replaceLocation(file, intermediateDir, base) 182 | }); 183 | 184 | // Sourcemap integration 185 | if (options.sourcemap === 'file' && pathExists.sync(file + '.map')) { 186 | // Remove sourcemap comment; gulp-sourcemaps will add it back in 187 | data = Buffer.from(convert.removeMapFileComments(data.toString())); 188 | const sourceMapObject = JSON.parse(fs.readFileSync(file + '.map', 'utf8')); 189 | 190 | // Create relative paths for sources 191 | sourceMapObject.sources = sourceMapObject.sources.map(sourcePath => { 192 | const absoluteSourcePath = decodeURI(path.resolve( 193 | '/', 194 | sourcePath.replace('file:///', '') 195 | )); 196 | return path.relative(base, absoluteSourcePath); 197 | }); 198 | 199 | vinylFile.sourceMap = sourceMapObject; 200 | } 201 | 202 | vinylFile.contents = data; 203 | stream.push(vinylFile); 204 | next(); 205 | }); 206 | }, () => { 207 | stream.push(null); 208 | }); 209 | }); 210 | }); 211 | 212 | return stream; 213 | } 214 | 215 | gulpRubySass.logError = err => { 216 | const message = new PluginError('gulp-ruby-sass', err); 217 | process.stderr.write(`${message}\n`); 218 | this.emit('end'); 219 | }; 220 | 221 | gulpRubySass.clearCache = tempDir => { 222 | tempDir = tempDir || defaults.tempDir; 223 | rimraf.sync(tempDir); 224 | }; 225 | 226 | module.exports = gulpRubySass; 227 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const escapeStringRegexp = require('escape-string-regexp'); 3 | const fancyLog = require('fancy-log'); 4 | const emitErr = require('./utils').emitErr; 5 | 6 | const logger = {}; 7 | 8 | // Remove intermediate directory for more Sass-like logging 9 | logger.prettifyDirectoryLogging = (msg, intermediateDir) => { 10 | const escapedDir = escapeStringRegexp(intermediateDir); 11 | return msg.replace(new RegExp(`${escapedDir}/?`, 'g'), './'); 12 | }; 13 | 14 | // TODO: Now that we've standardized on --update, remove parsing that only 15 | // applies to single, non update compilations. 16 | 17 | logger.verbose = (command, args) => { 18 | fancyLog(`Running command ${command} ${args.join(' ')}`); 19 | }; 20 | 21 | logger.stdout = (stream, intermediateDir, data) => { 22 | // Bundler error: no Sass version found 23 | if (/bundler: command not found: sass/.test(data)) { 24 | emitErr(stream, 'bundler: command not found: sass'); 25 | } else if (/Could not locate Gemfile or .bundle\/ directory/.test(data)) { // Bundler error: Gemfile not found 26 | emitErr(stream, 'bundler: could not locate Gemfile or .bundle directory'); 27 | } else if (/No such file or directory @ rb_sysopen/.test(data)) { // Sass error: directory missing 28 | emitErr(stream, data.trim()); 29 | } else { // Not an error: Sass logging 30 | data = logger.prettifyDirectoryLogging(data, intermediateDir); 31 | data = data.trim(); 32 | fancyLog(data); 33 | } 34 | }; 35 | 36 | logger.stderr = (stream, intermediateDir, data) => { 37 | const bundlerMissing = /Could not find 'bundler' \((.*?)\)/.exec(data); 38 | const sassVersionMissing = /Could not find gem 'sass \((.*?)\) ruby'/.exec(data); 39 | 40 | // Ruby error: Bundler gem not installed 41 | if (bundlerMissing) { 42 | emitErr(stream, `ruby: Could not find 'bundler' (${bundlerMissing[1]})`); 43 | } else if (sassVersionMissing) { // Bundler error: no matching Sass version 44 | emitErr(stream, `bundler: Could not find gem 'sass (${sassVersionMissing[1]})'`); 45 | } else if (/No such file or directory @ rb_sysopen/.test(data)) { // Sass error: file missing 46 | emitErr(stream, data.trim()); 47 | } else { // Not an error: Sass warnings, debug statements 48 | data = logger.prettifyDirectoryLogging(data, intermediateDir); 49 | data = data.trim(); 50 | fancyLog(data); 51 | } 52 | }; 53 | 54 | logger.error = (stream, err) => { 55 | if (err.code === 'ENOENT') { 56 | // Spawn error: gems not installed 57 | emitErr(stream, `Gem ${err.path} is not installed`); 58 | } else { 59 | // Other errors 60 | emitErr(stream, err); 61 | } 62 | }; 63 | 64 | module.exports = logger; 65 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const glob = require('glob'); 4 | const glob2base = require('glob2base'); 5 | const md5Hex = require('md5-hex'); 6 | const PluginError = require('plugin-error'); 7 | 8 | exports.emitErr = (stream, err) => { 9 | stream.emit('error', new PluginError('gulp-ruby-sass', err)); 10 | }; 11 | 12 | // Create unique temporary directory path per task using cwd, options, sources, 13 | // and all matched files. Switching options does not break Sass cache so we do 14 | // it ourselves. Possibly a bug: https://github.com/sass/sass/issues/1830 15 | exports.createIntermediatePath = (sources, matches, options) => { 16 | return path.join( 17 | options.tempDir, 18 | md5Hex( 19 | process.cwd() + 20 | JSON.stringify(sources) + 21 | JSON.stringify(matches) + 22 | JSON.stringify(options) 23 | ) 24 | ); 25 | }; 26 | 27 | exports.calculateBase = source => glob2base(new glob.Glob(source)); 28 | 29 | exports.replaceLocation = (origPath, currentLoc, newLoc) => path.join(newLoc, path.relative(currentLoc, origPath)); 30 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-ruby-sass", 3 | "version": "4.0.0", 4 | "description": "Compile Sass to CSS with Ruby Sass", 5 | "license": "MIT", 6 | "repository": "sindresorhus/gulp-ruby-sass", 7 | "maintainers": [ 8 | { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "sindresorhus.com" 12 | }, 13 | { 14 | "name": "Rob Wierzbowski", 15 | "email": "robwierzbowski@gmail.com", 16 | "web": "robwierzbowski.com" 17 | } 18 | ], 19 | "engines": { 20 | "node": ">=4" 21 | }, 22 | "scripts": { 23 | "test": "xo && cd test && mocha test.js" 24 | }, 25 | "files": [ 26 | "index.js", 27 | "lib" 28 | ], 29 | "keywords": [ 30 | "gulpplugin", 31 | "scss", 32 | "sass", 33 | "css", 34 | "compile", 35 | "preprocessor", 36 | "style", 37 | "ruby", 38 | "source-map", 39 | "source-maps", 40 | "sourcemap", 41 | "sourcemaps" 42 | ], 43 | "dependencies": { 44 | "chalk": "^2.3.0", 45 | "convert-source-map": "^1.0.0", 46 | "cross-spawn": "^5.0.0", 47 | "dargs": "^3.0.1", 48 | "each-async": "^1.0.0", 49 | "escape-string-regexp": "^1.0.3", 50 | "fancy-log": "^1.3.2", 51 | "glob": "^7.0.3", 52 | "glob2base": "0.0.12", 53 | "md5-hex": "^2.0.0", 54 | "path-exists": "^3.0.0", 55 | "plugin-error": "^0.1.2", 56 | "replace-ext": "^1.0.0", 57 | "rimraf": "^2.2.8", 58 | "vinyl": "^2.1.0" 59 | }, 60 | "devDependencies": { 61 | "gulp": "^3.8.11", 62 | "gulp-sourcemaps": "^2.2.0", 63 | "mocha": "*", 64 | "vinyl-file": "^3.0.0", 65 | "xo": "*" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | This project is deprecated because [Ruby Sass is deprecated](http://sass.logdown.com/posts/7081811). Switch to [`gulp-sass`](https://github.com/dlmanning/gulp-sass). 4 | 5 | --- 6 | 7 | # gulp-ruby-sass [![Build Status](https://travis-ci.org/sindresorhus/gulp-ruby-sass.svg?branch=master)](https://travis-ci.org/sindresorhus/gulp-ruby-sass) 8 | 9 | Compiles Sass with the [Sass gem](http://sass-lang.com/install) and pipes the results into a gulp stream.
10 | To compile Sass with [libsass](http://libsass.org/), use [gulp-sass](https://github.com/dlmanning/gulp-sass) 11 | 12 | 13 | 14 | ## Install 15 | 16 | ``` 17 | $ npm install --save-dev gulp-ruby-sass 18 | ``` 19 | 20 | Requires [Sass >=3.4](http://sass-lang.com/install). 21 | 22 | 23 | ## Usage 24 | 25 | ### sass(source, [options]) 26 | 27 | Use gulp-ruby-sass *instead of `gulp.src`* to compile Sass files. 28 | 29 | ```js 30 | const gulp = require('gulp'); 31 | const sass = require('gulp-ruby-sass'); 32 | 33 | gulp.task('sass', () => 34 | sass('source/file.scss') 35 | .on('error', sass.logError) 36 | .pipe(gulp.dest('result')) 37 | ); 38 | ``` 39 | 40 | #### source 41 | 42 | Type: `string` `string[]` 43 | 44 | File or glob pattern (`source/**/*.scss`) to compile. Ignores files prefixed with an underscore. **Directory sources are not supported.** 45 | 46 | #### options 47 | 48 | Type: `Object` 49 | 50 | Object containing plugin and Sass options. 51 | 52 | ##### bundleExec 53 | 54 | Type: `boolean`
55 | Default: `false` 56 | 57 | Run Sass with [bundle exec](http://gembundler.com/man/bundle-exec.1.html). 58 | 59 | ##### sourcemap 60 | 61 | Type: `boolean`
62 | Default: `false` 63 | 64 | Initialize and pass Sass sourcemaps to [gulp-sourcemaps](https://github.com/floridoo/gulp-sourcemaps). Note this option replaces Sass's `sourcemap` option. 65 | 66 | ```js 67 | const gulp = require('gulp'); 68 | const sass = require('gulp-ruby-sass'); 69 | const sourcemaps = require('gulp-sourcemaps'); 70 | 71 | gulp.task('sass', () => 72 | sass('source/file.scss', {sourcemap: true}) 73 | .on('error', sass.logError) 74 | // for inline sourcemaps 75 | .pipe(sourcemaps.write()) 76 | // for file sourcemaps 77 | .pipe(sourcemaps.write('maps', { 78 | includeContent: false, 79 | sourceRoot: 'source' 80 | })) 81 | .pipe(gulp.dest('result')) 82 | ); 83 | ``` 84 | 85 | ##### base 86 | 87 | Type: `string` 88 | 89 | Identical to `gulp.src`'s [`base` option](https://github.com/gulpjs/gulp/blob/master/docs/API.md#optionsbase). 90 | 91 | ##### tempDir 92 | 93 | Type: `string`
94 | Default: System temp directory 95 | 96 | This plugin compiles Sass files to a temporary directory before pushing them through the stream. Use `tempDir` to choose an alternate directory if you aren't able to use the default OS temporary directory. 97 | 98 | ##### emitCompileError 99 | 100 | Type: `boolean`
101 | Default: `false` 102 | 103 | Emit a gulp error when Sass compilation fails. 104 | 105 | ##### verbose 106 | 107 | Type: `boolean`
108 | Default: `false` 109 | 110 | Log the spawned Sass or Bundler command. Useful for debugging. 111 | 112 | ##### Sass options 113 | 114 | Any additional options are passed directly to the Sass executable. The options are camelCase versions of Sass's options parsed by [`dargs`](https://github.com/sindresorhus/dargs). 115 | 116 | Run `sass -h` for a complete list of Sass options. 117 | 118 | ```js 119 | gulp.task('sass', () => 120 | sass('source/file.scss', { 121 | precision: 6, 122 | stopOnError: true, 123 | cacheLocation: './', 124 | loadPath: [ 'library', '../../shared-components' ] 125 | }) 126 | .on('error', sass.logError) 127 | .pipe(gulp.dest('result')) 128 | ); 129 | ``` 130 | 131 | ### sass.logError(err) 132 | 133 | Convenience function for pretty error logging. 134 | 135 | ### sass.clearCache([tempDir]) 136 | 137 | In rare cases you may need to clear gulp-ruby-sass's cache. This sync function deletes all files used for Sass caching. If you've set a custom temporary directory in your task you must pass it to `clearCache`. 138 | 139 | 140 | ## Issues 141 | 142 | This plugin wraps the Sass gem for the gulp build system. It does not alter Sass's output in any way. Any issues with Sass output should be reported to the [Sass issue tracker](https://github.com/sass/sass/issues). 143 | 144 | Before submitting an issue please read the [contributing guidelines](https://github.com/sindresorhus/gulp-ruby-sass/blob/master/contributing.md). 145 | 146 | 147 | ## License 148 | 149 | MIT © [Sindre Sorhus](https://sindresorhus.com) 150 | -------------------------------------------------------------------------------- /test/gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const gulp = require('gulp'); 3 | const sourcemaps = require('gulp-sourcemaps'); 4 | const sass = require('..'); 5 | 6 | gulp.task('sass', () => 7 | sass('source/**/*.scss', {verbose: true}) 8 | .on('error', sass.logError) 9 | .pipe(sourcemaps.write()) 10 | .pipe(gulp.dest('result')) 11 | ); 12 | -------------------------------------------------------------------------------- /test/result/directory with spaces/file with spaces.css: -------------------------------------------------------------------------------- 1 | .i-have-spaces { 2 | background: pink; } 3 | -------------------------------------------------------------------------------- /test/result/directory/file.css: -------------------------------------------------------------------------------- 1 | a { 2 | color: green; } 3 | -------------------------------------------------------------------------------- /test/result/file.css: -------------------------------------------------------------------------------- 1 | a { 2 | color: green; } 3 | 4 | html { 5 | font-size: 16px; } 6 | 7 | body { 8 | color: black; 9 | content: 'mixin from nested partial'; } 10 | -------------------------------------------------------------------------------- /test/result/warnings.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sindresorhus/gulp-ruby-sass/6addbc462a55383d697713e8ad6ff424bf530413/test/result/warnings.css -------------------------------------------------------------------------------- /test/source/_partial.scss: -------------------------------------------------------------------------------- 1 | a { 2 | color: green; 3 | } 4 | -------------------------------------------------------------------------------- /test/source/directory with spaces/file with spaces.scss: -------------------------------------------------------------------------------- 1 | .i-have-spaces { 2 | background: pink; 3 | } 4 | -------------------------------------------------------------------------------- /test/source/directory/_nested-partial.scss: -------------------------------------------------------------------------------- 1 | @mixin bar() { 2 | content: 'mixin from nested partial'; 3 | } 4 | -------------------------------------------------------------------------------- /test/source/directory/file.scss: -------------------------------------------------------------------------------- 1 | @import '../partial'; 2 | -------------------------------------------------------------------------------- /test/source/file.scss: -------------------------------------------------------------------------------- 1 | @import 'partial'; 2 | @import 'directory/nested-partial'; 3 | 4 | $base-font-size: 16px !default; 5 | 6 | @mixin foo() { 7 | color: black; 8 | } 9 | 10 | html { 11 | font-size: $base-font-size; 12 | } 13 | 14 | body { 15 | @include foo(); 16 | @include bar(); 17 | } 18 | -------------------------------------------------------------------------------- /test/source/warnings.scss: -------------------------------------------------------------------------------- 1 | @debug 'A debug statement'; 2 | @warn 'A warn statement'; 3 | -------------------------------------------------------------------------------- /test/special/computational.scss: -------------------------------------------------------------------------------- 1 | @for $i from 1 through 4000 { 2 | .class-#{$i} { 3 | width: 60px * $i; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/special/error.scss: -------------------------------------------------------------------------------- 1 | @import 'i-dont-exist'; 2 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict'; 3 | const path = require('path'); 4 | const assert = require('assert'); 5 | const pathExists = require('path-exists'); 6 | const rimraf = require('rimraf'); 7 | const vinylFile = require('vinyl-file'); 8 | const sass = require('../'); 9 | const logger = require('../lib/logger'); 10 | 11 | const defaultOptions = { 12 | quiet: true, 13 | // Normalize compilation results on Windows systems 14 | unixNewlines: true 15 | }; 16 | 17 | // Load the expected result file from the compiled results directory 18 | const loadExpectedFile = (relativePath, base) => { 19 | base = base || 'result'; 20 | const file = path.join(base, relativePath); 21 | return vinylFile.readSync(file, {base}); 22 | }; 23 | 24 | const sortByRelative = (a, b) => a.relative.localeCompare(b.relative); 25 | 26 | const compilesSource = (source, expected, options) => { 27 | const files = []; 28 | options = options || defaultOptions; 29 | 30 | before(done => { 31 | sass(source, options) 32 | .on('data', data => { 33 | files.push(data); 34 | }) 35 | .on('end', () => { 36 | files.sort(sortByRelative); 37 | done(); 38 | }); 39 | }); 40 | 41 | it('creates the correct number of files', () => { 42 | assert.equal(files.length, expected.length); 43 | }); 44 | 45 | it('creates files at the correct path', () => { 46 | assert(files.length); 47 | files.forEach((file, i) => { 48 | assert.equal(file.relative, expected[i].relative); 49 | }); 50 | }); 51 | 52 | it('creates correct file contents', () => { 53 | assert(files.length); 54 | files.forEach((file, i) => { 55 | assert.deepEqual( 56 | file.contents.toString(), 57 | expected[i].contents.toString() 58 | ); 59 | }); 60 | }); 61 | }; 62 | 63 | describe('compiling', function () { 64 | this.timeout(20000); 65 | 66 | describe('a single file', () => { 67 | const source = 'source/file.scss'; 68 | const expected = [loadExpectedFile('file.css')]; 69 | 70 | compilesSource(source, expected); 71 | }); 72 | 73 | describe('multiple files', () => { 74 | const source = 'source/**/*.scss'; 75 | const expected = [ 76 | loadExpectedFile('directory with spaces/file with spaces.css'), 77 | loadExpectedFile('directory/file.css'), 78 | loadExpectedFile('file.css'), 79 | loadExpectedFile('warnings.css') 80 | ]; 81 | 82 | compilesSource(source, expected); 83 | }); 84 | 85 | describe('array sources', () => { 86 | const source = [ 87 | 'source/file.scss', 88 | 'source/directory with spaces/file with spaces.scss' 89 | ]; 90 | const expected = [ 91 | loadExpectedFile('file with spaces.css', 'result/directory with spaces'), 92 | loadExpectedFile('file.css') 93 | ]; 94 | 95 | compilesSource(source, expected); 96 | }); 97 | 98 | describe('nonexistent sources', () => { 99 | it('does not error when no files match source', done => { 100 | const source = 'source/does-not-exist.scss'; 101 | let error; 102 | 103 | sass(source, defaultOptions) 104 | .on('data', () => {}) 105 | .on('error', err => { 106 | error = err; 107 | }) 108 | .on('end', () => { 109 | assert.equal(error, undefined); 110 | done(); 111 | }); 112 | }); 113 | }); 114 | }); 115 | 116 | describe('concurrently run tasks', function () { 117 | this.timeout(20000); 118 | const aFiles = []; 119 | const bFiles = []; 120 | const cFiles = []; 121 | let counter = 0; 122 | 123 | const isDone = done => { 124 | counter++; 125 | 126 | if (counter === 3) { 127 | done(); 128 | } 129 | }; 130 | 131 | before(done => { 132 | sass('source/file.scss', defaultOptions) 133 | .on('data', data => { 134 | aFiles.push(data); 135 | }) 136 | .on('end', () => { 137 | isDone(done); 138 | }); 139 | 140 | sass('source/directory/file.scss', defaultOptions) 141 | .on('data', data => { 142 | bFiles.push(data); 143 | }) 144 | .on('end', () => { 145 | isDone(done); 146 | }); 147 | 148 | sass('source/directory with spaces/file with spaces.scss', defaultOptions) 149 | .on('data', data => { 150 | cFiles.push(data); 151 | }) 152 | .on('end', () => { 153 | isDone(done); 154 | }); 155 | }); 156 | 157 | it('don\'t intermix result files', () => { 158 | assert.equal(aFiles.length, 1); 159 | assert.equal(bFiles.length, 1); 160 | assert.equal(cFiles.length, 1); 161 | }); 162 | }); 163 | 164 | describe('options', function () { 165 | this.timeout(20000); 166 | 167 | describe('sourcemap', () => { 168 | describe('replaces Sass sourcemaps with vinyl sourceMaps', () => { 169 | const files = []; 170 | const options = Object.assign({}, defaultOptions, {sourcemap: true}); 171 | 172 | before(done => { 173 | sass('source/file.scss', options) 174 | .on('data', data => { 175 | files.push(data); 176 | }) 177 | .on('end', done); 178 | }); 179 | 180 | it('doesn\'t stream Sass sourcemap files', () => { 181 | assert.equal(files.length, 1); 182 | }); 183 | 184 | it('removes Sass sourcemap comment', () => { 185 | assert( 186 | files[0].contents.toString().indexOf('sourceMap') === -1, 187 | 'File contains sourcemap comment' 188 | ); 189 | }); 190 | 191 | it('adds a vinyl sourcemap', () => { 192 | assert.equal(typeof files[0].sourceMap, 'object'); 193 | assert.equal(files[0].sourceMap.version, 3); 194 | }); 195 | }); 196 | 197 | const includesCorrectSources = (source, expected) => { 198 | const files = []; 199 | const options = Object.assign({}, defaultOptions, {sourcemap: true}); 200 | 201 | before(done => { 202 | sass(source, options) 203 | .on('data', data => { 204 | files.push(data); 205 | }) 206 | .on('end', () => { 207 | files.sort(sortByRelative); 208 | done(); 209 | }); 210 | }); 211 | 212 | it('includes the correct sources', () => { 213 | files.forEach((file, i) => { 214 | assert.deepEqual(file.sourceMap.sources, expected[i]); 215 | }); 216 | }); 217 | }; 218 | 219 | describe('compiling files from a single file source', () => { 220 | const source = ['source/file.scss']; 221 | const expected = [ 222 | ['_partial.scss', 'file.scss', 'directory/_nested-partial.scss'] 223 | ]; 224 | 225 | includesCorrectSources(source, expected); 226 | }); 227 | 228 | describe('compiling files and directories with spaces', () => { 229 | const source = ['source/directory with spaces/file with spaces.scss']; 230 | const expected = [ 231 | ['file with spaces.scss'] 232 | ]; 233 | 234 | includesCorrectSources(source, expected); 235 | }); 236 | 237 | describe('compiling files from glob source', () => { 238 | const source = ['source/**/file.scss']; 239 | const expected = [ 240 | ['_partial.scss'], 241 | ['_partial.scss', 'file.scss', 'directory/_nested-partial.scss'] 242 | ]; 243 | 244 | includesCorrectSources(source, expected); 245 | }); 246 | }); 247 | 248 | describe('emitCompileError', () => { 249 | let error; 250 | 251 | before(done => { 252 | const options = Object.assign({}, defaultOptions, {emitCompileError: true}); 253 | 254 | sass('special/error.scss', options) 255 | .on('data', () => {}) 256 | .on('error', err => { 257 | error = err; 258 | }) 259 | .on('end', done); 260 | }); 261 | 262 | it('emits a gulp error when Sass compilation fails', () => { 263 | assert(error instanceof Error); 264 | assert.equal( 265 | error.message, 266 | 'Sass compilation failed. See console output for more information.' 267 | ); 268 | }); 269 | }); 270 | 271 | describe('base (for colliding sources)', () => { 272 | const source = ['source/file.scss', 'source/directory/file.scss']; 273 | const expected = [ 274 | loadExpectedFile('directory/file.css'), 275 | loadExpectedFile('file.css') 276 | ]; 277 | const options = Object.assign({}, defaultOptions, {base: 'source'}); 278 | 279 | compilesSource(source, expected, options); 280 | }); 281 | 282 | describe('tempDir', () => { 283 | it('compiles files to the specified directory', done => { 284 | const source = 'source/file.scss'; 285 | const tempDir = './custom-temp-dir'; 286 | const options = Object.assign({}, defaultOptions, {tempDir}); 287 | 288 | assert.equal( 289 | pathExists.sync(tempDir), 290 | false, 291 | 'The temporary directory already exists, and would create false positives.' 292 | ); 293 | 294 | sass(source, options) 295 | 296 | .on('data', () => { 297 | assert(pathExists.sync(tempDir)); 298 | }) 299 | 300 | // Clean up if tests are run locally 301 | .on('end', () => { 302 | rimraf(tempDir, done); 303 | }); 304 | }); 305 | }); 306 | }); 307 | 308 | describe('caching', function () { 309 | this.timeout(20000); 310 | 311 | it('compiles an unchanged file faster the second time', done => { 312 | sass.clearCache(); 313 | const startOne = new Date(); 314 | 315 | sass('special/computational.scss', defaultOptions) 316 | .on('data', () => {}) 317 | .on('end', () => { 318 | const endOne = new Date(); 319 | const runtimeOne = endOne - startOne; 320 | 321 | sass('special/computational.scss', defaultOptions) 322 | .on('data', () => {}) 323 | .on('end', () => { 324 | const runtimeTwo = new Date() - endOne; 325 | 326 | assert( 327 | // Pad time to avoid potential intermittents 328 | runtimeOne > runtimeTwo + 50, 329 | 'Compilation times were not decreased significantly. Caching may be broken.' 330 | ); 331 | 332 | done(); 333 | }); 334 | }); 335 | }); 336 | }); 337 | 338 | describe('logging', function () { 339 | this.timeout(20000); 340 | 341 | it('correctly processes paths with special characters', () => { 342 | const dir = 'foo/bar/++/__/(a|f)'; 343 | const msg = 'dir: ' + dir + '/some/directory, Gettin\' Sassy!'; 344 | 345 | assert( 346 | logger.prettifyDirectoryLogging(msg, dir), 347 | 'dir: some/directory, Gettin\' Sassy!' 348 | ); 349 | }); 350 | }); 351 | --------------------------------------------------------------------------------