Rewrite gulp-sass to be a very light wrapper

Simply pass everything to node-sass and just hand
back the results to gulp.
Removed custom options like success/failure cb
and sync rendering.

Missing:
 -  gulp-sourcemaps support + test
2.x
Vincent Prouillet 2015-03-23 22:01:12 +00:00
parent 5a99375ac0
commit 1dd861e4d6
19 changed files with 174 additions and 381 deletions

View File

@ -1,3 +1,5 @@
language: node_js
node_js:
- "0.10"
- node
- iojs

View File

@ -10,7 +10,7 @@ Sass plugin for [gulp](https://github.com/gulpjs/gulp).
# Install
```
npm install gulp-sass
npm install gulp-sass --save-dev
```
# Basic Usage
@ -19,38 +19,19 @@ Something like this:
```javascript
var gulp = require('gulp');
var gutil = require('gulp-util');
var sass = require('gulp-sass');
gulp.task('sass', function () {
gulp.src('./scss/*.scss')
.pipe(sass())
.pipe(sass().on('error', gutil.log))
.pipe(gulp.dest('./css'));
});
```
Options passed as a hash into `sass()` will be passed along to [`node-sass`](https://github.com/sass/node-sass).
If you want to use the indented syntax (`.sass`) as the top level file, use `sass({indentedSyntax: true})`.
## gulp-sass specific options
#### `errLogToConsole: true`
If you pass `errLogToConsole: true` into the options hash, sass errors will be logged to the console instead of generating a `gutil.PluginError` object. Use this option with `gulp.watch` to keep gulp from stopping every time you mess up your sass.
#### `onSuccess: callback`
Pass in your own callback to be called upon successful compilation by node-sass. The callback has the form `callback(css)`, and is passed the compiled css as a string. Note: This *does not* prevent gulp-sass's default behavior of writing the output css file.
#### `onError: callback`
Pass in your own callback to be called upon a sass error from node-sass. The callback has the form `callback(err)`, where err is the error string generated by libsass. Note: this *does* prevent an actual `gulpPluginError` object from being created.
#### `sync: true`
If you pass `sync: true` into the options hash, sass.renderSync will be called, instead of sass.render. This should help when memory and/or cpu usage is getting very high when rendering many and/or big files.
## Source Maps
## Source Maps TODO
gulp-sass can be used in tandem with [gulp-sourcemaps](https://github.com/floridoo/gulp-sourcemaps) to generate source maps for the SASS to CSS compilation. You will need to initialize [gulp-sourcemaps](https://github.com/floridoo/gulp-sourcemaps) prior to running the gulp-sass compiler and write the source maps after.
@ -80,35 +61,6 @@ gulp.src('./scss/*.scss')
// will write the source maps to ./dest/css/maps
```
# Imports and Partials
gulp-sass now automatically passes along the directory of every scss file it parses as an include path for node-sass. This means that as long as you specify your includes relative to path of your scss file, everything will just work.
scss/includes/_settings.scss:
```scss
$blue: #3bbfce;
$margin: 16px;
```
scss/style.scss:
```scss
@import "includes/settings";
.content-navigation {
border-color: $blue;
color:
darken($blue, 9%);
}
.border {
padding: $margin / 2;
margin: $margin / 2;
border-color: $blue;
}
```
# Issues
Before submitting an issue, please understand that gulp-sass is only a wrapper for [node-sass](https://github.com/sass/node-sass), which in turn is a node front end for [libsass](https://github.com/sass/libsass). Missing sass features and errors should not be reported here.

110
index.js
View File

@ -1,104 +1,38 @@
var fs = require('fs')
, map = require('map-stream')
, nodeSass = require('node-sass')
, path = require('path')
, gutil = require('gulp-util')
, clone = require('clone')
, ext = gutil.replaceExtension
, applySourceMap = require('vinyl-sourcemaps-apply')
;
var gutil = require("gulp-util");
var through = require("through2");
var assign = require("object-assign");
var path = require('path');
var sass = require("node-sass");
module.exports = function (options) {
function sass (file, cb) {
var opts = options ? clone(options) : {};
var fileDir = path.dirname(file.path);
var PLUGIN_NAME = 'gulp-sass';
module.exports = function(options) {
return through.obj(function(file, enc, cb) {
if (file.isNull()) {
return cb(null, file);
}
if (file.isStream()) {
return cb(new gutil.PluginError(PLUGIN_NAME, 'Streaming not supported'));
}
if (path.basename(file.path).indexOf('_') === 0) {
return cb();
}
if (file.sourceMap) {
opts.sourceMap = file.path;
}
opts.data = file.contents.toString();
var opts = assign({}, options);
opts.file = file.path;
if (opts.includePaths && Array.isArray(opts.includePaths)) {
if (opts.includePaths.indexOf(fileDir) === -1) {
opts.includePaths.push(fileDir);
}
} else {
opts.includePaths = [fileDir];
}
opts.success = function (obj) {
if (typeof opts.onSuccess === 'function') opts.onSuccess(obj);
if (obj.map && typeof obj.map === 'string') {
// hack to remove the already added sourceMappingURL from libsass
obj.css = obj.css.replace(/\/\*#\s*sourceMappingURL\=.*\*\//, '');
// libsass gives us sources' paths relative to file;
// gulp-sourcemaps needs sources' paths relative to file.base;
// so alter the sources' paths to please gulp-sourcemaps.
obj.map = JSON.parse(obj.map);
if (obj.map.sources) {
obj.map.sources = obj.map.sources.map(function(source) {
var abs = path.resolve(path.dirname(file.path), source);
return path.relative(file.base, abs);
});
obj.map = JSON.stringify(obj.map);
applySourceMap(file, obj.map);
}
var callback = function(error, obj) {
if (error) {
return cb(new gutil.PluginError(
PLUGIN_NAME, error.message + ' on line ' + error.line + ' in ' + error.file
));
}
handleOutput(obj, file, cb);
file.contents = new Buffer(obj.css);
file.path = gutil.replaceExtension(file.path, '.css');
cb(null, file);
};
opts.error = function (err) {
if (opts.errLogToConsole) {
gutil.log(gutil.colors.red('[gulp-sass]', err.message, 'on line', err.line + 'in', err.file));
return cb();
}
if (typeof opts.onError === 'function') {
opts.onError(err);
return cb();
}
err.lineNumber = err.line;
err.fileName = err.file;
return cb(new gutil.PluginError('gulp-sass', err));
};
if ( opts.sync ) {
try {
var output = nodeSass.renderSync(opts);
opts.success(output);
handleOutput(output, file, cb);
} catch(err) {
opts.error(err);
}
} else {
nodeSass.render(opts);
}
}
return map(sass);
sass.render(opts, callback);
});
};
function handleOutput(output, file, cb) {
file.path = ext(file.path, '.css');
file.contents = new Buffer(output.css);
cb(null, file);
}

View File

@ -1,10 +1,10 @@
{
"name": "gulp-sass",
"version": "1.3.3",
"version": "2.0.0",
"description": "Gulp plugin for sass",
"main": "index.js",
"scripts": {
"test": "node test/test.js"
"test": "./node_modules/.bin/jshint index.js && ./node_modules/.bin/mocha test"
},
"repository": {
"type": "git",
@ -21,17 +21,16 @@
"url": "https://github.com/dlmanning/gulp-sass/issues"
},
"dependencies": {
"clone": "~0.1.18",
"gulp-util": "^3.0",
"map-stream": "~0.1",
"node-sass": "^2.0.1",
"node-sass": "^3.0.0-alpha.0",
"object-assign": "^2.0.0",
"through2": "^0.6.3",
"vinyl-sourcemaps-apply": "~0.1.1"
},
"devDependencies": {
"tape": "~2.3",
"concat-stream": "~1.4"
},
"jshintConfig": {
"laxcomma": true
"gulp-sourcemaps": "^1.5.1",
"jshint": "^2.6.3",
"mocha": "^2.2.1",
"should": "^5.2.0"
}
}

View File

@ -1,8 +1,11 @@
body {
background: pink; }
.error, .badError {
border: #f00;
background: #fdd; }
.error.intrusion {
.error.intrusion, .intrusion.badError {
font-size: 1.3em;
font-weight: bold; }

124
test/main.js Normal file
View File

@ -0,0 +1,124 @@
var should = require("should");
var gutil = require("gulp-util");
var path = require("path");
var fs = require("fs");
var sourcemaps = require('gulp-sourcemaps');
var sass = require("../index");
function createVinyl(filename, contents) {
var base = path.join(__dirname, "scss");
var filePath = path.join(base, filename);
return new gutil.File({
cwd: __dirname,
base: base,
path: filePath,
contents: contents || fs.readFileSync(filePath)
});
}
describe("gulp-sass", function() {
it("should pass file when it isNull()", function(done) {
var stream = sass();
var emptyFile = {
isNull: function () { return true; }
};
stream.on('data', function(data) {
data.should.equal(emptyFile);
done();
});
stream.write(emptyFile);
});
it('should emit error when file isStream()', function (done) {
var stream = sass();
var streamFile = {
isNull: function () { return false; },
isStream: function () { return true; }
};
stream.on('error', function(err) {
err.message.should.equal('Streaming not supported');
done();
});
stream.write(streamFile);
});
it("should compile a single sass file", function(done) {
var sassFile = createVinyl("mixins.scss");
var stream = sass();
stream.on("data", function(cssFile) {
should.exist(cssFile);
should.exist(cssFile.path);
should.exist(cssFile.relative);
should.exist(cssFile.contents);
String(cssFile.contents).should.equal(
fs.readFileSync(path.join(__dirname, 'expected/mixins.css'), 'utf8')
);
done();
});
stream.write(sassFile);
});
it("should compile multiple sass files", function(done) {
var files = [
createVinyl('mixins.scss'),
createVinyl('variables.scss')
];
var stream = sass();
var mustSee = files.length;
stream.on("data", function(cssFile) {
should.exist(cssFile);
should.exist(cssFile.path);
should.exist(cssFile.relative);
should.exist(cssFile.contents);
var expectedPath = 'expected/mixins.css';
if (cssFile.path.indexOf("variables") !== -1) {
expectedPath = 'expected/variables.css';
}
String(cssFile.contents).should.equal(
fs.readFileSync(path.join(__dirname, expectedPath), 'utf8')
);
mustSee--;
if (mustSee <= 0) {
done();
}
});
files.forEach(function (file) {
stream.write(file);
});
});
it("should compile files with partials in another folder", function(done) {
var sassFile = createVinyl("inheritance.scss");
var stream = sass();
stream.on("data", function(cssFile) {
should.exist(cssFile);
should.exist(cssFile.path);
should.exist(cssFile.relative);
should.exist(cssFile.contents);
String(cssFile.contents).should.equal(
fs.readFileSync(path.join(__dirname, 'expected/inheritance.css'), 'utf8')
);
done();
});
stream.write(sassFile);
});
it("should handle sass errors", function(done) {
var errorFile = createVinyl("error.scss");
var stream = sass();
stream.on("error", function(err) {
err.message.indexOf('property "font" must be followed by a \':\'').should.equal(0);
done();
});
stream.write(errorFile);
});
it("should work with gulp-sourcemaps", function(done) {
// TODO
done();
});
});

View File

@ -1,9 +0,0 @@
table.hl {
margin: 2em 0; }
table.hl td.ln {
text-align: right; }
li {
font-family: serif;
font-weight: bold;
font-size: 1.2em; }

3
test/scss/_partial.scss Normal file
View File

@ -0,0 +1,3 @@
body {
background: red;
}

3
test/scss/error.scss Normal file
View File

@ -0,0 +1,3 @@
body {
font 'Comic Sans';
}

View File

@ -1 +0,0 @@
@import "cats";

View File

@ -1,2 +0,0 @@
@import "cats";

View File

@ -2,5 +2,5 @@ $blue: #3bbfce;
$margin: 16px;
body {
background: pink;
background: pink;
}

View File

@ -13,4 +13,4 @@
.badError {
@extend .error;
border-width: 3px;
}
}

View File

@ -1,14 +0,0 @@
table.hl {
margin: 2em 0;
td.ln {
text-align: right;
}
}
li {
font: {
family: serif;
weight: bold;
size: 1.2em;
}
}

View File

@ -1 +0,0 @@
@import "../inheritance";

View File

@ -11,4 +11,4 @@ $margin: 16px;
padding: $margin / 2;
margin: $margin / 2;
border-color: $blue;
}
}

View File

@ -1,200 +0,0 @@
var assert = require('assert');
var gsass = require('../');
var gutil = require('gulp-util');
var fs = require('fs');
var path = require('path');
var test = require('tape');
function createVinyl(sassFileName, contents, base) {
base = base || path.join(__dirname, 'scss');
var filePath = path.join(base, sassFileName);
return new gutil.File({
cwd: __dirname,
base: base,
path: filePath,
contents: contents || fs.readFileSync(filePath)
});
}
test('pass file when isNull()', function (t) {
var stream = gsass();
var emptyFile = {
isNull: function () { return true; }
};
stream.on('data', function (data) {
t.equal(data, emptyFile);
t.end();
});
stream.write(emptyFile);
});
// test('emit error when file isStream()', function (t) {
// var stream = gsass();
// var streamFile = {
// isNull: function () { return false; },
// isStream: function () { return true; }
// };
// stream.on()
// });
test('compile a single sass file', function (t) {
var sassFile = createVinyl('mixins.scss');
var stream = gsass();
stream.on('data', function (cssFile) {
t.ok(cssFile, 'cssFile should exist');
t.ok(cssFile.path, 'cssFile.path should exist');
t.ok(cssFile.relative, 'cssFile.relative should exist');
t.ok(cssFile.contents, 'cssFile.contents should exist');
t.equal(cssFile.path, path.join(__dirname, 'scss', 'mixins.css'));
t.equal(
fs.readFileSync(path.join(__dirname, 'ref/mixins.css'), 'utf8'),
cssFile.contents.toString(),
'file compiles correctly to css'
);
t.end();
})
stream.write(sassFile);
});
test('compile a single sass file synchronously', function (t) {
var sassFile = createVinyl('mixins.scss');
var stream = gsass({sync: true});
stream.on('data', function (cssFile) {
t.ok(cssFile, 'cssFile should exist');
t.ok(cssFile.path, 'cssFile.path should exist');
t.ok(cssFile.relative, 'cssFile.relative should exist');
t.ok(cssFile.contents, 'cssFile.contents should exist');
t.equal(cssFile.path, path.join(__dirname, 'scss', 'mixins.css'));
t.equal(
fs.readFileSync(path.join(__dirname, 'ref/mixins.css'), 'utf8'),
cssFile.contents.toString(),
'file compiles correctly to css'
);
t.end();
})
stream.write(sassFile);
});
test('compile multiple sass files', function (t) {
var files = [
createVinyl('inheritance.scss'),
createVinyl('mixins.scss'),
createVinyl('nesting.scss'),
createVinyl('variables.scss')
];
t.plan(files.length * 4);
var stream = gsass();
stream.on('data', function (cssFile) {
t.ok(cssFile, 'cssFile exists');
t.ok(cssFile.path, 'cssFile.path exists');
t.ok(cssFile.relative, 'cssFile.relative exists');
t.ok(cssFile.contents, 'cssFile.contents exists');
});
files.forEach(function (file) {
stream.write(file);
});
});
test('compile multiple sass files with includePaths', function (t) {
var files = [
createVinyl('file1.scss', null, path.join(__dirname, 'scss', 'include-path-tests')),
createVinyl('file2.scss', null, path.join(__dirname, 'scss', 'include-path-tests'))
];
var options = {
includePaths: [path.resolve(__dirname, 'scss', 'includes')]
};
t.plan(files.length * 4);
var stream = gsass(options);
stream.on('data', function (cssFile) {
t.ok(cssFile, 'cssFile exists');
t.ok(cssFile.path, 'cssFile.path exists');
t.ok(cssFile.relative, 'cssFile.relative exists');
t.ok(cssFile.contents, 'cssFile.contents exists');
});
files.forEach(function (file) {
stream.write(file);
});
});
test('emit error on sass errors', function (t) {
var stream = gsass();
var errorFile = createVinyl('somefile.sass',
new Buffer('body { font \'Comic Sans\'; }'));
stream.on('error', function (err) {
t.equal(err.message,
'property "font" must be followed by a \':\''
);
t.end();
});
stream.write(errorFile);
});
test('emit error on sass errors when using sync true', function (t) {
var stream = gsass({sync: true});
var errorFile = createVinyl('somefile.sass',
new Buffer('body { font \'Comic Sans\'; }'));
stream.on('error', function (err) {
t.equal(err.message,
'property "font" must be followed by a \':\''
);
t.end();
});
stream.write(errorFile);
});
test('call custom error callback when opts.onError is given', function (t) {
var stream = gsass({ onError: function (err) {
t.equal(err.message,
'property "font" must be followed by a \':\''
);
t.end();
}});
var errorFile = createVinyl('somefile.sass',
new Buffer('body { font \'Comic Sans\'; }'));
stream.write(errorFile);
});
test('sourcemaps', function (t) {
var sassFile = createVinyl('subdir/multilevelimport.scss');
// Pretend sourcemap.init() happened by mimicking
// the object it would create.
sassFile.sourceMap = '{' +
'"version": 3,' +
'"file": "scss/subdir/multilevelimport.scss",' +
'"names": [],' +
'"mappings": "",' +
'"sources": [ "scss/subdir/multilevelimport.scss" ],' +
'"sourcesContent": [ "@import ../inheritance;" ]' +
'}';
// Expected sources are relative to file.base
var expectedSources = [
'includes/_cats.scss',
'inheritance.scss'
];
var stream = gsass();
stream.on('data', function (cssFile) {
t.deepEqual(
cssFile.sourceMap.sources,
expectedSources,
'sourcemap paths are relative to file.base'
);
t.end();
});
stream.write(sassFile);
});