node-sass 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. #!/usr/bin/env node
  2. var Emitter = require('events').EventEmitter,
  3. path = require('path'),
  4. Gaze = require('gaze'),
  5. meow = require('meow'),
  6. replaceExt = require('replace-ext'),
  7. stdin = require('get-stdin'),
  8. grapher = require('sass-graph'),
  9. render = require('../lib/render');
  10. /**
  11. * Initialize CLI
  12. */
  13. var cli = meow({
  14. pkg: '../package.json',
  15. help: [
  16. require('../lib/').info(),
  17. '',
  18. 'Usage',
  19. ' node-sass [options] <input.scss> [output.css]',
  20. ' cat <input.scss> | node-sass > output.css',
  21. '',
  22. 'Example',
  23. ' node-sass --output-style compressed foobar.scss foobar.css',
  24. ' cat foobar.scss | node-sass --output-style compressed > foobar.css',
  25. '',
  26. 'Options',
  27. ' -w, --watch Watch a directory or file',
  28. ' -r, --recursive Recursively watch directories or files',
  29. ' -o, --output Output directory',
  30. ' -x, --omit-source-map-url Omit source map URL comment from output',
  31. ' -i, --indented-syntax Treat data from stdin as sass code (versus scss)',
  32. ' --output-style CSS output style (nested|expanded|compact|compressed)',
  33. ' --source-comments Include debug info in output',
  34. ' --source-map Emit source map',
  35. ' --source-map-embed Embed sourceMappingUrl as data URI',
  36. ' --source-map-contents Embed include contents in map',
  37. ' --include-path Path to look for imported files',
  38. ' --image-path Path to prepend when using the `image-url()` helper',
  39. ' --precision The amount of precision allowed in decimal numbers',
  40. ' --stdout Print the resulting CSS to stdout',
  41. ' --importer Path to custom importer',
  42. ' --help Print usage info'
  43. ].join('\n')
  44. }, {
  45. boolean: [
  46. 'indented-syntax',
  47. 'omit-source-map-url',
  48. 'recursive',
  49. 'stdout',
  50. 'source-map-embed',
  51. 'source-map-contents',
  52. 'source-comments'
  53. ],
  54. string: [
  55. 'image-path',
  56. 'include-path',
  57. 'output',
  58. 'output-style',
  59. 'precision'
  60. ],
  61. alias: {
  62. i: 'indented-syntax',
  63. o: 'output',
  64. w: 'watch',
  65. x: 'omit-source-map-url',
  66. c: 'source-comments',
  67. r: 'recursive'
  68. },
  69. default: {
  70. 'image-path': '',
  71. 'include-path': process.cwd(),
  72. 'output-style': 'nested',
  73. precision: 5
  74. }
  75. });
  76. /**
  77. * Check if file is a Sass file
  78. *
  79. * @param {String} file
  80. * @api private
  81. */
  82. function isSassFile(file) {
  83. return file.match(/\.(sass|scss)/);
  84. }
  85. /**
  86. * Create emitter
  87. *
  88. * @api private
  89. */
  90. function getEmitter() {
  91. var emitter = new Emitter();
  92. emitter.on('error', function(err) {
  93. console.error(err);
  94. if (!options.watch) {
  95. process.exit(1);
  96. }
  97. });
  98. emitter.on('warn', function(data){
  99. console.warn(data);
  100. });
  101. emitter.on('log', function(data){
  102. console.log(data);
  103. });
  104. emitter.on('done', function() {
  105. if (!options.watch) {
  106. process.exit;
  107. }
  108. });
  109. return emitter;
  110. }
  111. /**
  112. * Construct options
  113. *
  114. * @param {Array} arguments
  115. * @param {Object} options
  116. * @api private
  117. */
  118. function getOptions(args, options) {
  119. var dir = options.output ? path.resolve(process.cwd(), options.output) : process.cwd();
  120. options.src = args[0];
  121. options.dest = args[1] ? path.resolve(dir, args[1]) : undefined;
  122. if (!options.dest && !options.stdout) {
  123. var ext = path.extname(options.src);
  124. var out = path.basename(options.src);
  125. var suffix = '.css';
  126. if (ext !== suffix) {
  127. out = replaceExt(out, suffix);
  128. }
  129. options.dest = path.join(dir, out);
  130. }
  131. return options;
  132. }
  133. /**
  134. * Watch
  135. *
  136. * @param {Object} options
  137. * @param {Object} emitter
  138. * @api private
  139. */
  140. function watch(options, emitter) {
  141. var dir = options.watch;
  142. var gaze = new Gaze();
  143. if (dir === true) {
  144. dir = [];
  145. } else if (!Array.isArray(dir)) {
  146. dir = [dir];
  147. }
  148. dir.push(options.src);
  149. dir = dir.map(function(d) {
  150. var glob = options.recursive ? '**/*.{sass,scss}' : '*.{sass,scss}';
  151. return isSassFile(d) ? d : path.join(d, glob);
  152. });
  153. gaze.add(dir);
  154. gaze.on('error', emitter.emit.bind(emitter, 'error'));
  155. var graph = grapher.parseDir(options.src, { loadPaths: options.includePath });
  156. gaze.on('changed', function(file) {
  157. var files = [file];
  158. graph.visitAncestors(file, function(parent) {
  159. files.push(parent);
  160. });
  161. files.forEach(function(file) {
  162. if (path.basename(file)[0] === '_') return;
  163. options = getOptions([path.resolve(file)], options);
  164. emitter.emit('warn', '=> changed: ' + file);
  165. render(options, emitter);
  166. });
  167. });
  168. }
  169. /**
  170. * Run
  171. *
  172. * @param {Object} options
  173. * @param {Object} emitter
  174. * @api private
  175. */
  176. function run(options, emitter) {
  177. if (!Array.isArray(options.includePath)) {
  178. options.includePath = [options.includePath];
  179. }
  180. if (options.sourceMap) {
  181. if (options.sourceMap === 'true') {
  182. options.sourceMap = options.dest + '.map';
  183. } else {
  184. options.sourceMap = path.resolve(process.cwd(), options.sourceMap);
  185. }
  186. }
  187. if (options.importer) {
  188. if ((path.resolve(options.importer) === path.normalize(options.importer).replace(/(.+)([\/|\\])$/, '$1'))) {
  189. options.importer = require(options.importer);
  190. } else {
  191. options.importer = require(path.resolve(process.cwd(), options.importer));
  192. }
  193. }
  194. if (options.watch) {
  195. watch(options, emitter);
  196. } else {
  197. render(options, emitter);
  198. }
  199. }
  200. /**
  201. * Arguments and options
  202. */
  203. var input = cli.input;
  204. var options = getOptions(input, cli.flags);
  205. var emitter = getEmitter();
  206. /**
  207. * Show usage if no arguments are supplied
  208. */
  209. if (!input.length && process.stdin.isTTY) {
  210. emitter.emit('error', [
  211. 'Provide a Sass file to render',
  212. '',
  213. ' Example',
  214. ' node-sass --output-style compressed foobar.scss foobar.css',
  215. ' cat foobar.scss | node-sass --output-style compressed > foobar.css'
  216. ].join('\n'));
  217. }
  218. /**
  219. * Apply arguments
  220. */
  221. if (options.src) {
  222. run(options, emitter);
  223. } else if (!process.stdin.isTTY) {
  224. stdin(function(data) {
  225. options.data = data;
  226. options.stdin = true;
  227. run(options, emitter);
  228. });
  229. }
  230. return emitter;