var fs = require('fs'); var path = require('path'); var cli = require('clap'); var csso = require('csso'); var SourceMapConsumer = require('source-map').SourceMapConsumer; function unixPathname(pathname) { return pathname.replace(/\\/g, '/'); } function readFromStream(stream, minify) { var buffer = []; stream .setEncoding('utf8') .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, outputMap, inputFile, outputFile) { var inputMapContent = null; var inputMapFile = null; var outputMapFile = null; switch (outputMap) { case 'none': // don't generate source map outputMap = false; inputMap = 'none'; break; case 'inline': // nothing to do break; case 'file': if (!outputFile) { console.error('Output filename should be specified when `--source-map file` is used'); process.exit(2); } outputMapFile = outputFile + '.map'; break; default: // process filename if (outputMap) { // check path is reachable if (!fs.existsSync(path.dirname(outputMap))) { console.error('Directory for map file should exists:', path.dirname(path.resolve(outputMap))); process.exit(2); } // resolve to absolute path outputMapFile = path.resolve(process.cwd(), outputMap); } } switch (inputMap) { case 'none': // nothing to do break; case 'auto': if (outputMap) { // 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 = Buffer.from(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: outputMap, 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); } function processOptions(options, args) { var inputFile = options.input || args[0]; var outputFile = options.output; var usageFile = options.usage; var usageData = false; var sourceMap = options.sourceMap; var inputSourceMap = options.inputSourceMap; var declarationList = options.declarationList; var restructure = Boolean(options.restructure); var forceMediaMerge = Boolean(options.forceMediaMerge); var comments = processCommentsOption(options.comments); var debug = options.debug; var statistics = options.stat; var watch = options.watch; if (process.stdin.isTTY && !inputFile && !outputFile) { return null; } if (!inputFile) { inputFile = ''; } else { inputFile = path.resolve(process.cwd(), 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); } } return { inputFile: inputFile, outputFile: outputFile, usageData: usageData, sourceMap: sourceMap, inputSourceMap: inputSourceMap, declarationList: declarationList, restructure: restructure, forceMediaMerge: forceMediaMerge, comments: comments, statistics: statistics, debug: debug, watch: watch }; } function minifyStream(options) { var inputStream = options.inputFile !== '' ? fs.createReadStream(options.inputFile) : process.stdin; readFromStream(inputStream, function(source) { var time = process.hrtime(); var mem = process.memoryUsage().heapUsed; var relInputFilename = path.relative(process.cwd(), options.inputFile); var sourceMap = resolveSourceMap( source, options.inputSourceMap, options.sourceMap, options.inputFile, options.outputFile ); var sourceMapAnnotation = ''; var result; // main action try { var minifyFunc = options.declarationList ? csso.minifyBlock : csso.minify; result = minifyFunc(source, { filename: unixPathname(relInputFilename), sourceMap: Boolean(sourceMap.output), usage: options.usageData, restructure: options.restructure, forceMediaMerge: options.forceMediaMerge, comments: options.comments, debug: options.debug }); // for backward capability minify returns a string if (typeof result === 'string') { result = { css: result, map: null }; } } catch (e) { if (e.parseError) { showParseError(source, options.inputFile, e.parseError, e.message); if (!options.debug) { process.exit(2); } } throw e; } if (sourceMap.output && result.map) { // apply input map if (sourceMap.input) { result.map.applySourceMap( new SourceMapConsumer(sourceMap.input), unixPathname(relInputFilename) ); } // 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=' + unixPathname(path.relative(options.outputFile ? path.dirname(options.outputFile) : process.cwd(), sourceMap.outputFile)) + ' */'; } else { // inline source map sourceMapAnnotation = '\n' + '/*# sourceMappingURL=data:application/json;base64,' + Buffer.from(result.map.toString()).toString('base64') + ' */'; } result.css += sourceMapAnnotation; } // output result if (options.outputFile) { fs.writeFileSync(options.outputFile, result.css, 'utf-8'); } else { console.log(result.css); } // output statistics if (options.statistics) { var timeDiff = process.hrtime(time); showStat( relInputFilename, source.length, result.css.length, sourceMap.inputFile, sourceMapAnnotation.length, parseInt(timeDiff[0] * 1e3 + timeDiff[1] / 1e6, 10), process.memoryUsage().heapUsed - mem ); } }); } var command = cli.create('csso', '[input]') .version(require('csso/package.json').version) .option('-i, --input ', 'Input file') .option('-o, --output ', 'Output file (result outputs to stdout if not set)') .option('-s, --source-map ', 'Generate source map: none (default), inline, file or ', 'none') .option('-u, --usage ', 'Usage data file') .option('--input-source-map ', 'Input source map: none, auto (default) or ', 'auto') .option('-d, --declaration-list', 'Treat input as a declaration list') .option('--no-restructure', 'Disable structural optimisations') .option('--force-media-merge', 'Enable unsafe merge of @media rules') .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 a compression', debugLevel, 0) .option('--watch', 'Watch source file for changes') .action(function(args) { var options = processOptions(this.values, args); if (options === null) { this.showHelp(); return; } minifyStream(options); // enable watch mode only when input is a file if (options.watch && options.inputFile !== '') { // NOTE: require chokidar here to keep down start up time when --watch doesn't use // (yep, chokidar adds a penalty ~0.2-0.3s on its init) require('chokidar').watch(options.inputFile).on('change', function() { minifyStream(options); }); } }); module.exports = { run: command.run.bind(command), isCliError: function(err) { return err instanceof cli.Error; } };