node-sass 11 KB

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