3 var Emitter = require('events').EventEmitter,
4 forEach = require('async-foreach').forEach,
5 Gaze = require('gaze'),
6 grapher = require('sass-graph'),
7 meow = require('meow'),
8 util = require('util'),
9 path = require('path'),
10 glob = require('glob'),
11 sass = require('../lib'),
12 render = require('../lib/render'),
13 stdout = require('stdout-stream'),
14 stdin = require('get-stdin'),
22 pkg: '../package.json',
26 ' node-sass [options] <input.scss>',
27 ' cat <input.scss> | node-sass [options] > output.css',
29 'Example: Compile foobar.scss to foobar.css',
30 ' node-sass --output-style compressed foobar.scss > foobar.css',
31 ' cat foobar.scss | node-sass --output-style compressed > foobar.css',
33 'Example: Watch the sass directory for changes, compile with sourcemaps to the css directory',
34 ' node-sass --watch --recursive --output css',
35 ' --source-map true --source-map-contents sass',
38 ' -w, --watch Watch a directory or file',
39 ' -r, --recursive Recursively watch directories or files',
40 ' -o, --output Output directory',
41 ' -x, --omit-source-map-url Omit source map URL comment from output',
42 ' -i, --indented-syntax Treat data from stdin as sass code (versus scss)',
43 ' -q, --quiet Suppress log output except on error',
44 ' -v, --version Prints version info',
45 ' --output-style CSS output style (nested | expanded | compact | compressed)',
46 ' --indent-type Indent type for output CSS (space | tab)',
47 ' --indent-width Indent width; number of spaces or tabs (maximum value: 10)',
48 ' --linefeed Linefeed style (cr | crlf | lf | lfcr)',
49 ' --source-comments Include debug info in output',
50 ' --source-map Emit source map',
51 ' --source-map-contents Embed include contents in map',
52 ' --source-map-embed Embed sourceMappingUrl as data URI',
53 ' --source-map-root Base path, will be emitted in source-map as is',
54 ' --include-path Path to look for imported files',
55 ' --follow Follow symlinked directories',
56 ' --precision The amount of precision allowed in decimal numbers',
57 ' --error-bell Output a bell character on errors',
58 ' --importer Path to .js file containing custom importer',
59 ' --functions Path to .js file containing custom functions',
60 ' --help Print usage info'
67 'omit-source-map-url',
71 'source-map-contents',
92 x: 'omit-source-map-url',
97 'include-path': process.cwd(),
98 'indent-type': 'space',
101 'output-style': 'nested',
111 * @param {String} filePath
116 function isDirectory(filePath) {
119 var absolutePath = path.resolve(filePath);
120 isDir = fs.statSync(absolutePath).isDirectory();
122 isDir = e.code === 'ENOENT';
128 * Get correct glob pattern
130 * @param {Object} options
135 function globPattern(options) {
136 return options.recursive ? '**/*.{sass,scss}' : '*.{sass,scss}';
145 function getEmitter() {
146 var emitter = new Emitter();
148 emitter.on('error', function(err) {
149 if (options.errorBell) {
153 if (!options.watch) {
158 emitter.on('warn', function(data) {
159 if (!options.quiet) {
164 emitter.on('log', stdout.write.bind(stdout));
172 * @param {Array} arguments
173 * @param {Object} options
177 function getOptions(args, options) {
178 var cssDir, sassDir, file, mapDir;
179 options.src = args[0];
182 options.dest = path.resolve(args[1]);
183 } else if (options.output) {
184 options.dest = path.join(
185 path.resolve(options.output),
186 [path.basename(options.src, path.extname(options.src)), '.css'].join('')); // replace ext.
189 if (options.directory) {
190 sassDir = path.resolve(options.directory);
191 file = path.relative(sassDir, args[0]);
192 cssDir = path.resolve(options.output);
193 options.dest = path.join(cssDir, file).replace(path.extname(file), '.css');
196 if (options.sourceMap) {
197 if(!options.sourceMapOriginal) {
198 options.sourceMapOriginal = options.sourceMap;
201 // check if sourceMap path ends with .map to avoid isDirectory false-positive
202 var sourceMapIsDirectory = options.sourceMapOriginal.indexOf('.map', options.sourceMapOriginal.length - 4) === -1 && isDirectory(options.sourceMapOriginal);
204 if (options.sourceMapOriginal === 'true') {
205 options.sourceMap = options.dest + '.map';
206 } else if (!sourceMapIsDirectory) {
207 options.sourceMap = path.resolve(options.sourceMapOriginal);
208 } else if (sourceMapIsDirectory) {
209 if (!options.directory) {
210 options.sourceMap = path.resolve(options.sourceMapOriginal, path.basename(options.dest) + '.map');
212 sassDir = path.resolve(options.directory);
213 file = path.relative(sassDir, args[0]);
214 mapDir = path.resolve(options.sourceMapOriginal);
215 options.sourceMap = path.join(mapDir, file).replace(path.extname(file), '.css.map');
226 * @param {Object} options
227 * @param {Object} emitter
231 function watch(options, emitter) {
232 var buildGraph = function(options) {
235 loadPaths: options.includePath,
236 extensions: ['scss', 'sass', 'css']
239 if (options.directory) {
240 graph = grapher.parseDir(options.directory, graphOptions);
242 graph = grapher.parseFile(options.src, graphOptions);
249 var graph = buildGraph(options);
251 // Add all files to watch list
252 for (var i in graph.index) {
256 var gaze = new Gaze();
258 gaze.on('error', emitter.emit.bind(emitter, 'error'));
260 gaze.on('changed', function(file) {
263 // descendents may be added, so we need a new graph
264 graph = buildGraph(options);
265 graph.visitAncestors(file, function(parent) {
269 // Add children to watcher
270 graph.visitDescendents(file, function(child) {
271 if (watch.indexOf(child) === -1) {
276 files.forEach(function(file) {
277 if (path.basename(file)[0] !== '_') {
278 renderFile(file, options, emitter);
283 gaze.on('added', function() {
284 graph = buildGraph(options);
287 gaze.on('deleted', function() {
288 graph = buildGraph(options);
295 * @param {Object} options
296 * @param {Object} emitter
300 function run(options, emitter) {
301 if (!Array.isArray(options.includePath)) {
302 options.includePath = [options.includePath];
305 if (options.directory) {
306 if (!options.output) {
307 emitter.emit('error', 'An output directory must be specified when compiling a directory');
309 if (!isDirectory(options.output)) {
310 emitter.emit('error', 'An output directory must be specified when compiling a directory');
314 if (options.sourceMapOriginal && options.directory && !isDirectory(options.sourceMapOriginal) && options.sourceMapOriginal !== 'true') {
315 emitter.emit('error', 'The --source-map option must be either a boolean or directory when compiling a directory');
318 if (options.importer) {
319 if ((path.resolve(options.importer) === path.normalize(options.importer).replace(/(.+)([\/|\\])$/, '$1'))) {
320 options.importer = require(options.importer);
322 options.importer = require(path.resolve(options.importer));
326 if (options.functions) {
327 if ((path.resolve(options.functions) === path.normalize(options.functions).replace(/(.+)([\/|\\])$/, '$1'))) {
328 options.functions = require(options.functions);
330 options.functions = require(path.resolve(options.functions));
335 watch(options, emitter);
336 } else if (options.directory) {
337 renderDir(options, emitter);
339 render(options, emitter);
346 * @param {String} file
347 * @param {Object} options
348 * @param {Object} emitter
351 function renderFile(file, options, emitter) {
352 options = getOptions([path.resolve(file)], options);
354 emitter.emit('warn', util.format('=> changed: %s', file));
356 render(options, emitter);
360 * Render all sass files in a directory
362 * @param {Object} options
363 * @param {Object} emitter
366 function renderDir(options, emitter) {
367 var globPath = path.resolve(options.directory, globPattern(options));
368 glob(globPath, { ignore: '**/_*', follow: options.follow }, function(err, files) {
370 return emitter.emit('error', util.format('You do not have permission to access this path: %s.', err.path));
371 } else if (!files.length) {
372 return emitter.emit('error', 'No input file was found.');
375 forEach(files, function(subject) {
376 emitter.once('done', this.async());
377 renderFile(subject, options, emitter);
378 }, function(successful, arr) {
379 var outputDir = path.join(process.cwd(), options.output);
380 emitter.emit('warn', util.format('Wrote %s CSS files to %s', arr.length, outputDir));
387 * Arguments and options
390 var options = getOptions(cli.input, cli.flags);
391 var emitter = getEmitter();
394 * Show usage if no arguments are supplied
397 if (!options.src && process.stdin.isTTY) {
398 emitter.emit('error', [
399 'Provide a Sass file to render',
401 'Example: Compile foobar.scss to foobar.css',
402 ' node-sass --output-style compressed foobar.scss > foobar.css',
403 ' cat foobar.scss | node-sass --output-style compressed > foobar.css',
405 'Example: Watch the sass directory for changes, compile with sourcemaps to the css directory',
406 ' node-sass --watch --recursive --output css',
407 ' --source-map true --source-map-contents sass',
416 if (isDirectory(options.src)) {
417 options.directory = options.src;
419 run(options, emitter);
420 } else if (!process.stdin.isTTY) {
421 stdin(function(data) {
423 options.stdin = true;
424 run(options, emitter);