mincer
Version:
Web assets processor. Native JavaScript port of Sprockets.
245 lines (194 loc) • 6.78 kB
JavaScript
/**
* class SassEngine
*
* Engine for the SASS/SCSS compiler. You will need `node-sass` Node module installed
* in order to use [[Mincer]] with `*.sass` or `*.scss` files:
*
* npm install node-sass
*
*
* ##### SUBCLASS OF
*
* [[Template]]
**/
;
// stdlib
var path = require('path');
var util = require('util');
// 3rd-party
var _ = require('lodash');
var sass; // initialized later
// internal
var Template = require('../template');
var prop = require('../common').prop;
var logger = require('../logger');
////////////////////////////////////////////////////////////////////////////////
// Class constructor
var SassEngine = module.exports = function SassEngine() {
Template.apply(this, arguments);
sass = sass || Template.libs['node-sass'] || require('node-sass');
// Ensure node sass module has renderSync method
if (!sass.renderSync) {
throw new Error('node-sass < v0.5 is not supported.');
}
};
require('util').inherits(SassEngine, Template);
// Internal (private) options storage
var options = {
sourceComments: false,
omitSourceMapUrl: true,
sourceMapContents: true
};
/**
* SassEngine.configure(opts) -> Void
* - opts (Object):
*
* Allows to set Sass compilation opts.
*
* Default: `{}`.
*
*
* ##### Example
*
* SassEngine.configure({ sourceComments: true });
**/
SassEngine.configure = function (opts) {
options = _.assign({}, options, opts);
};
// helper to generate human-friendly errors.
// adapted version from less_engine.js
function sassError(ctx /*, options*/) {
if (ctx.line && ctx.message) { // libsass 3.x error object
return new Error('Line ' + ctx.line + ': ' + ctx.message);
}
if (typeof ctx === 'string') { // libsass error string format: path:line: error: message
var error = _.zipObject(
[ 'path', 'line', 'level', 'message' ],
ctx.split(':', 4).map(function (str) { return str.trim(); })
);
if (error.line && error.level && error.message) {
return new Error('Line ' + error.line + ': ' + error.message);
}
}
return new Error(ctx);
}
// Render data
SassEngine.prototype.evaluate = function (context, locals) {
var withSourcemap = context.environment.isEnabled('source_maps');
try {
var result = sass.renderSync(_.assign({}, options, {
file: this.file,
data: this.data,
importer: this.sassImporter.bind(this, context),
functions: this.sassFunctions(locals),
includePaths: [ path.dirname(this.file) ].concat(context.environment.paths),
indentedSyntax: /^.*\.sass$/.test(this.file),
outFile: path.basename(this.file).replace(/\.(sass|scss)$/, ''),
sourceMap: withSourcemap
}));
this.data = String(result.css || result);
if (withSourcemap) {
this.map = this.normalizeSourceMap(context, String(result.map));
}
} catch (err) {
var error = sassError(err);
throw error;
}
};
SassEngine.prototype.normalizeSourceMap = function (context, map) {
map = JSON.parse(map);
var root = context.environment.root;
var base = path.dirname(this.file);
// Make sure all source paths are relative to the processed file!
map.sources = map.sources.map(function (source) {
return path.relative(base, path.join(root, source));
});
return JSON.stringify(map);
};
// Returns the argument of the @import() call relative to the asset search paths.
function importArgumentRelativeToSearchPaths(importer, importArgument, searchPaths) {
var importAbsolutePath = path.resolve(path.dirname(importer), importArgument);
var importSearchPath = _.find(searchPaths, function (path) {
return importAbsolutePath.indexOf(path) === 0;
});
if (importSearchPath) {
return path.relative(importSearchPath, importAbsolutePath);
}
}
function isFileNotFound(error) {
return error && error.code === 'FileNotFound';
}
function tryDepend(context, importPath) {
if (importPath) {
try {
context.dependOn(importPath);
} catch (error) {
return error;
}
}
}
SassEngine.prototype.sassImporter = function (context, url, prev) {
var importPath = importArgumentRelativeToSearchPaths(prev, url, context.environment.__trail__.paths);
// "If you have a SCSS or Sass file that you want to import but don't want to compile to a CSS file, you can add an
// underscore to the beginning of the filename. ... You can then import these files without using the underscore."
// https://github.com/sass/sass/blob/d26e6f/doc-src/SASS_REFERENCE.md#partials-partials
var underscoredImportPath = importPath && path.join(path.dirname(importPath), '_' + path.basename(importPath));
var firstError = tryDepend(context, importPath);
var secondError = isFileNotFound(firstError) && tryDepend(context, underscoredImportPath);
// While @import()ing assets outside of the search paths should be strongly discouraged, it is valid. Because the
// asset is outside of the search path, there's no way to call depend_on() on it, so we shouldn't throw an error.
if (isFileNotFound(firstError) && isFileNotFound(secondError)) {
logger.warn(util.format('%s will not change when %s changes, because the file could not be found.', prev, url,
firstError.message, secondError.message));
}
return {
file: url
};
};
function isSassType(obj) {
for (var type in sass.types) {
if (sass.types.hasOwnProperty(type)) {
if (obj instanceof sass.types[type]) { return true; }
}
}
return false;
}
SassEngine.prototype.sassFunctions = function (locals) {
return _.transform(locals, function (out, fn, key) {
if (!_.isFunction(fn)) {
return;
}
var functionName = key + '('
+ _.range(fn.length)
.map(function (i) {
// sass function params
// add default value to support optional param in helper
return '$_' + i + ': ""';
})
.join(',')
+ ')';
out[functionName] = function () {
// drop done() callback
var args = _.toArray(arguments).slice(0, fn.length)
.map(function ($arg) {
return $arg.getValue();
});
var result = fn.apply(null, args);
switch (typeof result) {
case 'string':
return new sass.types.String(result);
case 'number':
return new sass.types.Number(result);
case 'boolean':
return new sass.types.Boolean(result);
case 'object':
if (isSassType(result)) { return result; }
// otherwise fall through and throw!
default:
throw new Error('Unsupported value: ' + result);
}
};
});
};
// Expose default MimeType of an engine
prop(SassEngine, 'defaultMimeType', 'text/css');