cli.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. var fs = require('fs');
  2. var path = require('path');
  3. var cli = require('clap');
  4. var SourceMapConsumer = require('source-map').SourceMapConsumer;
  5. var csso = require('./index.js');
  6. function readFromStream(stream, minify) {
  7. var buffer = [];
  8. // FIXME: don't chain until node.js 0.10 drop, since setEncoding isn't chainable in 0.10
  9. stream.setEncoding('utf8');
  10. stream
  11. .on('data', function(chunk) {
  12. buffer.push(chunk);
  13. })
  14. .on('end', function() {
  15. minify(buffer.join(''));
  16. });
  17. }
  18. function showStat(filename, source, result, inputMap, map, time, mem) {
  19. function fmt(size) {
  20. return String(size).split('').reverse().reduce(function(size, digit, idx) {
  21. if (idx && idx % 3 === 0) {
  22. size = ' ' + size;
  23. }
  24. return digit + size;
  25. }, '');
  26. }
  27. map = map || 0;
  28. result -= map;
  29. console.error('Source: ', filename === '<stdin>' ? filename : path.relative(process.cwd(), filename));
  30. if (inputMap) {
  31. console.error('Map source:', inputMap);
  32. }
  33. console.error('Original: ', fmt(source), 'bytes');
  34. console.error('Compressed:', fmt(result), 'bytes', '(' + (100 * result / source).toFixed(2) + '%)');
  35. console.error('Saving: ', fmt(source - result), 'bytes', '(' + (100 * (source - result) / source).toFixed(2) + '%)');
  36. if (map) {
  37. console.error('Source map:', fmt(map), 'bytes', '(' + (100 * map / (result + map)).toFixed(2) + '% of total)');
  38. console.error('Total: ', fmt(map + result), 'bytes');
  39. }
  40. console.error('Time: ', time, 'ms');
  41. console.error('Memory: ', (mem / (1024 * 1024)).toFixed(3), 'MB');
  42. }
  43. function showParseError(source, filename, details, message) {
  44. function processLines(start, end) {
  45. return lines.slice(start, end).map(function(line, idx) {
  46. var num = String(start + idx + 1);
  47. while (num.length < maxNumLength) {
  48. num = ' ' + num;
  49. }
  50. return num + ' |' + line;
  51. }).join('\n');
  52. }
  53. var lines = source.split(/\n|\r\n?|\f/);
  54. var column = details.column;
  55. var line = details.line;
  56. var startLine = Math.max(1, line - 2);
  57. var endLine = Math.min(line + 2, lines.length + 1);
  58. var maxNumLength = Math.max(4, String(endLine).length) + 1;
  59. console.error('\nParse error ' + filename + ': ' + message);
  60. console.error(processLines(startLine - 1, line));
  61. console.error(new Array(column + maxNumLength + 2).join('-') + '^');
  62. console.error(processLines(line, endLine));
  63. console.error();
  64. }
  65. function debugLevel(level) {
  66. // level is undefined when no param -> 1
  67. return isNaN(level) ? 1 : Math.max(Number(level), 0);
  68. }
  69. function resolveSourceMap(source, inputMap, map, inputFile, outputFile) {
  70. var inputMapContent = null;
  71. var inputMapFile = null;
  72. var outputMapFile = null;
  73. switch (map) {
  74. case 'none':
  75. // don't generate source map
  76. map = false;
  77. inputMap = 'none';
  78. break;
  79. case 'inline':
  80. // nothing to do
  81. break;
  82. case 'file':
  83. if (!outputFile) {
  84. console.error('Output filename should be specified when `--map file` is used');
  85. process.exit(2);
  86. }
  87. outputMapFile = outputFile + '.map';
  88. break;
  89. default:
  90. // process filename
  91. if (map) {
  92. // check path is reachable
  93. if (!fs.existsSync(path.dirname(map))) {
  94. console.error('Directory for map file should exists:', path.dirname(path.resolve(map)));
  95. process.exit(2);
  96. }
  97. // resolve to absolute path
  98. outputMapFile = path.resolve(process.cwd(), map);
  99. }
  100. }
  101. switch (inputMap) {
  102. case 'none':
  103. // nothing to do
  104. break;
  105. case 'auto':
  106. if (map) {
  107. // try fetch source map from source
  108. var inputMapComment = source.match(/\/\*# sourceMappingURL=(\S+)\s*\*\/\s*$/);
  109. if (inputFile === '<stdin>') {
  110. inputFile = false;
  111. }
  112. if (inputMapComment) {
  113. // if comment found – value is filename or base64-encoded source map
  114. inputMapComment = inputMapComment[1];
  115. if (inputMapComment.substr(0, 5) === 'data:') {
  116. // decode source map content from comment
  117. inputMapContent = new Buffer(inputMapComment.substr(inputMapComment.indexOf('base64,') + 7), 'base64').toString();
  118. } else {
  119. // value is filename – resolve it as absolute path
  120. if (inputFile) {
  121. inputMapFile = path.resolve(path.dirname(inputFile), inputMapComment);
  122. }
  123. }
  124. } else {
  125. // comment doesn't found - look up file with `.map` extension nearby input file
  126. if (inputFile && fs.existsSync(inputFile + '.map')) {
  127. inputMapFile = inputFile + '.map';
  128. }
  129. }
  130. }
  131. break;
  132. default:
  133. if (inputMap) {
  134. inputMapFile = inputMap;
  135. }
  136. }
  137. // source map placed in external file
  138. if (inputMapFile) {
  139. inputMapContent = fs.readFileSync(inputMapFile, 'utf8');
  140. }
  141. return {
  142. input: inputMapContent,
  143. inputFile: inputMapFile || (inputMapContent ? '<inline>' : false),
  144. output: map,
  145. outputFile: outputMapFile
  146. };
  147. }
  148. function processCommentsOption(value) {
  149. switch (value) {
  150. case 'exclamation':
  151. case 'first-exclamation':
  152. case 'none':
  153. return value;
  154. }
  155. console.error('Wrong value for `comments` option: %s', value);
  156. process.exit(2);
  157. }
  158. var command = cli.create('csso', '[input] [output]')
  159. .version(require('../package.json').version)
  160. .option('-i, --input <filename>', 'Input file')
  161. .option('-o, --output <filename>', 'Output file (result outputs to stdout if not set)')
  162. .option('-m, --map <destination>', 'Generate source map: none (default), inline, file or <filename>', 'none')
  163. .option('-u, --usage <filenane>', 'Usage data file')
  164. .option('--input-map <source>', 'Input source map: none, auto (default) or <filename>', 'auto')
  165. .option('--restructure-off', 'Turns structure minimization off')
  166. .option('--comments <value>', 'Comments to keep: exclamation (default), first-exclamation or none', 'exclamation')
  167. .option('--stat', 'Output statistics in stderr')
  168. .option('--debug [level]', 'Output intermediate state of CSS during compression', debugLevel, 0)
  169. .action(function(args) {
  170. var options = this.values;
  171. var inputFile = options.input || args[0];
  172. var outputFile = options.output || args[1];
  173. var usageFile = options.usage;
  174. var usageData = false;
  175. var map = options.map;
  176. var inputMap = options.inputMap;
  177. var structureOptimisationOff = options.restructureOff;
  178. var comments = processCommentsOption(options.comments);
  179. var debug = options.debug;
  180. var statistics = options.stat;
  181. var inputStream;
  182. if (process.stdin.isTTY && !inputFile && !outputFile) {
  183. this.showHelp();
  184. return;
  185. }
  186. if (!inputFile) {
  187. inputFile = '<stdin>';
  188. inputStream = process.stdin;
  189. } else {
  190. inputFile = path.resolve(process.cwd(), inputFile);
  191. inputStream = fs.createReadStream(inputFile);
  192. }
  193. if (outputFile) {
  194. outputFile = path.resolve(process.cwd(), outputFile);
  195. }
  196. if (usageFile) {
  197. if (!fs.existsSync(usageFile)) {
  198. console.error('Usage data file doesn\'t found (%s)', usageFile);
  199. process.exit(2);
  200. }
  201. usageData = fs.readFileSync(usageFile, 'utf-8');
  202. try {
  203. usageData = JSON.parse(usageData);
  204. } catch (e) {
  205. console.error('Usage data parse error (%s)', usageFile);
  206. process.exit(2);
  207. }
  208. }
  209. readFromStream(inputStream, function(source) {
  210. var time = process.hrtime();
  211. var mem = process.memoryUsage().heapUsed;
  212. var sourceMap = resolveSourceMap(source, inputMap, map, inputFile, outputFile);
  213. var sourceMapAnnotation = '';
  214. var result;
  215. // main action
  216. try {
  217. result = csso.minify(source, {
  218. filename: inputFile,
  219. sourceMap: sourceMap.output,
  220. usage: usageData,
  221. restructure: !structureOptimisationOff,
  222. comments: comments,
  223. debug: debug
  224. });
  225. // for backward capability minify returns a string
  226. if (typeof result === 'string') {
  227. result = {
  228. css: result,
  229. map: null
  230. };
  231. }
  232. } catch (e) {
  233. if (e.parseError) {
  234. showParseError(source, inputFile, e.parseError, e.message);
  235. if (!debug) {
  236. process.exit(2);
  237. }
  238. }
  239. throw e;
  240. }
  241. if (sourceMap.output && result.map) {
  242. // apply input map
  243. if (sourceMap.input) {
  244. result.map.applySourceMap(
  245. new SourceMapConsumer(sourceMap.input),
  246. inputFile
  247. );
  248. }
  249. // add source map to result
  250. if (sourceMap.outputFile) {
  251. // write source map to file
  252. fs.writeFileSync(sourceMap.outputFile, result.map.toString(), 'utf-8');
  253. sourceMapAnnotation = '\n' +
  254. '/*# sourceMappingURL=' +
  255. path.relative(outputFile ? path.dirname(outputFile) : process.cwd(), sourceMap.outputFile) +
  256. ' */';
  257. } else {
  258. // inline source map
  259. sourceMapAnnotation = '\n' +
  260. '/*# sourceMappingURL=data:application/json;base64,' +
  261. new Buffer(result.map.toString()).toString('base64') +
  262. ' */';
  263. }
  264. result.css += sourceMapAnnotation;
  265. }
  266. // output result
  267. if (outputFile) {
  268. fs.writeFileSync(outputFile, result.css, 'utf-8');
  269. } else {
  270. console.log(result.css);
  271. }
  272. // output statistics
  273. if (statistics) {
  274. var timeDiff = process.hrtime(time);
  275. showStat(
  276. path.relative(process.cwd(), inputFile),
  277. source.length,
  278. result.css.length,
  279. sourceMap.inputFile,
  280. sourceMapAnnotation.length,
  281. parseInt(timeDiff[0] * 1e3 + timeDiff[1] / 1e6),
  282. process.memoryUsage().heapUsed - mem
  283. );
  284. }
  285. });
  286. });
  287. module.exports = {
  288. run: command.run.bind(command),
  289. isCliError: function(err) {
  290. return err instanceof cli.Error;
  291. }
  292. };