Initial commit
[yaffs-website] / node_modules / node-sass / bin / node-sass
1 #!/usr/bin/env node
2
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'),
15   fs = require('fs');
16
17 /**
18  * Initialize CLI
19  */
20
21 var cli = meow({
22   pkg: '../package.json',
23   version: sass.info,
24   help: [
25     'Usage:',
26     '  node-sass [options] <input.scss>',
27     '  cat <input.scss> | node-sass [options] > output.css',
28     '',
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',
32     '',
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',
36     '',
37     'Options',
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'
61   ].join('\n')
62 }, {
63   boolean: [
64     'error-bell',
65     'follow',
66     'indented-syntax',
67     'omit-source-map-url',
68     'quiet',
69     'recursive',
70     'source-map-embed',
71     'source-map-contents',
72     'source-comments',
73     'watch'
74   ],
75   string: [
76     'functions',
77     'importer',
78     'include-path',
79     'indent-type',
80     'linefeed',
81     'output',
82     'output-style',
83     'precision',
84     'source-map-root'
85   ],
86   alias: {
87     c: 'source-comments',
88     i: 'indented-syntax',
89     q: 'quiet',
90     o: 'output',
91     r: 'recursive',
92     x: 'omit-source-map-url',
93     v: 'version',
94     w: 'watch'
95   },
96   default: {
97     'include-path': process.cwd(),
98     'indent-type': 'space',
99     'indent-width': 2,
100     linefeed: 'lf',
101     'output-style': 'nested',
102     precision: 5,
103     quiet: false,
104     recursive: true
105   }
106 });
107
108 /**
109  * Is a Directory
110  *
111  * @param {String} filePath
112  * @returns {Boolean}
113  * @api private
114  */
115
116 function isDirectory(filePath) {
117   var isDir = false;
118   try {
119     var absolutePath = path.resolve(filePath);
120     isDir = fs.statSync(absolutePath).isDirectory();
121   } catch (e) {
122     isDir = e.code === 'ENOENT';
123   }
124   return isDir;
125 }
126
127 /**
128  * Get correct glob pattern
129  *
130  * @param {Object} options
131  * @returns {String}
132  * @api private
133  */
134
135 function globPattern(options) {
136   return options.recursive ? '**/*.{sass,scss}' : '*.{sass,scss}';
137 }
138
139 /**
140  * Create emitter
141  *
142  * @api private
143  */
144
145 function getEmitter() {
146   var emitter = new Emitter();
147
148   emitter.on('error', function(err) {
149     if (options.errorBell) {
150       err += '\x07';
151     }
152     console.error(err);
153     if (!options.watch) {
154       process.exit(1);
155     }
156   });
157
158   emitter.on('warn', function(data) {
159     if (!options.quiet) {
160       console.warn(data);
161     }
162   });
163
164   emitter.on('log', stdout.write.bind(stdout));
165
166   return emitter;
167 }
168
169 /**
170  * Construct options
171  *
172  * @param {Array} arguments
173  * @param {Object} options
174  * @api private
175  */
176
177 function getOptions(args, options) {
178   var cssDir, sassDir, file, mapDir;
179   options.src = args[0];
180
181   if (args[1]) {
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.
187   }
188
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');
194   }
195
196   if (options.sourceMap) {
197     if(!options.sourceMapOriginal) {
198       options.sourceMapOriginal = options.sourceMap;
199     }
200
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);
203
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');
211       } else {
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');
216       }
217     }
218   }
219
220   return options;
221 }
222
223 /**
224  * Watch
225  *
226  * @param {Object} options
227  * @param {Object} emitter
228  * @api private
229  */
230
231 function watch(options, emitter) {
232   var buildGraph = function(options) {
233     var graph;
234     var graphOptions = {
235       loadPaths: options.includePath,
236       extensions: ['scss', 'sass', 'css']
237     };
238
239     if (options.directory) {
240       graph = grapher.parseDir(options.directory, graphOptions);
241     } else {
242       graph = grapher.parseFile(options.src, graphOptions);
243     }
244
245     return graph;
246   };
247
248   var watch = [];
249   var graph = buildGraph(options);
250
251   // Add all files to watch list
252   for (var i in graph.index) {
253     watch.push(i);
254   }
255
256   var gaze = new Gaze();
257   gaze.add(watch);
258   gaze.on('error', emitter.emit.bind(emitter, 'error'));
259
260   gaze.on('changed', function(file) {
261     var files = [file];
262
263     // descendents may be added, so we need a new graph
264     graph = buildGraph(options);
265     graph.visitAncestors(file, function(parent) {
266       files.push(parent);
267     });
268
269     // Add children to watcher
270     graph.visitDescendents(file, function(child) {
271       if (watch.indexOf(child) === -1) {
272         watch.push(child);
273         gaze.add(child);
274       }
275     });
276     files.forEach(function(file) {
277       if (path.basename(file)[0] !== '_') {
278         renderFile(file, options, emitter);
279       }
280     });
281   });
282
283   gaze.on('added', function() {
284     graph = buildGraph(options);
285   });
286
287   gaze.on('deleted', function() {
288     graph = buildGraph(options);
289   });
290 }
291
292 /**
293  * Run
294  *
295  * @param {Object} options
296  * @param {Object} emitter
297  * @api private
298  */
299
300 function run(options, emitter) {
301   if (!Array.isArray(options.includePath)) {
302     options.includePath = [options.includePath];
303   }
304
305   if (options.directory) {
306     if (!options.output) {
307       emitter.emit('error', 'An output directory must be specified when compiling a directory');
308     }
309     if (!isDirectory(options.output)) {
310       emitter.emit('error', 'An output directory must be specified when compiling a directory');
311     }
312   }
313
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');
316   }
317
318   if (options.importer) {
319     if ((path.resolve(options.importer) === path.normalize(options.importer).replace(/(.+)([\/|\\])$/, '$1'))) {
320       options.importer = require(options.importer);
321     } else {
322       options.importer = require(path.resolve(options.importer));
323     }
324   }
325
326   if (options.functions) {
327     if ((path.resolve(options.functions) === path.normalize(options.functions).replace(/(.+)([\/|\\])$/, '$1'))) {
328       options.functions = require(options.functions);
329     } else {
330       options.functions = require(path.resolve(options.functions));
331     }
332   }
333
334   if (options.watch) {
335     watch(options, emitter);
336   } else if (options.directory) {
337     renderDir(options, emitter);
338   } else {
339     render(options, emitter);
340   }
341 }
342
343 /**
344  * Render a file
345  *
346  * @param {String} file
347  * @param {Object} options
348  * @param {Object} emitter
349  * @api private
350  */
351 function renderFile(file, options, emitter) {
352   options = getOptions([path.resolve(file)], options);
353   if (options.watch) {
354     emitter.emit('warn', util.format('=> changed: %s', file));
355   }
356   render(options, emitter);
357 }
358
359 /**
360  * Render all sass files in a directory
361  *
362  * @param {Object} options
363  * @param {Object} emitter
364  * @api private
365  */
366 function renderDir(options, emitter) {
367   var globPath = path.resolve(options.directory, globPattern(options));
368   glob(globPath, { ignore: '**/_*', follow: options.follow }, function(err, files) {
369     if (err) {
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.');
373     }
374
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));
381       process.exit();
382     });
383   });
384 }
385
386 /**
387  * Arguments and options
388  */
389
390 var options = getOptions(cli.input, cli.flags);
391 var emitter = getEmitter();
392
393 /**
394  * Show usage if no arguments are supplied
395  */
396
397 if (!options.src && process.stdin.isTTY) {
398   emitter.emit('error', [
399     'Provide a Sass file to render',
400     '',
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',
404     '',
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',
408   ].join('\n'));
409 }
410
411 /**
412  * Apply arguments
413  */
414
415 if (options.src) {
416   if (isDirectory(options.src)) {
417     options.directory = options.src;
418   }
419   run(options, emitter);
420 } else if (!process.stdin.isTTY) {
421   stdin(function(data) {
422     options.data = data;
423     options.stdin = true;
424     run(options, emitter);
425   });
426 }