index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. var fs = require('fs');
  2. var path = require('path');
  3. var CleanCSS = require('clean-css');
  4. var program = require('commander');
  5. var glob = require('glob');
  6. var COMPATIBILITY_PATTERN = /([\w\.]+)=(\w+)/g;
  7. var lineBreak = require('os').EOL;
  8. function cli(process, beforeMinifyCallback) {
  9. var packageConfig = fs.readFileSync(path.join(__dirname, 'package.json'));
  10. var buildVersion = JSON.parse(packageConfig).version;
  11. var fromStdin;
  12. var inputOptions;
  13. var options;
  14. var stdin;
  15. var data;
  16. beforeMinifyCallback = beforeMinifyCallback || Function.prototype;
  17. // Specify commander options to parse command line params correctly
  18. program
  19. .usage('[options] <source-file ...>')
  20. .option('-b, --batch', 'If enabled, optimizes input files one by one instead of joining them together')
  21. .option('-c, --compatibility [ie7|ie8]', 'Force compatibility mode (see Readme for advanced examples)')
  22. .option('-d, --debug', 'Shows debug information (minification time & compression efficiency)')
  23. .option('-f, --format <options>', 'Controls output formatting, see examples below')
  24. .option('-h, --help', 'display this help')
  25. .option('-o, --output [output-file]', 'Use [output-file] as output instead of STDOUT')
  26. .option('-O <n> [optimizations]', 'Turn on level <n> optimizations; optionally accepts a list of fine-grained options, defaults to `1`, see examples below, IMPORTANT: the prefix is O (a capital o letter), NOT a 0 (zero, a number)', function (val) { return Math.abs(parseInt(val)); })
  27. .version(buildVersion, '-v, --version')
  28. .option('--batch-suffix <suffix>', 'A suffix (without extension) appended to input file name when processing in batch mode (`-min` is the default)', '-min')
  29. .option('--inline [rules]', 'Enables inlining for listed sources (defaults to `local`)')
  30. .option('--inline-timeout [seconds]', 'Per connection timeout when fetching remote stylesheets (defaults to 5 seconds)', parseFloat)
  31. .option('--input-source-map [file]', 'Specifies the path of the input source map file')
  32. .option('--remove-inlined-files', 'Remove files inlined in <source-file ...> or via `@import` statements')
  33. .option('--source-map', 'Enables building input\'s source map')
  34. .option('--source-map-inline-sources', 'Enables inlining sources inside source maps')
  35. .option('--with-rebase', 'Enable URLs rebasing')
  36. .option('--watch', 'Runs CLI in watch mode');
  37. program.on('--help', function () {
  38. console.log('');
  39. console.log('Examples:\n');
  40. console.log(' %> cleancss one.css');
  41. console.log(' %> cleancss -o one-min.css one.css');
  42. console.log(' %> cleancss -o merged-and-minified.css one.css two.css three.css');
  43. console.log(' %> cleancss one.css two.css three.css | gzip -9 -c > merged-minified-and-gzipped.css.gz');
  44. console.log('');
  45. console.log('Formatting options:');
  46. console.log(' %> cleancss --format beautify one.css');
  47. console.log(' %> cleancss --format keep-breaks one.css');
  48. console.log(' %> cleancss --format \'indentBy:1;indentWith:tab\' one.css');
  49. console.log(' %> cleancss --format \'breaks:afterBlockBegins=on;spaces:aroundSelectorRelation=on\' one.css');
  50. console.log(' %> cleancss --format \'breaks:afterBlockBegins=2;spaces:aroundSelectorRelation=on\' one.css');
  51. console.log('');
  52. console.log('Level 0 optimizations:');
  53. console.log(' %> cleancss -O0 one.css');
  54. console.log('');
  55. console.log('Level 1 optimizations:');
  56. console.log(' %> cleancss -O1 one.css');
  57. console.log(' %> cleancss -O1 removeQuotes:off;roundingPrecision:4;specialComments:1 one.css');
  58. console.log(' %> cleancss -O1 all:off;specialComments:1 one.css');
  59. console.log('');
  60. console.log('Level 2 optimizations:');
  61. console.log(' %> cleancss -O2 one.css');
  62. console.log(' %> cleancss -O2 mergeMedia:off;restructureRules:off;mergeSemantically:on;mergeIntoShorthands:off one.css');
  63. console.log(' %> cleancss -O2 all:off;removeDuplicateRules:on one.css');
  64. process.exit();
  65. });
  66. program.parse(process.argv);
  67. inputOptions = program.opts();
  68. // If no sensible data passed in just print help and exit
  69. if (program.args.length === 0) {
  70. fromStdin = !process.env.__DIRECT__ && !process.stdin.isTTY;
  71. if (!fromStdin) {
  72. program.outputHelp();
  73. return 0;
  74. }
  75. }
  76. // Now coerce arguments into CleanCSS configuration...
  77. options = {
  78. batch: inputOptions.batch,
  79. compatibility: inputOptions.compatibility,
  80. format: inputOptions.format,
  81. inline: typeof inputOptions.inline == 'string' ? inputOptions.inline : 'local',
  82. inlineTimeout: inputOptions.inlineTimeout * 1000,
  83. level: { 1: true },
  84. output: inputOptions.output,
  85. rebase: inputOptions.withRebase ? true : false,
  86. rebaseTo: undefined,
  87. sourceMap: inputOptions.sourceMap,
  88. sourceMapInlineSources: inputOptions.sourceMapInlineSources
  89. };
  90. if (program.rawArgs.indexOf('-O0') > -1) {
  91. options.level[0] = true;
  92. }
  93. if (program.rawArgs.indexOf('-O1') > -1) {
  94. options.level[1] = findArgumentTo('-O1', program.rawArgs, program.args);
  95. }
  96. if (program.rawArgs.indexOf('-O2') > -1) {
  97. options.level[2] = findArgumentTo('-O2', program.rawArgs, program.args);
  98. }
  99. if (inputOptions.inputSourceMap && !options.sourceMap) {
  100. options.sourceMap = true;
  101. }
  102. if (options.sourceMap && !options.output && !options.batch) {
  103. outputFeedback(['Source maps will not be built because you have not specified an output file.'], true);
  104. options.sourceMap = false;
  105. }
  106. if (options.output && options.batch) {
  107. fs.mkdirSync(options.output, {recursive: true});
  108. }
  109. if (inputOptions.withRebase && ('output' in inputOptions) && inputOptions.output.length > 0) {
  110. if (isDirectory(path.resolve(inputOptions.output))) {
  111. options.rebaseTo = path.resolve(inputOptions.output);
  112. } else {
  113. options.rebaseTo = path.dirname(path.resolve(inputOptions.output));
  114. }
  115. } else {
  116. if (inputOptions.withRebase) {
  117. options.rebaseTo = process.cwd();
  118. }
  119. }
  120. var configurations = {
  121. batchSuffix: inputOptions.batchSuffix,
  122. beforeMinifyCallback: beforeMinifyCallback,
  123. debugMode: inputOptions.debug,
  124. removeInlinedFiles: inputOptions.removeInlinedFiles,
  125. inputSourceMap: inputOptions.inputSourceMap
  126. };
  127. // ... and do the magic!
  128. if (program.args.length > 0) {
  129. var expandedGlobs = expandGlobs(program.args);
  130. if (inputOptions.watch) {
  131. var inputPaths = expandedGlobs.map(function (path) { return path.expanded; });
  132. minify(process, options, configurations, expandedGlobs);
  133. require('chokidar').watch(inputPaths).on('change', function (pathToChangedFile) {
  134. console.log(`File '${pathToChangedFile}' has changed. Rerunning all optimizations...`);
  135. minify(process, options, configurations, expandedGlobs);
  136. });
  137. } else {
  138. minify(process, options, configurations, expandedGlobs);
  139. }
  140. } else {
  141. stdin = process.openStdin();
  142. stdin.setEncoding('utf-8');
  143. data = '';
  144. stdin.on('data', function (chunk) {
  145. data += chunk;
  146. });
  147. stdin.on('end', function () {
  148. minify(process, options, configurations, data);
  149. });
  150. }
  151. }
  152. function isDirectory(path) {
  153. try {
  154. return fs.statSync(path).isDirectory();
  155. } catch (e) {
  156. if (e.code == 'ENOENT') {
  157. return false;
  158. } else {
  159. throw e;
  160. }
  161. }
  162. }
  163. function findArgumentTo(option, rawArgs, args) {
  164. var value = true;
  165. var optionAt = rawArgs.indexOf(option);
  166. var nextOption = rawArgs[optionAt + 1];
  167. var looksLikePath;
  168. var asArgumentAt;
  169. if (!nextOption) {
  170. return value;
  171. }
  172. looksLikePath = nextOption.indexOf('.css') > -1 ||
  173. /\//.test(nextOption) ||
  174. /\\[^\-]/.test(nextOption) ||
  175. /^https?:\/\//.test(nextOption);
  176. asArgumentAt = args.indexOf(nextOption);
  177. if (!looksLikePath) {
  178. value = nextOption;
  179. }
  180. if (!looksLikePath && asArgumentAt > -1) {
  181. args.splice(asArgumentAt, 1);
  182. }
  183. return value;
  184. }
  185. function expandGlobs(paths) {
  186. var globPatterns = paths.filter(function (path) { return path[0] != '!'; });
  187. var ignoredGlobPatterns = paths
  188. .filter(function (path) { return path[0] == '!'; })
  189. .map(function (path) { return path.substring(1); });
  190. return globPatterns.reduce(function (accumulator, path) {
  191. var expandedWithSource =
  192. glob.sync(path, { ignore: ignoredGlobPatterns, nodir: true, nonull: true })
  193. .map(function (expandedPath) { return { expanded: expandedPath, source: path }; });
  194. return accumulator.concat(expandedWithSource);
  195. }, []);
  196. }
  197. function minify(process, options, configurations, data) {
  198. var cleanCss = new CleanCSS(options);
  199. var input = typeof(data) == 'string' ?
  200. data :
  201. data.map(function (o) { return o.expanded; });
  202. applyNonBooleanCompatibilityFlags(cleanCss, options.compatibility);
  203. configurations.beforeMinifyCallback(cleanCss);
  204. cleanCss.minify(input, getSourceMapContent(configurations.inputSourceMap), function (errors, minified) {
  205. var inputPath;
  206. var outputPath;
  207. if (options.batch && !('styles' in minified)) {
  208. for (inputPath in minified) {
  209. outputPath = options.batch && options.output ?
  210. toBatchOutputPath(inputPath, configurations.batchSuffix, options.output, data) :
  211. toSimpleOutputPath(inputPath, configurations.batchSuffix);
  212. processMinified(process, configurations, minified[inputPath], inputPath, outputPath);
  213. }
  214. } else {
  215. processMinified(process, configurations, minified, null, options.output);
  216. }
  217. });
  218. }
  219. function toSimpleOutputPath(inputPath, batchSuffix) {
  220. var extensionName = path.extname(inputPath);
  221. return inputPath.replace(new RegExp(extensionName + '$'), batchSuffix + extensionName);
  222. }
  223. function toBatchOutputPath(inputPath, batchSuffix, output, expandedWithSource) {
  224. var extensionName = path.extname(inputPath);
  225. var inputSource = expandedWithSource.find(function (ic) { return ic.expanded == inputPath; }).source;
  226. var inputSourceRoot = inputSource.indexOf('*') > -1 ?
  227. inputSource.substring(0, inputSource.indexOf('*')) :
  228. path.dirname(inputSource);
  229. return path.join(output, inputPath.replace(inputSourceRoot, '').replace(new RegExp(extensionName + '$'), batchSuffix + extensionName));
  230. }
  231. function processMinified(process, configurations, minified, inputPath, outputPath) {
  232. var mapOutputPath;
  233. if (configurations.debugMode) {
  234. if (inputPath) {
  235. console.error('File: %s', inputPath);
  236. }
  237. console.error('Original: %d bytes', minified.stats.originalSize);
  238. console.error('Minified: %d bytes', minified.stats.minifiedSize);
  239. console.error('Efficiency: %d%', ~~(minified.stats.efficiency * 10000) / 100.0);
  240. console.error('Time spent: %dms', minified.stats.timeSpent);
  241. if (minified.inlinedStylesheets.length > 0) {
  242. console.error('Inlined stylesheets:');
  243. minified.inlinedStylesheets.forEach(function (uri) {
  244. console.error('- %s', uri);
  245. });
  246. }
  247. console.error('');
  248. }
  249. outputFeedback(minified.errors, true);
  250. outputFeedback(minified.warnings);
  251. if (minified.errors.length > 0) {
  252. process.exit(1);
  253. }
  254. if (configurations.removeInlinedFiles) {
  255. minified.inlinedStylesheets.forEach(fs.unlinkSync);
  256. }
  257. if (minified.sourceMap) {
  258. mapOutputPath = outputPath + '.map';
  259. output(process, outputPath, minified.styles + lineBreak + '/*# sourceMappingURL=' + path.basename(mapOutputPath) + ' */');
  260. outputMap(mapOutputPath, minified.sourceMap);
  261. } else {
  262. output(process, outputPath, minified.styles);
  263. }
  264. }
  265. function applyNonBooleanCompatibilityFlags(cleanCss, compatibility) {
  266. var match;
  267. var scope;
  268. var parts;
  269. var i, l;
  270. if (!compatibility) {
  271. return;
  272. }
  273. patternLoop:
  274. while ((match = COMPATIBILITY_PATTERN.exec(compatibility)) !== null) {
  275. scope = cleanCss.options.compatibility;
  276. parts = match[1].split('.');
  277. for (i = 0, l = parts.length - 1; i < l; i++) {
  278. scope = scope[parts[i]];
  279. if (!scope) {
  280. continue patternLoop;
  281. }
  282. }
  283. scope[parts.pop()] = match[2];
  284. }
  285. }
  286. function outputFeedback(messages, isError) {
  287. var prefix = isError ? '\x1B[31mERROR\x1B[39m:' : 'WARNING:';
  288. messages.forEach(function (message) {
  289. console.error('%s %s', prefix, message);
  290. });
  291. }
  292. function getSourceMapContent(sourceMapPath) {
  293. if (!sourceMapPath || !fs.existsSync(sourceMapPath)) {
  294. return null;
  295. }
  296. var content = null;
  297. try {
  298. content = fs.readFileSync(sourceMapPath).toString();
  299. } catch (e) {
  300. console.error('Failed to read the input source map file.');
  301. }
  302. return content;
  303. }
  304. function output(process, outputPath, minified) {
  305. if (outputPath) {
  306. fs.mkdirSync(path.dirname(outputPath), {recursive: true});
  307. fs.writeFileSync(outputPath, minified, 'utf8');
  308. } else {
  309. process.stdout.write(minified);
  310. }
  311. }
  312. function outputMap(mapOutputPath, sourceMap) {
  313. fs.writeFileSync(mapOutputPath, sourceMap.toString(), 'utf-8');
  314. }
  315. module.exports = cli;