413 lines
11 KiB
Text
413 lines
11 KiB
Text
|
#!/usr/bin/env node
|
||
|
|
||
|
var Emitter = require('events').EventEmitter,
|
||
|
forEach = require('async-foreach').forEach,
|
||
|
Gaze = require('gaze'),
|
||
|
meow = require('meow'),
|
||
|
util = require('util'),
|
||
|
path = require('path'),
|
||
|
glob = require('glob'),
|
||
|
sass = require('../lib'),
|
||
|
render = require('../lib/render'),
|
||
|
watcher = require('../lib/watcher'),
|
||
|
stdout = require('stdout-stream'),
|
||
|
stdin = require('get-stdin'),
|
||
|
fs = require('fs');
|
||
|
|
||
|
/**
|
||
|
* Initialize CLI
|
||
|
*/
|
||
|
|
||
|
var cli = meow({
|
||
|
pkg: '../package.json',
|
||
|
version: sass.info,
|
||
|
help: [
|
||
|
'Usage:',
|
||
|
' node-sass [options] <input.scss>',
|
||
|
' cat <input.scss> | node-sass [options] > output.css',
|
||
|
'',
|
||
|
'Example: Compile foobar.scss to foobar.css',
|
||
|
' node-sass --output-style compressed foobar.scss > foobar.css',
|
||
|
' cat foobar.scss | node-sass --output-style compressed > foobar.css',
|
||
|
'',
|
||
|
'Example: Watch the sass directory for changes, compile with sourcemaps to the css directory',
|
||
|
' node-sass --watch --recursive --output css',
|
||
|
' --source-map true --source-map-contents sass',
|
||
|
'',
|
||
|
'Options',
|
||
|
' -w, --watch Watch a directory or file',
|
||
|
' -r, --recursive Recursively watch directories or files',
|
||
|
' -o, --output Output directory',
|
||
|
' -x, --omit-source-map-url Omit source map URL comment from output',
|
||
|
' -i, --indented-syntax Treat data from stdin as sass code (versus scss)',
|
||
|
' -q, --quiet Suppress log output except on error',
|
||
|
' -v, --version Prints version info',
|
||
|
' --output-style CSS output style (nested | expanded | compact | compressed)',
|
||
|
' --indent-type Indent type for output CSS (space | tab)',
|
||
|
' --indent-width Indent width; number of spaces or tabs (maximum value: 10)',
|
||
|
' --linefeed Linefeed style (cr | crlf | lf | lfcr)',
|
||
|
' --source-comments Include debug info in output',
|
||
|
' --source-map Emit source map (boolean, or path to output .map file)',
|
||
|
' --source-map-contents Embed include contents in map',
|
||
|
' --source-map-embed Embed sourceMappingUrl as data URI',
|
||
|
' --source-map-root Base path, will be emitted in source-map as is',
|
||
|
' --include-path Path to look for imported files',
|
||
|
' --follow Follow symlinked directories',
|
||
|
' --precision The amount of precision allowed in decimal numbers',
|
||
|
' --error-bell Output a bell character on errors',
|
||
|
' --importer Path to .js file containing custom importer',
|
||
|
' --functions Path to .js file containing custom functions',
|
||
|
' --help Print usage info'
|
||
|
].join('\n')
|
||
|
}, {
|
||
|
boolean: [
|
||
|
'error-bell',
|
||
|
'follow',
|
||
|
'indented-syntax',
|
||
|
'omit-source-map-url',
|
||
|
'quiet',
|
||
|
'recursive',
|
||
|
'source-map-embed',
|
||
|
'source-map-contents',
|
||
|
'source-comments',
|
||
|
'watch'
|
||
|
],
|
||
|
string: [
|
||
|
'functions',
|
||
|
'importer',
|
||
|
'include-path',
|
||
|
'indent-type',
|
||
|
'linefeed',
|
||
|
'output',
|
||
|
'output-style',
|
||
|
'precision',
|
||
|
'source-map-root'
|
||
|
],
|
||
|
alias: {
|
||
|
c: 'source-comments',
|
||
|
i: 'indented-syntax',
|
||
|
q: 'quiet',
|
||
|
o: 'output',
|
||
|
r: 'recursive',
|
||
|
x: 'omit-source-map-url',
|
||
|
v: 'version',
|
||
|
w: 'watch'
|
||
|
},
|
||
|
default: {
|
||
|
'include-path': process.cwd(),
|
||
|
'indent-type': 'space',
|
||
|
'indent-width': 2,
|
||
|
linefeed: 'lf',
|
||
|
'output-style': 'nested',
|
||
|
precision: 5,
|
||
|
quiet: false,
|
||
|
recursive: true
|
||
|
}
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* Is a Directory
|
||
|
*
|
||
|
* @param {String} filePath
|
||
|
* @returns {Boolean}
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function isDirectory(filePath) {
|
||
|
var isDir = false;
|
||
|
try {
|
||
|
var absolutePath = path.resolve(filePath);
|
||
|
isDir = fs.statSync(absolutePath).isDirectory();
|
||
|
} catch (e) {
|
||
|
isDir = e.code === 'ENOENT';
|
||
|
}
|
||
|
return isDir;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get correct glob pattern
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @returns {String}
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function globPattern(options) {
|
||
|
return options.recursive ? '**/*.{sass,scss}' : '*.{sass,scss}';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create emitter
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function getEmitter() {
|
||
|
var emitter = new Emitter();
|
||
|
|
||
|
emitter.on('error', function(err) {
|
||
|
if (options.errorBell) {
|
||
|
err += '\x07';
|
||
|
}
|
||
|
console.error(err);
|
||
|
if (!options.watch) {
|
||
|
process.exit(1);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
emitter.on('warn', function(data) {
|
||
|
if (!options.quiet) {
|
||
|
console.warn(data);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
emitter.on('info', function(data) {
|
||
|
if (!options.quiet) {
|
||
|
console.info(data);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
emitter.on('log', stdout.write.bind(stdout));
|
||
|
|
||
|
return emitter;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Construct options
|
||
|
*
|
||
|
* @param {Array} arguments
|
||
|
* @param {Object} options
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function getOptions(args, options) {
|
||
|
var cssDir, sassDir, file, mapDir;
|
||
|
options.src = args[0];
|
||
|
|
||
|
if (args[1]) {
|
||
|
options.dest = path.resolve(args[1]);
|
||
|
} else if (options.output) {
|
||
|
options.dest = path.join(
|
||
|
path.resolve(options.output),
|
||
|
[path.basename(options.src, path.extname(options.src)), '.css'].join('')); // replace ext.
|
||
|
}
|
||
|
|
||
|
if (options.directory) {
|
||
|
sassDir = path.resolve(options.directory);
|
||
|
file = path.relative(sassDir, args[0]);
|
||
|
cssDir = path.resolve(options.output);
|
||
|
options.dest = path.join(cssDir, file).replace(path.extname(file), '.css');
|
||
|
}
|
||
|
|
||
|
if (options.sourceMap) {
|
||
|
if(!options.sourceMapOriginal) {
|
||
|
options.sourceMapOriginal = options.sourceMap;
|
||
|
}
|
||
|
|
||
|
// check if sourceMap path ends with .map to avoid isDirectory false-positive
|
||
|
var sourceMapIsDirectory = options.sourceMapOriginal.indexOf('.map', options.sourceMapOriginal.length - 4) === -1 && isDirectory(options.sourceMapOriginal);
|
||
|
|
||
|
if (options.sourceMapOriginal === 'true') {
|
||
|
options.sourceMap = options.dest + '.map';
|
||
|
} else if (!sourceMapIsDirectory) {
|
||
|
options.sourceMap = path.resolve(options.sourceMapOriginal);
|
||
|
} else if (sourceMapIsDirectory) {
|
||
|
if (!options.directory) {
|
||
|
options.sourceMap = path.resolve(options.sourceMapOriginal, path.basename(options.dest) + '.map');
|
||
|
} else {
|
||
|
sassDir = path.resolve(options.directory);
|
||
|
file = path.relative(sassDir, args[0]);
|
||
|
mapDir = path.resolve(options.sourceMapOriginal);
|
||
|
options.sourceMap = path.join(mapDir, file).replace(path.extname(file), '.css.map');
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return options;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Watch
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @param {Object} emitter
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function watch(options, emitter) {
|
||
|
var handler = function(files) {
|
||
|
files.added.forEach(function(file) {
|
||
|
var watch = gaze.watched();
|
||
|
Object.keys(watch).forEach(function (dir) {
|
||
|
if (watch[dir].indexOf(file) !== -1) {
|
||
|
gaze.add(file);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
files.changed.forEach(function(file) {
|
||
|
if (path.basename(file)[0] !== '_') {
|
||
|
renderFile(file, options, emitter);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
files.removed.forEach(function(file) {
|
||
|
gaze.remove(file);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
var gaze = new Gaze();
|
||
|
gaze.add(watcher.reset(options));
|
||
|
gaze.on('error', emitter.emit.bind(emitter, 'error'));
|
||
|
|
||
|
gaze.on('changed', function(file) {
|
||
|
handler(watcher.changed(file));
|
||
|
});
|
||
|
|
||
|
gaze.on('added', function(file) {
|
||
|
handler(watcher.added(file));
|
||
|
});
|
||
|
|
||
|
gaze.on('deleted', function(file) {
|
||
|
handler(watcher.removed(file));
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Run
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @param {Object} emitter
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function run(options, emitter) {
|
||
|
if (!Array.isArray(options.includePath)) {
|
||
|
options.includePath = [options.includePath];
|
||
|
}
|
||
|
|
||
|
if (options.directory) {
|
||
|
if (!options.output) {
|
||
|
emitter.emit('error', 'An output directory must be specified when compiling a directory');
|
||
|
}
|
||
|
if (!isDirectory(options.output)) {
|
||
|
emitter.emit('error', 'An output directory must be specified when compiling a directory');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (options.sourceMapOriginal && options.directory && !isDirectory(options.sourceMapOriginal) && options.sourceMapOriginal !== 'true') {
|
||
|
emitter.emit('error', 'The --source-map option must be either a boolean or directory when compiling a directory');
|
||
|
}
|
||
|
|
||
|
if (options.importer) {
|
||
|
if ((path.resolve(options.importer) === path.normalize(options.importer).replace(/(.+)([\/|\\])$/, '$1'))) {
|
||
|
options.importer = require(options.importer);
|
||
|
} else {
|
||
|
options.importer = require(path.resolve(options.importer));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (options.functions) {
|
||
|
if ((path.resolve(options.functions) === path.normalize(options.functions).replace(/(.+)([\/|\\])$/, '$1'))) {
|
||
|
options.functions = require(options.functions);
|
||
|
} else {
|
||
|
options.functions = require(path.resolve(options.functions));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (options.watch) {
|
||
|
watch(options, emitter);
|
||
|
} else if (options.directory) {
|
||
|
renderDir(options, emitter);
|
||
|
} else {
|
||
|
render(options, emitter);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Render a file
|
||
|
*
|
||
|
* @param {String} file
|
||
|
* @param {Object} options
|
||
|
* @param {Object} emitter
|
||
|
* @api private
|
||
|
*/
|
||
|
function renderFile(file, options, emitter) {
|
||
|
options = getOptions([path.resolve(file)], options);
|
||
|
if (options.watch && !options.quiet) {
|
||
|
emitter.emit('info', util.format('=> changed: %s', file));
|
||
|
}
|
||
|
render(options, emitter);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Render all sass files in a directory
|
||
|
*
|
||
|
* @param {Object} options
|
||
|
* @param {Object} emitter
|
||
|
* @api private
|
||
|
*/
|
||
|
function renderDir(options, emitter) {
|
||
|
var globPath = path.resolve(options.directory, globPattern(options));
|
||
|
glob(globPath, { ignore: '**/_*', follow: options.follow }, function(err, files) {
|
||
|
if (err) {
|
||
|
return emitter.emit('error', util.format('You do not have permission to access this path: %s.', err.path));
|
||
|
} else if (!files.length) {
|
||
|
return emitter.emit('error', 'No input file was found.');
|
||
|
}
|
||
|
|
||
|
forEach(files, function(subject) {
|
||
|
emitter.once('done', this.async());
|
||
|
renderFile(subject, options, emitter);
|
||
|
}, function(successful, arr) {
|
||
|
var outputDir = path.join(process.cwd(), options.output);
|
||
|
if (!options.quiet) {
|
||
|
emitter.emit('info', util.format('Wrote %s CSS files to %s', arr.length, outputDir));
|
||
|
}
|
||
|
process.exit();
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Arguments and options
|
||
|
*/
|
||
|
|
||
|
var options = getOptions(cli.input, cli.flags);
|
||
|
var emitter = getEmitter();
|
||
|
|
||
|
/**
|
||
|
* Show usage if no arguments are supplied
|
||
|
*/
|
||
|
|
||
|
if (!options.src && process.stdin.isTTY) {
|
||
|
emitter.emit('error', [
|
||
|
'Provide a Sass file to render',
|
||
|
'',
|
||
|
'Example: Compile foobar.scss to foobar.css',
|
||
|
' node-sass --output-style compressed foobar.scss > foobar.css',
|
||
|
' cat foobar.scss | node-sass --output-style compressed > foobar.css',
|
||
|
'',
|
||
|
'Example: Watch the sass directory for changes, compile with sourcemaps to the css directory',
|
||
|
' node-sass --watch --recursive --output css',
|
||
|
' --source-map true --source-map-contents sass',
|
||
|
].join('\n'));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Apply arguments
|
||
|
*/
|
||
|
|
||
|
if (options.src) {
|
||
|
if (isDirectory(options.src)) {
|
||
|
options.directory = options.src;
|
||
|
}
|
||
|
run(options, emitter);
|
||
|
} else if (!process.stdin.isTTY) {
|
||
|
stdin(function(data) {
|
||
|
options.data = data;
|
||
|
options.stdin = true;
|
||
|
run(options, emitter);
|
||
|
});
|
||
|
}
|