| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 | #!/usr/bin/env node/** * html-minifier-terser CLI tool * * The MIT License (MIT) * *  Copyright (c) 2014-2016 Zoltan Frombach * *  Permission is hereby granted, free of charge, to any person obtaining a copy of *  this software and associated documentation files (the "Software"), to deal in *  the Software without restriction, including without limitation the rights to *  use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of *  the Software, and to permit persons to whom the Software is furnished to do so, *  subject to the following conditions: * *  The above copyright notice and this permission notice shall be included in all *  copies or substantial portions of the Software. * *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS *  FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR *  COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER *  IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN *  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */'use strict';var camelCase = require('camel-case').camelCase;var fs = require('fs');var info = require('./package.json');var minify = require('./' + info.main).minify;var paramCase = require('param-case').paramCase;var path = require('path');var { Command } = require('commander');const program = new Command();program.name(info.name);program.version(info.version);function fatal(message) {  console.error(message);  process.exit(1);}/** * JSON does not support regexes, so, e.g., JSON.parse() will not create * a RegExp from the JSON value `[ "/matchString/" ]`, which is * technically just an array containing a string that begins and end with * a forward slash. To get a RegExp from a JSON string, it must be * constructed explicitly in JavaScript. * * The likelihood of actually wanting to match text that is enclosed in * forward slashes is probably quite rare, so if forward slashes were * included in an argument that requires a regex, the user most likely * thought they were part of the syntax for specifying a regex. * * In the unlikely case that forward slashes are indeed desired in the * search string, the user would need to enclose the expression in a * second set of slashes: * *    --customAttrSrround "[\"//matchString//\"]" */function parseRegExp(value) {  if (value) {    return new RegExp(value.replace(/^\/(.*)\/$/, '$1'));  }}function parseJSON(value) {  if (value) {    try {      return JSON.parse(value);    }    catch (e) {      if (/^{/.test(value)) {        fatal('Could not parse JSON value \'' + value + '\'');      }      return value;    }  }}function parseJSONArray(value) {  if (value) {    value = parseJSON(value);    return Array.isArray(value) ? value : [value];  }}function parseJSONRegExpArray(value) {  value = parseJSONArray(value);  return value && value.map(parseRegExp);}function parseString(value) {  return value;}var mainOptions = {  caseSensitive: 'Treat attributes in case sensitive manner (useful for SVG; e.g. viewBox)',  collapseBooleanAttributes: 'Omit attribute values from boolean attributes',  collapseInlineTagWhitespace: 'Collapse white space around inline tag',  collapseWhitespace: 'Collapse white space that contributes to text nodes in a document tree.',  conservativeCollapse: 'Always collapse to 1 space (never remove it entirely)',  continueOnParseError: 'Handle parse errors instead of aborting',  customAttrAssign: ['Arrays of regex\'es that allow to support custom attribute assign expressions (e.g. \'<div flex?="{{mode != cover}}"></div>\')', parseJSONRegExpArray],  customAttrCollapse: ['Regex that specifies custom attribute to strip newlines from (e.g. /ng-class/)', parseRegExp],  customAttrSurround: ['Arrays of regex\'es that allow to support custom attribute surround expressions (e.g. <input {{#if value}}checked="checked"{{/if}}>)', parseJSONRegExpArray],  customEventAttributes: ['Arrays of regex\'es that allow to support custom event attributes for minifyJS (e.g. ng-click)', parseJSONRegExpArray],  decodeEntities: 'Use direct Unicode characters whenever possible',  html5: 'Parse input according to HTML5 specifications',  ignoreCustomComments: ['Array of regex\'es that allow to ignore certain comments, when matched', parseJSONRegExpArray],  ignoreCustomFragments: ['Array of regex\'es that allow to ignore certain fragments, when matched (e.g. <?php ... ?>, {{ ... }})', parseJSONRegExpArray],  includeAutoGeneratedTags: 'Insert tags generated by HTML parser',  keepClosingSlash: 'Keep the trailing slash on singleton elements',  maxLineLength: ['Max line length', parseInt],  minifyCSS: ['Minify CSS in style elements and style attributes (uses clean-css)', parseJSON],  minifyJS: ['Minify Javascript in script elements and on* attributes (uses terser)', parseJSON],  minifyURLs: ['Minify URLs in various attributes (uses relateurl)', parseJSON],  noNewlinesBeforeTagClose: 'Never add a newline before a tag that closes an element',  preserveLineBreaks: 'Always collapse to 1 line break (never remove it entirely) when whitespace between tags include a line break.',  preventAttributesEscaping: 'Prevents the escaping of the values of attributes.',  processConditionalComments: 'Process contents of conditional comments through minifier',  processScripts: ['Array of strings corresponding to types of script elements to process through minifier (e.g. "text/ng-template", "text/x-handlebars-template", etc.)', parseJSONArray],  quoteCharacter: ['Type of quote to use for attribute values (\' or ")', parseString],  removeAttributeQuotes: 'Remove quotes around attributes when possible.',  removeComments: 'Strip HTML comments',  removeEmptyAttributes: 'Remove all attributes with whitespace-only values',  removeEmptyElements: 'Remove all elements with empty contents',  removeOptionalTags: 'Remove unrequired tags',  removeRedundantAttributes: 'Remove attributes when value matches default.',  removeScriptTypeAttributes: 'Removes the following attributes from script tags: text/javascript, text/ecmascript, text/jscript, application/javascript, application/x-javascript, application/ecmascript. Other type attribute values are left intact',  removeStyleLinkTypeAttributes: 'Remove type="text/css" from style and link tags. Other type attribute values are left intact.',  removeTagWhitespace: 'Remove space between attributes whenever possible',  sortAttributes: 'Sort attributes by frequency',  sortClassName: 'Sort style classes by frequency',  trimCustomFragments: 'Trim white space around ignoreCustomFragments.',  useShortDoctype: 'Replaces the doctype with the short (HTML5) doctype'};var mainOptionKeys = Object.keys(mainOptions);mainOptionKeys.forEach(function(key) {  var option = mainOptions[key];  if (Array.isArray(option)) {    key = key === 'minifyURLs' ? '--minify-urls' : '--' + paramCase(key);    key += option[1] === parseJSON ? ' [value]' : ' <value>';    program.option(key, option[0], option[1]);  }  else if (~['html5', 'includeAutoGeneratedTags'].indexOf(key)) {    program.option('--no-' + paramCase(key), option);  }  else {    program.option('--' + paramCase(key), option);  }});program.option('-o --output <file>', 'Specify output file (if not specified STDOUT will be used for output)');function readFile(file) {  try {    return fs.readFileSync(file, { encoding: 'utf8' });  }  catch (e) {    fatal('Cannot read ' + file + '\n' + e.message);  }}var config = {};program.option('-c --config-file <file>', 'Use config file', function(configPath) {  var data = readFile(configPath);  try {    config = JSON.parse(data);  }  catch (je) {    try {      config = require(path.resolve(configPath));    }    catch (ne) {      fatal('Cannot read the specified config file.\nAs JSON: ' + je.message + '\nAs module: ' + ne.message);    }  }  mainOptionKeys.forEach(function(key) {    if (key in config) {      var option = mainOptions[key];      if (Array.isArray(option)) {        var value = config[key];        config[key] = option[1](typeof value === 'string' ? value : JSON.stringify(value));      }    }  });});program.option('--input-dir <dir>', 'Specify an input directory');program.option('--output-dir <dir>', 'Specify an output directory');program.option('--file-ext <text>', 'Specify an extension to be read, ex: html');var content;program.arguments('[files...]').action(function(files) {  content = files.map(readFile).join('');}).parse(process.argv);const programOptions = program.opts();function createOptions() {  var options = {};  mainOptionKeys.forEach(function(key) {    var param = programOptions[key === 'minifyURLs' ? 'minifyUrls' : camelCase(key)];    if (typeof param !== 'undefined') {      options[key] = param;    }    else if (key in config) {      options[key] = config[key];    }  });  return options;}function mkdir(outputDir, callback) {  fs.mkdir(outputDir, function(err) {    if (err) {      switch (err.code) {        case 'ENOENT':          return mkdir(path.join(outputDir, '..'), function() {            mkdir(outputDir, callback);          });        case 'EEXIST':          break;        default:          fatal('Cannot create directory ' + outputDir + '\n' + err.message);      }    }    callback();  });}function processFile(inputFile, outputFile) {  fs.readFile(inputFile, { encoding: 'utf8' }, async function(err, data) {    if (err) {      fatal('Cannot read ' + inputFile + '\n' + err.message);    }    var minified;    try {      minified = await minify(data, createOptions());    }    catch (e) {      fatal('Minification error on ' + inputFile + '\n' + e.message);    }    fs.writeFile(outputFile, minified, { encoding: 'utf8' }, function(err) {      if (err) {        fatal('Cannot write ' + outputFile + '\n' + err.message);      }    });  });}function processDirectory(inputDir, outputDir, fileExt) {  fs.readdir(inputDir, function(err, files) {    if (err) {      fatal('Cannot read directory ' + inputDir + '\n' + err.message);    }    files.forEach(function(file) {      var inputFile = path.join(inputDir, file);      var outputFile = path.join(outputDir, file);      fs.stat(inputFile, function(err, stat) {        if (err) {          fatal('Cannot read ' + inputFile + '\n' + err.message);        }        else if (stat.isDirectory()) {          processDirectory(inputFile, outputFile, fileExt);        }        else if (!fileExt || path.extname(file) === '.' + fileExt) {          mkdir(outputDir, function() {            processFile(inputFile, outputFile);          });        }      });    });  });}async function writeMinify() {  var minified;  try {    minified = await minify(content, createOptions());  }  catch (e) {    fatal('Minification error:\n' + e.message);  }  (programOptions.output ? fs.createWriteStream(programOptions.output).on('error', function(e) {    fatal('Cannot write ' + programOptions.output + '\n' + e.message);  }) : process.stdout).write(minified);}var inputDir = programOptions.inputDir;var outputDir = programOptions.outputDir;var fileExt = programOptions.fileExt;if (inputDir || outputDir) {  if (!inputDir) {    fatal('The option output-dir needs to be used with the option input-dir. If you are working with a single file, use -o.');  }  else if (!outputDir) {    fatal('You need to specify where to write the output files with the option --output-dir');  }  processDirectory(inputDir, outputDir, fileExt);}// Minifying one or more files specified on the CMD lineelse if (content) {  writeMinify();}// Minifying input coming from STDINelse {  content = '';  process.stdin.setEncoding('utf8');  process.stdin.on('data', function(data) {    content += data;  }).on('end', writeMinify);}
 |