index.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  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. // Preserve the behaviour people have come to expect.
  139. // This behaviour was removed from Sass in 3.4 and
  140. // LibSass in 3.5.
  141. options.includePaths.unshift(process.cwd());
  142. return options.includePaths.join(path.delimiter);
  143. }
  144. /**
  145. * Get options
  146. *
  147. * @param {Object} options
  148. * @api private
  149. */
  150. function getOptions(opts, cb) {
  151. if (typeof opts !== 'object') {
  152. throw new Error('Invalid: options is not an object.');
  153. }
  154. var options = clonedeep(opts || {});
  155. options.sourceComments = options.sourceComments || false;
  156. if (options.hasOwnProperty('file')) {
  157. options.file = getInputFile(options);
  158. }
  159. options.outFile = getOutputFile(options);
  160. options.includePaths = buildIncludePaths(options);
  161. options.precision = parseInt(options.precision) || 5;
  162. options.sourceMap = getSourceMap(options);
  163. options.style = getStyle(options);
  164. options.indentWidth = getIndentWidth(options);
  165. options.indentType = getIndentType(options);
  166. options.linefeed = getLinefeed(options);
  167. // context object represents node-sass environment
  168. options.context = { options: options, callback: cb };
  169. options.result = {
  170. stats: getStats(options)
  171. };
  172. return options;
  173. }
  174. /**
  175. * Executes a callback and transforms any exception raised into a sass error
  176. *
  177. * @param {Function} callback
  178. * @param {Array} arguments
  179. * @api private
  180. */
  181. function tryCallback(callback, args) {
  182. try {
  183. return callback.apply(this, args);
  184. } catch (e) {
  185. if (typeof e === 'string') {
  186. return new binding.types.Error(e);
  187. } else if (e instanceof Error) {
  188. return new binding.types.Error(e.message);
  189. } else {
  190. return new binding.types.Error('An unexpected error occurred');
  191. }
  192. }
  193. }
  194. /**
  195. * Normalizes the signature of custom functions to make it possible to just supply the
  196. * function name and have the signature default to `fn(...)`. The callback is adjusted
  197. * to transform the input sass list into discrete arguments.
  198. *
  199. * @param {String} signature
  200. * @param {Function} callback
  201. * @return {Object}
  202. * @api private
  203. */
  204. function normalizeFunctionSignature(signature, callback) {
  205. if (!/^\*|@warn|@error|@debug|\w+\(.*\)$/.test(signature)) {
  206. if (!/\w+/.test(signature)) {
  207. throw new Error('Invalid function signature format "' + signature + '"');
  208. }
  209. return {
  210. signature: signature + '(...)',
  211. callback: function() {
  212. var args = Array.prototype.slice.call(arguments),
  213. list = args.shift(),
  214. i;
  215. for (i = list.getLength() - 1; i >= 0; i--) {
  216. args.unshift(list.getValue(i));
  217. }
  218. return callback.apply(this, args);
  219. }
  220. };
  221. }
  222. return {
  223. signature: signature,
  224. callback: callback
  225. };
  226. }
  227. /**
  228. * Render
  229. *
  230. * @param {Object} options
  231. * @api public
  232. */
  233. module.exports.render = function(opts, cb) {
  234. var options = getOptions(opts, cb);
  235. // options.error and options.success are for libsass binding
  236. options.error = function(err) {
  237. var payload = assign(new Error(), JSON.parse(err));
  238. if (cb) {
  239. options.context.callback.call(options.context, payload, null);
  240. }
  241. };
  242. options.success = function() {
  243. var result = options.result;
  244. var stats = endStats(result.stats);
  245. var payload = {
  246. css: result.css,
  247. map: result.map,
  248. stats: stats
  249. };
  250. if (cb) {
  251. options.context.callback.call(options.context, null, payload);
  252. }
  253. };
  254. var importer = options.importer;
  255. if (importer) {
  256. if (Array.isArray(importer)) {
  257. options.importer = [];
  258. importer.forEach(function(subject, index) {
  259. options.importer[index] = function(file, prev, bridge) {
  260. function done(result) {
  261. bridge.success(result === module.exports.NULL ? null : result);
  262. }
  263. var result = subject.call(options.context, file, prev, done);
  264. if (result !== undefined) {
  265. done(result);
  266. }
  267. };
  268. });
  269. } else {
  270. options.importer = function(file, prev, bridge) {
  271. function done(result) {
  272. bridge.success(result === module.exports.NULL ? null : result);
  273. }
  274. var result = importer.call(options.context, file, prev, done);
  275. if (result !== undefined) {
  276. done(result);
  277. }
  278. };
  279. }
  280. }
  281. var functions = clonedeep(options.functions);
  282. if (functions) {
  283. options.functions = {};
  284. Object.keys(functions).forEach(function(subject) {
  285. var cb = normalizeFunctionSignature(subject, functions[subject]);
  286. options.functions[cb.signature] = function() {
  287. var args = Array.prototype.slice.call(arguments),
  288. bridge = args.pop();
  289. function done(data) {
  290. bridge.success(data);
  291. }
  292. var result = tryCallback(cb.callback.bind(options.context), args.concat(done));
  293. if (result) {
  294. done(result);
  295. }
  296. };
  297. });
  298. }
  299. if (options.data) {
  300. binding.render(options);
  301. } else if (options.file) {
  302. binding.renderFile(options);
  303. } else {
  304. cb({status: 3, message: 'No input specified: provide a file name or a source string to process' });
  305. }
  306. };
  307. /**
  308. * Render sync
  309. *
  310. * @param {Object} options
  311. * @api public
  312. */
  313. module.exports.renderSync = function(opts) {
  314. var options = getOptions(opts);
  315. var importer = options.importer;
  316. if (importer) {
  317. if (Array.isArray(importer)) {
  318. options.importer = [];
  319. importer.forEach(function(subject, index) {
  320. options.importer[index] = function(file, prev) {
  321. var result = subject.call(options.context, file, prev);
  322. return result === module.exports.NULL ? null : result;
  323. };
  324. });
  325. } else {
  326. options.importer = function(file, prev) {
  327. var result = importer.call(options.context, file, prev);
  328. return result === module.exports.NULL ? null : result;
  329. };
  330. }
  331. }
  332. var functions = clonedeep(options.functions);
  333. if (options.functions) {
  334. options.functions = {};
  335. Object.keys(functions).forEach(function(signature) {
  336. var cb = normalizeFunctionSignature(signature, functions[signature]);
  337. options.functions[cb.signature] = function() {
  338. return tryCallback(cb.callback.bind(options.context), arguments);
  339. };
  340. });
  341. }
  342. var status;
  343. if (options.data) {
  344. status = binding.renderSync(options);
  345. } else if (options.file) {
  346. status = binding.renderFileSync(options);
  347. } else {
  348. throw new Error('No input specified: provide a file name or a source string to process');
  349. }
  350. var result = options.result;
  351. if (status) {
  352. result.stats = endStats(result.stats);
  353. return result;
  354. }
  355. throw assign(new Error(), JSON.parse(result.error));
  356. };
  357. /**
  358. * API Info
  359. *
  360. * @api public
  361. */
  362. module.exports.info = sass.getVersionInfo(binding);
  363. /**
  364. * Expose sass types
  365. */
  366. module.exports.types = binding.types;
  367. module.exports.TRUE = binding.types.Boolean.TRUE;
  368. module.exports.FALSE = binding.types.Boolean.FALSE;
  369. module.exports.NULL = binding.types.Null.NULL;
  370. /**
  371. * Polyfill the old API
  372. *
  373. * TODO: remove for 4.0
  374. */
  375. function processSassDeprecationMessage() {
  376. console.log('Deprecation warning: `process.sass` is an undocumented internal that will be removed in future versions of Node Sass.');
  377. }
  378. process.sass = process.sass || {
  379. get versionInfo() { processSassDeprecationMessage(); return module.exports.info; },
  380. get binaryName() { processSassDeprecationMessage(); return sass.getBinaryName(); },
  381. get binaryUrl() { processSassDeprecationMessage(); return sass.getBinaryUrl(); },
  382. get binaryPath() { processSassDeprecationMessage(); return sass.getBinaryPath(); },
  383. get getBinaryPath() { processSassDeprecationMessage(); return sass.getBinaryPath; },
  384. };