var fs = require('fs'); var path = require('path'); var cli = require('clap'); var SourceMapConsumer = require('source-map').SourceMapConsumer; var csso = require('./index.js'); function readFromStream(stream, minify) { var buffer = []; // FIXME: don't chain until node.js 0.10 drop, since setEncoding isn't chainable in 0.10 stream.setEncoding('utf8'); stream .on('data', function(chunk) { buffer.push(chunk); }) .on('end', function() { minify(buffer.join('')); }); } function showStat(filename, source, result, inputMap, map, time, mem) { function fmt(size) { return String(size).split('').reverse().reduce(function(size, digit, idx) { if (idx && idx % 3 === 0) { size = ' ' + size; } return digit + size; }, ''); } map = map || 0; result -= map; console.error('Source: ', filename === '' ? filename : path.relative(process.cwd(), filename)); if (inputMap) { console.error('Map source:', inputMap); } console.error('Original: ', fmt(source), 'bytes'); console.error('Compressed:', fmt(result), 'bytes', '(' + (100 * result / source).toFixed(2) + '%)'); console.error('Saving: ', fmt(source - result), 'bytes', '(' + (100 * (source - result) / source).toFixed(2) + '%)'); if (map) { console.error('Source map:', fmt(map), 'bytes', '(' + (100 * map / (result + map)).toFixed(2) + '% of total)'); console.error('Total: ', fmt(map + result), 'bytes'); } console.error('Time: ', time, 'ms'); console.error('Memory: ', (mem / (1024 * 1024)).toFixed(3), 'MB'); } function showParseError(source, filename, details, message) { function processLines(start, end) { return lines.slice(start, end).map(function(line, idx) { var num = String(start + idx + 1); while (num.length < maxNumLength) { num = ' ' + num; } return num + ' |' + line; }).join('\n'); } var lines = source.split(/\n|\r\n?|\f/); var column = details.column; var line = details.line; var startLine = Math.max(1, line - 2); var endLine = Math.min(line + 2, lines.length + 1); var maxNumLength = Math.max(4, String(endLine).length) + 1; console.error('\nParse error ' + filename + ': ' + message); console.error(processLines(startLine - 1, line)); console.error(new Array(column + maxNumLength + 2).join('-') + '^'); console.error(processLines(line, endLine)); console.error(); } function debugLevel(level) { // level is undefined when no param -> 1 return isNaN(level) ? 1 : Math.max(Number(level), 0); } function resolveSourceMap(source, inputMap, map, inputFile, outputFile) { var inputMapContent = null; var inputMapFile = null; var outputMapFile = null; switch (map) { case 'none': // don't generate source map map = false; inputMap = 'none'; break; case 'inline': // nothing to do break; case 'file': if (!outputFile) { console.error('Output filename should be specified when `--map file` is used'); process.exit(2); } outputMapFile = outputFile + '.map'; break; default: // process filename if (map) { // check path is reachable if (!fs.existsSync(path.dirname(map))) { console.error('Directory for map file should exists:', path.dirname(path.resolve(map))); process.exit(2); } // resolve to absolute path outputMapFile = path.resolve(process.cwd(), map); } } switch (inputMap) { case 'none': // nothing to do break; case 'auto': if (map) { // try fetch source map from source var inputMapComment = source.match(/\/\*# sourceMappingURL=(\S+)\s*\*\/\s*$/); if (inputFile === '') { inputFile = false; } if (inputMapComment) { // if comment found – value is filename or base64-encoded source map inputMapComment = inputMapComment[1]; if (inputMapComment.substr(0, 5) === 'data:') { // decode source map content from comment inputMapContent = new Buffer(inputMapComment.substr(inputMapComment.indexOf('base64,') + 7), 'base64').toString(); } else { // value is filename – resolve it as absolute path if (inputFile) { inputMapFile = path.resolve(path.dirname(inputFile), inputMapComment); } } } else { // comment doesn't found - look up file with `.map` extension nearby input file if (inputFile && fs.existsSync(inputFile + '.map')) { inputMapFile = inputFile + '.map'; } } } break; default: if (inputMap) { inputMapFile = inputMap; } } // source map placed in external file if (inputMapFile) { inputMapContent = fs.readFileSync(inputMapFile, 'utf8'); } return { input: inputMapContent, inputFile: inputMapFile || (inputMapContent ? '' : false), output: map, outputFile: outputMapFile }; } function processCommentsOption(value) { switch (value) { case 'exclamation': case 'first-exclamation': case 'none': return value; } console.error('Wrong value for `comments` option: %s', value); process.exit(2); } var command = cli.create('csso', '[input] [output]') .version(require('../package.json').version) .option('-i, --input ', 'Input file') .option('-o, --output ', 'Output file (result outputs to stdout if not set)') .option('-m, --map ', 'Generate source map: none (default), inline, file or ', 'none') .option('-u, --usage ', 'Usage data file') .option('--input-map ', 'Input source map: none, auto (default) or ', 'auto') .option('--restructure-off', 'Turns structure minimization off') .option('--comments ', 'Comments to keep: exclamation (default), first-exclamation or none', 'exclamation') .option('--stat', 'Output statistics in stderr') .option('--debug [level]', 'Output intermediate state of CSS during compression', debugLevel, 0) .action(function(args) { var options = this.values; var inputFile = options.input || args[0]; var outputFile = options.output || args[1]; var usageFile = options.usage; var usageData = false; var map = options.map; var inputMap = options.inputMap; var structureOptimisationOff = options.restructureOff; var comments = processCommentsOption(options.comments); var debug = options.debug; var statistics = options.stat; var inputStream; if (process.stdin.isTTY && !inputFile && !outputFile) { this.showHelp(); return; } if (!inputFile) { inputFile = ''; inputStream = process.stdin; } else { inputFile = path.resolve(process.cwd(), inputFile); inputStream = fs.createReadStream(inputFile); } if (outputFile) { outputFile = path.resolve(process.cwd(), outputFile); } if (usageFile) { if (!fs.existsSync(usageFile)) { console.error('Usage data file doesn\'t found (%s)', usageFile); process.exit(2); } usageData = fs.readFileSync(usageFile, 'utf-8'); try { usageData = JSON.parse(usageData); } catch (e) { console.error('Usage data parse error (%s)', usageFile); process.exit(2); } } readFromStream(inputStream, function(source) { var time = process.hrtime(); var mem = process.memoryUsage().heapUsed; var sourceMap = resolveSourceMap(source, inputMap, map, inputFile, outputFile); var sourceMapAnnotation = ''; var result; // main action try { result = csso.minify(source, { filename: inputFile, sourceMap: sourceMap.output, usage: usageData, restructure: !structureOptimisationOff, comments: comments, debug: debug }); // for backward capability minify returns a string if (typeof result === 'string') { result = { css: result, map: null }; } } catch (e) { if (e.parseError) { showParseError(source, inputFile, e.parseError, e.message); if (!debug) { process.exit(2); } } throw e; } if (sourceMap.output && result.map) { // apply input map if (sourceMap.input) { result.map.applySourceMap( new SourceMapConsumer(sourceMap.input), inputFile ); } // add source map to result if (sourceMap.outputFile) { // write source map to file fs.writeFileSync(sourceMap.outputFile, result.map.toString(), 'utf-8'); sourceMapAnnotation = '\n' + '/*# sourceMappingURL=' + path.relative(outputFile ? path.dirname(outputFile) : process.cwd(), sourceMap.outputFile) + ' */'; } else { // inline source map sourceMapAnnotation = '\n' + '/*# sourceMappingURL=data:application/json;base64,' + new Buffer(result.map.toString()).toString('base64') + ' */'; } result.css += sourceMapAnnotation; } // output result if (outputFile) { fs.writeFileSync(outputFile, result.css, 'utf-8'); } else { console.log(result.css); } // output statistics if (statistics) { var timeDiff = process.hrtime(time); showStat( path.relative(process.cwd(), inputFile), source.length, result.css.length, sourceMap.inputFile, sourceMapAnnotation.length, parseInt(timeDiff[0] * 1e3 + timeDiff[1] / 1e6), process.memoryUsage().heapUsed - mem ); } }); }); module.exports = { run: command.run.bind(command), isCliError: function(err) { return err instanceof cli.Error; } };