node-sass 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. #!/usr/bin/env node
  2. var Emitter = require('events').EventEmitter,
  3. forEach = require('async-foreach').forEach,
  4. Gaze = require('gaze'),
  5. meow = require('meow'),
  6. util = require('util'),
  7. path = require('path'),
  8. glob = require('glob'),
  9. sass = require('../lib'),
  10. render = require('../lib/render'),
  11. watcher = require('../lib/watcher'),
  12. stdout = require('stdout-stream'),
  13. stdin = require('get-stdin'),
  14. fs = require('fs');
  15. /**
  16. * Initialize CLI
  17. */
  18. var cli = meow({
  19. pkg: '../package.json',
  20. version: sass.info,
  21. help: [
  22. 'Usage:',
  23. ' node-sass [options] <input.scss>',
  24. ' cat <input.scss> | node-sass [options] > output.css',
  25. '',
  26. 'Example: Compile foobar.scss to foobar.css',
  27. ' node-sass --output-style compressed foobar.scss > foobar.css',
  28. ' cat foobar.scss | node-sass --output-style compressed > foobar.css',
  29. '',
  30. 'Example: Watch the sass directory for changes, compile with sourcemaps to the css directory',
  31. ' node-sass --watch --recursive --output css',
  32. ' --source-map true --source-map-contents sass',
  33. '',
  34. 'Options',
  35. ' -w, --watch Watch a directory or file',
  36. ' -r, --recursive Recursively watch directories or files',
  37. ' -o, --output Output directory',
  38. ' -x, --omit-source-map-url Omit source map URL comment from output',
  39. ' -i, --indented-syntax Treat data from stdin as sass code (versus scss)',
  40. ' -q, --quiet Suppress log output except on error',
  41. ' -v, --version Prints version info',
  42. ' --output-style CSS output style (nested | expanded | compact | compressed)',
  43. ' --indent-type Indent type for output CSS (space | tab)',
  44. ' --indent-width Indent width; number of spaces or tabs (maximum value: 10)',
  45. ' --linefeed Linefeed style (cr | crlf | lf | lfcr)',
  46. ' --source-comments Include debug info in output',
  47. ' --source-map Emit source map (boolean, or path to output .map file)',
  48. ' --source-map-contents Embed include contents in map',
  49. ' --source-map-embed Embed sourceMappingUrl as data URI',
  50. ' --source-map-root Base path, will be emitted in source-map as is',
  51. ' --include-path Path to look for imported files',
  52. ' --follow Follow symlinked directories',
  53. ' --precision The amount of precision allowed in decimal numbers',
  54. ' --error-bell Output a bell character on errors',
  55. ' --importer Path to .js file containing custom importer',
  56. ' --functions Path to .js file containing custom functions',
  57. ' --help Print usage info'
  58. ].join('\n')
  59. }, {
  60. boolean: [
  61. 'error-bell',
  62. 'follow',
  63. 'indented-syntax',
  64. 'omit-source-map-url',
  65. 'quiet',
  66. 'recursive',
  67. 'source-map-embed',
  68. 'source-map-contents',
  69. 'source-comments',
  70. 'watch'
  71. ],
  72. string: [
  73. 'functions',
  74. 'importer',
  75. 'include-path',
  76. 'indent-type',
  77. 'linefeed',
  78. 'output',
  79. 'output-style',
  80. 'precision',
  81. 'source-map-root'
  82. ],
  83. alias: {
  84. c: 'source-comments',
  85. i: 'indented-syntax',
  86. q: 'quiet',
  87. o: 'output',
  88. r: 'recursive',
  89. x: 'omit-source-map-url',
  90. v: 'version',
  91. w: 'watch'
  92. },
  93. default: {
  94. 'include-path': process.cwd(),
  95. 'indent-type': 'space',
  96. 'indent-width': 2,
  97. linefeed: 'lf',
  98. 'output-style': 'nested',
  99. precision: 5,
  100. quiet: false,
  101. recursive: true
  102. }
  103. });
  104. /**
  105. * Is a Directory
  106. *
  107. * @param {String} filePath
  108. * @returns {Boolean}
  109. * @api private
  110. */
  111. function isDirectory(filePath) {
  112. var isDir = false;
  113. try {
  114. var absolutePath = path.resolve(filePath);
  115. isDir = fs.statSync(absolutePath).isDirectory();
  116. } catch (e) {
  117. isDir = e.code === 'ENOENT';
  118. }
  119. return isDir;
  120. }
  121. /**
  122. * Get correct glob pattern
  123. *
  124. * @param {Object} options
  125. * @returns {String}
  126. * @api private
  127. */
  128. function globPattern(options) {
  129. return options.recursive ? '**/*.{sass,scss}' : '*.{sass,scss}';
  130. }
  131. /**
  132. * Create emitter
  133. *
  134. * @api private
  135. */
  136. function getEmitter() {
  137. var emitter = new Emitter();
  138. emitter.on('error', function(err) {
  139. if (options.errorBell) {
  140. err += '\x07';
  141. }
  142. console.error(err);
  143. if (!options.watch) {
  144. process.exit(1);
  145. }
  146. });
  147. emitter.on('warn', function(data) {
  148. if (!options.quiet) {
  149. console.warn(data);
  150. }
  151. });
  152. emitter.on('info', function(data) {
  153. if (!options.quiet) {
  154. console.info(data);
  155. }
  156. });
  157. emitter.on('log', stdout.write.bind(stdout));
  158. return emitter;
  159. }
  160. /**
  161. * Construct options
  162. *
  163. * @param {Array} arguments
  164. * @param {Object} options
  165. * @api private
  166. */
  167. function getOptions(args, options) {
  168. var cssDir, sassDir, file, mapDir;
  169. options.src = args[0];
  170. if (args[1]) {
  171. options.dest = path.resolve(args[1]);
  172. } else if (options.output) {
  173. options.dest = path.join(
  174. path.resolve(options.output),
  175. [path.basename(options.src, path.extname(options.src)), '.css'].join('')); // replace ext.
  176. }
  177. if (options.directory) {
  178. sassDir = path.resolve(options.directory);
  179. file = path.relative(sassDir, args[0]);
  180. cssDir = path.resolve(options.output);
  181. options.dest = path.join(cssDir, file).replace(path.extname(file), '.css');
  182. }
  183. if (options.sourceMap) {
  184. if(!options.sourceMapOriginal) {
  185. options.sourceMapOriginal = options.sourceMap;
  186. }
  187. // check if sourceMap path ends with .map to avoid isDirectory false-positive
  188. var sourceMapIsDirectory = options.sourceMapOriginal.indexOf('.map', options.sourceMapOriginal.length - 4) === -1 && isDirectory(options.sourceMapOriginal);
  189. if (options.sourceMapOriginal === 'true') {
  190. options.sourceMap = options.dest + '.map';
  191. } else if (!sourceMapIsDirectory) {
  192. options.sourceMap = path.resolve(options.sourceMapOriginal);
  193. } else if (sourceMapIsDirectory) {
  194. if (!options.directory) {
  195. options.sourceMap = path.resolve(options.sourceMapOriginal, path.basename(options.dest) + '.map');
  196. } else {
  197. sassDir = path.resolve(options.directory);
  198. file = path.relative(sassDir, args[0]);
  199. mapDir = path.resolve(options.sourceMapOriginal);
  200. options.sourceMap = path.join(mapDir, file).replace(path.extname(file), '.css.map');
  201. }
  202. }
  203. }
  204. return options;
  205. }
  206. /**
  207. * Watch
  208. *
  209. * @param {Object} options
  210. * @param {Object} emitter
  211. * @api private
  212. */
  213. function watch(options, emitter) {
  214. var handler = function(files) {
  215. files.added.forEach(function(file) {
  216. var watch = gaze.watched();
  217. Object.keys(watch).forEach(function (dir) {
  218. if (watch[dir].indexOf(file) !== -1) {
  219. gaze.add(file);
  220. }
  221. });
  222. });
  223. files.changed.forEach(function(file) {
  224. if (path.basename(file)[0] !== '_') {
  225. renderFile(file, options, emitter);
  226. }
  227. });
  228. files.removed.forEach(function(file) {
  229. gaze.remove(file);
  230. });
  231. };
  232. var gaze = new Gaze();
  233. gaze.add(watcher.reset(options));
  234. gaze.on('error', emitter.emit.bind(emitter, 'error'));
  235. gaze.on('changed', function(file) {
  236. handler(watcher.changed(file));
  237. });
  238. gaze.on('added', function(file) {
  239. handler(watcher.added(file));
  240. });
  241. gaze.on('deleted', function(file) {
  242. handler(watcher.removed(file));
  243. });
  244. }
  245. /**
  246. * Run
  247. *
  248. * @param {Object} options
  249. * @param {Object} emitter
  250. * @api private
  251. */
  252. function run(options, emitter) {
  253. if (!Array.isArray(options.includePath)) {
  254. options.includePath = [options.includePath];
  255. }
  256. if (options.directory) {
  257. if (!options.output) {
  258. emitter.emit('error', 'An output directory must be specified when compiling a directory');
  259. }
  260. if (!isDirectory(options.output)) {
  261. emitter.emit('error', 'An output directory must be specified when compiling a directory');
  262. }
  263. }
  264. if (options.sourceMapOriginal && options.directory && !isDirectory(options.sourceMapOriginal) && options.sourceMapOriginal !== 'true') {
  265. emitter.emit('error', 'The --source-map option must be either a boolean or directory when compiling a directory');
  266. }
  267. if (options.importer) {
  268. if ((path.resolve(options.importer) === path.normalize(options.importer).replace(/(.+)([\/|\\])$/, '$1'))) {
  269. options.importer = require(options.importer);
  270. } else {
  271. options.importer = require(path.resolve(options.importer));
  272. }
  273. }
  274. if (options.functions) {
  275. if ((path.resolve(options.functions) === path.normalize(options.functions).replace(/(.+)([\/|\\])$/, '$1'))) {
  276. options.functions = require(options.functions);
  277. } else {
  278. options.functions = require(path.resolve(options.functions));
  279. }
  280. }
  281. if (options.watch) {
  282. watch(options, emitter);
  283. } else if (options.directory) {
  284. renderDir(options, emitter);
  285. } else {
  286. render(options, emitter);
  287. }
  288. }
  289. /**
  290. * Render a file
  291. *
  292. * @param {String} file
  293. * @param {Object} options
  294. * @param {Object} emitter
  295. * @api private
  296. */
  297. function renderFile(file, options, emitter) {
  298. options = getOptions([path.resolve(file)], options);
  299. if (options.watch && !options.quiet) {
  300. emitter.emit('info', util.format('=> changed: %s', file));
  301. }
  302. render(options, emitter);
  303. }
  304. /**
  305. * Render all sass files in a directory
  306. *
  307. * @param {Object} options
  308. * @param {Object} emitter
  309. * @api private
  310. */
  311. function renderDir(options, emitter) {
  312. var globPath = path.resolve(options.directory, globPattern(options));
  313. glob(globPath, { ignore: '**/_*', follow: options.follow }, function(err, files) {
  314. if (err) {
  315. return emitter.emit('error', util.format('You do not have permission to access this path: %s.', err.path));
  316. } else if (!files.length) {
  317. return emitter.emit('error', 'No input file was found.');
  318. }
  319. forEach(files, function(subject) {
  320. emitter.once('done', this.async());
  321. renderFile(subject, options, emitter);
  322. }, function(successful, arr) {
  323. var outputDir = path.join(process.cwd(), options.output);
  324. if (!options.quiet) {
  325. emitter.emit('info', util.format('Wrote %s CSS files to %s', arr.length, outputDir));
  326. }
  327. process.exit();
  328. });
  329. });
  330. }
  331. /**
  332. * Arguments and options
  333. */
  334. var options = getOptions(cli.input, cli.flags);
  335. var emitter = getEmitter();
  336. /**
  337. * Show usage if no arguments are supplied
  338. */
  339. if (!options.src && process.stdin.isTTY) {
  340. emitter.emit('error', [
  341. 'Provide a Sass file to render',
  342. '',
  343. 'Example: Compile foobar.scss to foobar.css',
  344. ' node-sass --output-style compressed foobar.scss > foobar.css',
  345. ' cat foobar.scss | node-sass --output-style compressed > foobar.css',
  346. '',
  347. 'Example: Watch the sass directory for changes, compile with sourcemaps to the css directory',
  348. ' node-sass --watch --recursive --output css',
  349. ' --source-map true --source-map-contents sass',
  350. ].join('\n'));
  351. }
  352. /**
  353. * Apply arguments
  354. */
  355. if (options.src) {
  356. if (isDirectory(options.src)) {
  357. options.directory = options.src;
  358. }
  359. run(options, emitter);
  360. } else if (!process.stdin.isTTY) {
  361. stdin(function(data) {
  362. options.data = data;
  363. options.stdin = true;
  364. run(options, emitter);
  365. });
  366. }