node-sass 11 KB

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