UNPKG

htmlnano

Version:

Modular HTML minifier, built on top of the PostHTML

145 lines (141 loc) 6.29 kB
#!/usr/bin/env node 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();