htmlnano
Version:
Modular HTML minifier, built on top of the PostHTML
145 lines (141 loc) • 6.29 kB
JavaScript
var commander = require('commander');
var fs = require('fs');
var path = require('path');
var process = require('process');
var tinyglobby = require('tinyglobby');
var index_js = require('./index.js');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var fs__default = /*#__PURE__*/_interopDefault(fs);
var path__default = /*#__PURE__*/_interopDefault(path);
var process__default = /*#__PURE__*/_interopDefault(process);
function fail(message) {
process__default.default.stderr.write(`${message}\n`);
process__default.default.exit(1);
}
// Expand the positional inputs into a concrete list of files.
// Glob patterns are expanded by the CLI itself so that shells that don't
// expand globs (e.g. Windows) behave the same as POSIX shells.
function resolveInputs(inputs) {
const resolved = [];
const seen = new Set();
for (const rawInput of inputs){
const isGlob = /[*?[\]{}]/.test(rawInput);
if (isGlob) {
const matches = tinyglobby.globSync(rawInput, {
absolute: false
});
if (matches.length === 0) {
fail(`No files matched the pattern: ${rawInput}`);
}
for (const match of matches){
addInput(match);
}
} else {
if (!fs__default.default.existsSync(rawInput)) {
fail(`Input file does not exist: ${rawInput}`);
}
addInput(rawInput);
}
}
return resolved;
function addInput(file) {
const key = path__default.default.resolve(file);
if (!seen.has(key)) {
seen.add(key);
resolved.push(file);
}
}
}
// The base directory used to preserve the relative structure when writing to
// --output-dir. It is the deepest common parent directory of all inputs, so
// e.g. inputs `pages/a.html` and `pages/nested/b.html` are written under
// <outputDir>/a.html and <outputDir>/nested/b.html.
function commonBaseDir(files) {
const dirs = files.map((file)=>path__default.default.resolve(path__default.default.dirname(file)).split(path__default.default.sep));
let common = dirs[0];
for (const parts of dirs.slice(1)){
const next = [];
for(let i = 0; i < Math.min(common.length, parts.length); i++){
if (common[i] === parts[i]) {
next.push(common[i]);
} else {
break;
}
}
common = next;
}
return common.join(path__default.default.sep) || path__default.default.sep;
}
async function minify(html, options, chosenPreset) {
const htmlnanoOptions = {};
if (options.config) {
htmlnanoOptions.configPath = options.config;
}
const result = await index_js.process(html, htmlnanoOptions, chosenPreset);
return result.html;
}
// Emit a single result either to the -o/--output file or to STDOUT.
function emitSingle(minified, output) {
if (output !== undefined && output !== '-') {
fs__default.default.writeFileSync(output, minified);
} else {
process__default.default.stdout.write(minified);
}
}
commander.program.name('htmlnano').description('Minify HTML with htmlnano').argument('[inputs...]', 'input files or glob patterns (use "-" or omit for STDIN)').option('-o, --output <file>', 'output file (single input only)', '-').option('-d, --output-dir <dir>', 'write each input into <dir>, preserving relative structure').option('--in-place', 'rewrite each input file in place').option('-p, --preset <preset>', 'preset to use', 'safe').option('-c, --config <file>', 'path to config file').action(async (inputs, options)=>{
const { preset } = options;
if (!preset || !(preset in index_js.presets)) {
const available = Object.keys(index_js.presets).join(', ');
fail(`Unknown preset: ${preset}. Available presets: ${available}`);
}
const chosenPreset = index_js.presets[preset];
const outputDirSet = options.outputDir !== undefined;
const outputSet = options.output !== undefined && options.output !== '-';
if (options.inPlace && (outputSet || outputDirSet)) {
fail('--in-place cannot be combined with -o/--output or -d/--output-dir');
}
if (outputSet && outputDirSet) {
fail('-o/--output cannot be combined with -d/--output-dir');
}
// STDIN mode: no inputs or the explicit "-" placeholder.
const positionals = inputs.filter((input)=>input !== '-');
const stdinMode = positionals.length === 0;
if (stdinMode) {
if (outputDirSet || options.inPlace) {
fail('--output-dir and --in-place require at least one input file');
}
const html = fs__default.default.readFileSync(0, 'utf8');
emitSingle(await minify(html, options, chosenPreset), options.output);
return;
}
const files = resolveInputs(positionals);
// Single input keeps the original -o/STDOUT behaviour for backwards compat.
if (files.length === 1 && !outputDirSet && !options.inPlace) {
const html = fs__default.default.readFileSync(files[0], 'utf8');
emitSingle(await minify(html, options, chosenPreset), options.output);
return;
}
// Multiple inputs require an explicit destination strategy.
if (!outputDirSet && !options.inPlace) {
fail('Multiple input files require --output-dir <dir> or --in-place');
}
const base = outputDirSet ? commonBaseDir(files) : '';
for (const file of files){
const html = fs__default.default.readFileSync(file, 'utf8');
const minified = await minify(html, options, chosenPreset);
let destination;
if (options.inPlace) {
destination = file;
} else {
const relative = path__default.default.relative(base, path__default.default.resolve(file));
destination = path__default.default.join(options.outputDir, relative);
fs__default.default.mkdirSync(path__default.default.dirname(destination), {
recursive: true
});
}
fs__default.default.writeFileSync(destination, minified);
process__default.default.stderr.write(`${file} -> ${destination}\n`);
}
});
commander.program.parse();