securityos/node_modules/.bin/html-minifier-terser

309 lines
12 KiB
JavaScript
Executable File

#!/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.
*
*/
import fs from 'fs';
import path from 'path';
import { createRequire } from 'module';
import { camelCase } from 'camel-case';
import { paramCase } from 'param-case';
import { Command } from 'commander';
import { minify } from './src/htmlminifier.js';
const require = createRequire(import.meta.url);
const pkg = require('./package.json');
const program = new Command();
program.name(pkg.name);
program.version(pkg.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;
}
const 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'
};
// configure commandline flags
const mainOptionKeys = Object.keys(mainOptions);
mainOptionKeys.forEach(function (key) {
const 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);
}
}
let config = {};
program.option('-c --config-file <file>', 'Use config file', function (configPath) {
const 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) {
const option = mainOptions[key];
if (Array.isArray(option)) {
const 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');
let content;
program.arguments('[files...]').action(function (files) {
content = files.map(readFile).join('');
}).parse(process.argv);
const programOptions = program.opts();
function createOptions() {
const options = {};
mainOptionKeys.forEach(function (key) {
const 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, { recursive: true }, function (err) {
if (err) {
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);
}
let 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) {
const inputFile = path.join(inputDir, file);
const 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);
});
}
});
});
});
}
const writeMinify = async () => {
const minifierOptions = createOptions();
let minified;
try {
minified = await minify(content, minifierOptions);
} catch (e) {
fatal('Minification error:\n' + e.message);
}
let stream = process.stdout;
if (programOptions.output) {
stream = fs.createWriteStream(programOptions.output)
.on('error', (e) => {
fatal('Cannot write ' + programOptions.output + '\n' + e.message);
});
}
stream.write(minified);
};
const { inputDir, outputDir, fileExt } = programOptions;
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);
} else if (content) { // Minifying one or more files specified on the CMD line
writeMinify();
} else { // Minifying input coming from STDIN
content = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', function (data) {
content += data;
}).on('end', writeMinify);
}