index.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. /*!
  2. * node-sass: lib/index.js
  3. */
  4. var path = require('path'),
  5. clonedeep = require('lodash.clonedeep'),
  6. assign = require('lodash.assign'),
  7. sass = require('./extensions');
  8. /**
  9. * Require binding
  10. */
  11. var binding = require('./binding')(sass);
  12. /**
  13. * Get input file
  14. *
  15. * @param {Object} options
  16. * @api private
  17. */
  18. function getInputFile(options) {
  19. return options.file ? path.resolve(options.file) : null;
  20. }
  21. /**
  22. * Get output file
  23. *
  24. * @param {Object} options
  25. * @api private
  26. */
  27. function getOutputFile(options) {
  28. var outFile = options.outFile;
  29. if (!outFile || typeof outFile !== 'string' || (!options.data && !options.file)) {
  30. return null;
  31. }
  32. return path.resolve(outFile);
  33. }
  34. /**
  35. * Get source map
  36. *
  37. * @param {Object} options
  38. * @api private
  39. */
  40. function getSourceMap(options) {
  41. var sourceMap = options.sourceMap;
  42. if (sourceMap && typeof sourceMap !== 'string' && options.outFile) {
  43. sourceMap = options.outFile + '.map';
  44. }
  45. return sourceMap && typeof sourceMap === 'string' ? path.resolve(sourceMap) : null;
  46. }
  47. /**
  48. * Get stats
  49. *
  50. * @param {Object} options
  51. * @api private
  52. */
  53. function getStats(options) {
  54. var stats = {};
  55. stats.entry = options.file || 'data';
  56. stats.start = Date.now();
  57. return stats;
  58. }
  59. /**
  60. * End stats
  61. *
  62. * @param {Object} stats
  63. * @param {Object} sourceMap
  64. * @api private
  65. */
  66. function endStats(stats) {
  67. stats.end = Date.now();
  68. stats.duration = stats.end - stats.start;
  69. return stats;
  70. }
  71. /**
  72. * Get style
  73. *
  74. * @param {Object} options
  75. * @api private
  76. */
  77. function getStyle(options) {
  78. var styles = {
  79. nested: 0,
  80. expanded: 1,
  81. compact: 2,
  82. compressed: 3
  83. };
  84. return styles[options.outputStyle] || 0;
  85. }
  86. /**
  87. * Get indent width
  88. *
  89. * @param {Object} options
  90. * @api private
  91. */
  92. function getIndentWidth(options) {
  93. var width = parseInt(options.indentWidth) || 2;
  94. return width > 10 ? 2 : width;
  95. }
  96. /**
  97. * Get indent type
  98. *
  99. * @param {Object} options
  100. * @api private
  101. */
  102. function getIndentType(options) {
  103. var types = {
  104. space: 0,
  105. tab: 1
  106. };
  107. return types[options.indentType] || 0;
  108. }
  109. /**
  110. * Get linefeed
  111. *
  112. * @param {Object} options
  113. * @api private
  114. */
  115. function getLinefeed(options) {
  116. var feeds = {
  117. cr: '\r',
  118. crlf: '\r\n',
  119. lf: '\n',
  120. lfcr: '\n\r'
  121. };
  122. return feeds[options.linefeed] || '\n';
  123. }
  124. /**
  125. * Build an includePaths string
  126. * from the options.includePaths array and the SASS_PATH environment variable
  127. *
  128. * @param {Object} options
  129. * @api private
  130. */
  131. function buildIncludePaths(options) {
  132. options.includePaths = options.includePaths || [];
  133. if (process.env.hasOwnProperty('SASS_PATH')) {
  134. options.includePaths = options.includePaths.concat(
  135. process.env.SASS_PATH.split(path.delimiter)
  136. );
  137. }
  138. return options.includePaths.join(path.delimiter);
  139. }
  140. /**
  141. * Get options
  142. *
  143. * @param {Object} options
  144. * @api private
  145. */
  146. function getOptions(opts, cb) {
  147. if (typeof opts !== 'object') {
  148. throw new Error('Invalid: options is not an object.');
  149. }
  150. var options = clonedeep(opts || {});
  151. options.sourceComments = options.sourceComments || false;
  152. if (options.hasOwnProperty('file')) {
  153. options.file = getInputFile(options);
  154. }
  155. options.outFile = getOutputFile(options);
  156. options.includePaths = buildIncludePaths(options);
  157. options.precision = parseInt(options.precision) || 5;
  158. options.sourceMap = getSourceMap(options);
  159. options.style = getStyle(options);
  160. options.indentWidth = getIndentWidth(options);
  161. options.indentType = getIndentType(options);
  162. options.linefeed = getLinefeed(options);
  163. // context object represents node-sass environment
  164. options.context = { options: options, callback: cb };
  165. options.result = {
  166. stats: getStats(options)
  167. };
  168. return options;
  169. }
  170. /**
  171. * Executes a callback and transforms any exception raised into a sass error
  172. *
  173. * @param {Function} callback
  174. * @param {Array} arguments
  175. * @api private
  176. */
  177. function tryCallback(callback, args) {
  178. try {
  179. return callback.apply(this, args);
  180. } catch (e) {
  181. if (typeof e === 'string') {
  182. return new binding.types.Error(e);
  183. } else if (e instanceof Error) {
  184. return new binding.types.Error(e.message);
  185. } else {
  186. return new binding.types.Error('An unexpected error occurred');
  187. }
  188. }
  189. }
  190. /**
  191. * Normalizes the signature of custom functions to make it possible to just supply the
  192. * function name and have the signature default to `fn(...)`. The callback is adjusted
  193. * to transform the input sass list into discrete arguments.
  194. *
  195. * @param {String} signature
  196. * @param {Function} callback
  197. * @return {Object}
  198. * @api private
  199. */
  200. function normalizeFunctionSignature(signature, callback) {
  201. if (!/^\*|@warn|@error|@debug|\w+\(.*\)$/.test(signature)) {
  202. if (!/\w+/.test(signature)) {
  203. throw new Error('Invalid function signature format "' + signature + '"');
  204. }
  205. return {
  206. signature: signature + '(...)',
  207. callback: function() {
  208. var args = Array.prototype.slice.call(arguments),
  209. list = args.shift(),
  210. i;
  211. for (i = list.getLength() - 1; i >= 0; i--) {
  212. args.unshift(list.getValue(i));
  213. }
  214. return callback.apply(this, args);
  215. }
  216. };
  217. }
  218. return {
  219. signature: signature,
  220. callback: callback
  221. };
  222. }
  223. /**
  224. * Render
  225. *
  226. * @param {Object} options
  227. * @api public
  228. */
  229. module.exports.render = function(opts, cb) {
  230. var options = getOptions(opts, cb);
  231. // options.error and options.success are for libsass binding
  232. options.error = function(err) {
  233. var payload = assign(new Error(), JSON.parse(err));
  234. if (cb) {
  235. options.context.callback.call(options.context, payload, null);
  236. }
  237. };
  238. options.success = function() {
  239. var result = options.result;
  240. var stats = endStats(result.stats);
  241. var payload = {
  242. css: result.css,
  243. map: result.map,
  244. stats: stats
  245. };
  246. if (cb) {
  247. options.context.callback.call(options.context, null, payload);
  248. }
  249. };
  250. var importer = options.importer;
  251. if (importer) {
  252. if (Array.isArray(importer)) {
  253. options.importer = [];
  254. importer.forEach(function(subject, index) {
  255. options.importer[index] = function(file, prev, bridge) {
  256. function done(result) {
  257. bridge.success(result === module.exports.NULL ? null : result);
  258. }
  259. var result = subject.call(options.context, file, prev, done);
  260. if (result !== undefined) {
  261. done(result);
  262. }
  263. };
  264. });
  265. } else {
  266. options.importer = function(file, prev, bridge) {
  267. function done(result) {
  268. bridge.success(result === module.exports.NULL ? null : result);
  269. }
  270. var result = importer.call(options.context, file, prev, done);
  271. if (result !== undefined) {
  272. done(result);
  273. }
  274. };
  275. }
  276. }
  277. var functions = clonedeep(options.functions);
  278. if (functions) {
  279. options.functions = {};
  280. Object.keys(functions).forEach(function(subject) {
  281. var cb = normalizeFunctionSignature(subject, functions[subject]);
  282. options.functions[cb.signature] = function() {
  283. var args = Array.prototype.slice.call(arguments),
  284. bridge = args.pop();
  285. function done(data) {
  286. bridge.success(data);
  287. }
  288. var result = tryCallback(cb.callback.bind(options.context), args.concat(done));
  289. if (result) {
  290. done(result);
  291. }
  292. };
  293. });
  294. }
  295. if (options.data) {
  296. binding.render(options);
  297. } else if (options.file) {
  298. binding.renderFile(options);
  299. } else {
  300. cb({status: 3, message: 'No input specified: provide a file name or a source string to process' });
  301. }
  302. };
  303. /**
  304. * Render sync
  305. *
  306. * @param {Object} options
  307. * @api public
  308. */
  309. module.exports.renderSync = function(opts) {
  310. var options = getOptions(opts);
  311. var importer = options.importer;
  312. if (importer) {
  313. if (Array.isArray(importer)) {
  314. options.importer = [];
  315. importer.forEach(function(subject, index) {
  316. options.importer[index] = function(file, prev) {
  317. var result = subject.call(options.context, file, prev);
  318. return result === module.exports.NULL ? null : result;
  319. };
  320. });
  321. } else {
  322. options.importer = function(file, prev) {
  323. var result = importer.call(options.context, file, prev);
  324. return result === module.exports.NULL ? null : result;
  325. };
  326. }
  327. }
  328. var functions = clonedeep(options.functions);
  329. if (options.functions) {
  330. options.functions = {};
  331. Object.keys(functions).forEach(function(signature) {
  332. var cb = normalizeFunctionSignature(signature, functions[signature]);
  333. options.functions[cb.signature] = function() {
  334. return tryCallback(cb.callback.bind(options.context), arguments);
  335. };
  336. });
  337. }
  338. var status;
  339. if (options.data) {
  340. status = binding.renderSync(options);
  341. } else if (options.file) {
  342. status = binding.renderFileSync(options);
  343. } else {
  344. throw new Error('No input specified: provide a file name or a source string to process');
  345. }
  346. var result = options.result;
  347. if (status) {
  348. result.stats = endStats(result.stats);
  349. return result;
  350. }
  351. throw assign(new Error(), JSON.parse(result.error));
  352. };
  353. /**
  354. * API Info
  355. *
  356. * @api public
  357. */
  358. module.exports.info = sass.getVersionInfo(binding);
  359. /**
  360. * Expose sass types
  361. */
  362. module.exports.types = binding.types;
  363. module.exports.TRUE = binding.types.Boolean.TRUE;
  364. module.exports.FALSE = binding.types.Boolean.FALSE;
  365. module.exports.NULL = binding.types.Null.NULL;
  366. /**
  367. * Polyfill the old API
  368. *
  369. * TODO: remove for 4.0
  370. */
  371. function processSassDeprecationMessage() {
  372. console.log('Deprecation warning: `process.sass` is an undocumented internal that will be removed in future versions of Node Sass.');
  373. }
  374. process.sass = process.sass || {
  375. get versionInfo() { processSassDeprecationMessage(); return module.exports.info; },
  376. get binaryName() { processSassDeprecationMessage(); return sass.getBinaryName(); },
  377. get binaryUrl() { processSassDeprecationMessage(); return sass.getBinaryUrl(); },
  378. get binaryPath() { processSassDeprecationMessage(); return sass.getBinaryPath(); },
  379. get getBinaryPath() { processSassDeprecationMessage(); return sass.getBinaryPath; },
  380. };