index.js 9.6 KB

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