html-minifier-next 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright 2010–2019 Juriy “kangax” Zaytsev
4
+
5
+ Copyright Terser
6
+
7
+ Copyright Jens Oliver Meiert
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # HTML Minifier Next (HTMLMinifier)
2
+
3
+ <!-- [![Build Status](https://github.com/j9t/html-minifier-next/workflows/CI/badge.svg)](https://github.com/j9t/html-minifier-next/actions?workflow=CI) -->
4
+
5
+ (This project is based on [Terser’s html-minifier-terser](https://github.com/terser/html-minifier-terser), which in turn is based on [Juriy Zaytsev’s html-minifier](https://github.com/kangax/html-minifier). It was set up because as of May 2025, both html-minifier-terser and html-minifier seem unmaintained. **This project is currently under test.** If it seems maintainable to me, [Jens](https://meiert.com/), even without community support, the project will be updated and documented further. The following documentation largely matches the original project.)
6
+
7
+ HTMLMinifier is a highly **configurable, well-tested, JavaScript-based HTML minifier**.
8
+
9
+ ## Installation
10
+
11
+ From npm for use as a command line app:
12
+
13
+ ```shell
14
+ npm i -g html-minifier-next
15
+ ```
16
+
17
+ From npm for programmatic use:
18
+
19
+ ```shell
20
+ npm i html-minifier-next
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ **Note** that almost all options are disabled by default. Experiment and find what works best for you and your project.
26
+
27
+ For command line usage please see `html-minifier-next --help` for a list of available options.
28
+
29
+ **Sample command line:**
30
+
31
+ ```bash
32
+ html-minifier-next --collapse-whitespace --remove-comments --minify-js true
33
+ ```
34
+
35
+ ### Node.js
36
+
37
+ ```js
38
+ const { minify } = require('html-minifier-next');
39
+
40
+ const result = await minify('<p title="blah" id="moo">foo</p>', {
41
+ removeAttributeQuotes: true,
42
+ });
43
+ result; // '<p title=blah id=moo>foo</p>'
44
+ ```
45
+
46
+ See [the origina blog post](http://perfectionkills.com/experimenting-with-html-minifier) for details of [how it works](http://perfectionkills.com/experimenting-with-html-minifier#how_it_works), [description of each option](http://perfectionkills.com/experimenting-with-html-minifier#options), [testing results](http://perfectionkills.com/experimenting-with-html-minifier#field_testing), and [conclusions](http://perfectionkills.com/experimenting-with-html-minifier#cost_and_benefits).
47
+
48
+ For lint-like capabilities take a look at [HTMLLint](https://github.com/kangax/html-lint).
49
+
50
+ ## Minification comparison
51
+
52
+ How does HTMLMinifier compare to other solutions — [HTML Minifier from Will Peavy](http://www.willpeavy.com/minifier/) (1st result in [Google search for “html minifier”](https://www.google.com/#q=html+minifier)) as well as [htmlcompressor.com](http://htmlcompressor.com) and [minimize](https://github.com/Swaagie/minimize)?
53
+
54
+ | Site | Original size (KB) | HTMLMinifier | minimize | htmlcompressor.com |
55
+ | --- | --- | --- | --- | --- |
56
+ | [Amazon](https://www.amazon.com/) | 6 | **4** | 4 | n/a |
57
+ | [BBC](https://www.bbc.co.uk/) | 759 | **699** | 752 | n/a |
58
+ | [ECMAScript](https://tc39.es/ecma262/) | 7195 | **6352** | 6571 | n/a |
59
+ | [EFF](https://www.eff.org/) | 60 | **51** | 54 | n/a |
60
+ | [Eloquent JavaScript](https://eloquentjavascript.net/) | 6 | **5** | 6 | n/a |
61
+ | [FAZ](https://www.faz.net/aktuell/) | 1738 | **1614** | 1649 | n/a |
62
+ | [Frontend Dogma](https://frontenddogma.com/) | 119 | **115** | 128 | n/a |
63
+ | [Google](https://www.google.com/) | 50 | **46** | 50 | n/a |
64
+ | [HTMLMinifier](https://github.com/kangax/html-minifier) | 363 | **243** | 341 | n/a |
65
+ | [Mastodon](https://mastodon.social/explore) | 22 | **13** | 21 | n/a |
66
+ | [NBC](https://www.nbc.com/) | 1204 | **1099** | 1191 | n/a |
67
+ | [New York Times](https://www.nytimes.com/) | 900 | **760** | 890 | n/a |
68
+ | [United Nations](https://www.un.org/) | 10 | **7** | 8 | n/a |
69
+ | [W3C](https://www.w3.org/) | 51 | **36** | 42 | n/a |
70
+ | [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | 114 | **100** | 107 | n/a |
71
+
72
+ ## Options Quick Reference
73
+
74
+ Most of the options are disabled by default.
75
+
76
+ | Option | Description | Default |
77
+ | --- | --- | --- |
78
+ | `caseSensitive` | Treat attributes in case sensitive manner (useful for custom HTML tags) | `false` |
79
+ | `collapseBooleanAttributes` | [Omit attribute values from boolean attributes](http://perfectionkills.com/experimenting-with-html-minifier#collapse_boolean_attributes) | `false` |
80
+ | `collapseInlineTagWhitespace` | Don’t leave any spaces between `display:inline;` elements when collapsing. Must be used in conjunction with `collapseWhitespace=true` | `false` |
81
+ | `collapseWhitespace` | [Collapse white space that contributes to text nodes in a document tree](http://perfectionkills.com/experimenting-with-html-minifier#collapse_whitespace) | `false` |
82
+ | `conservativeCollapse` | Always collapse to 1 space (never remove it entirely). Must be used in conjunction with `collapseWhitespace=true` | `false` |
83
+ | `continueOnParseError` | [Handle parse errors](https://html.spec.whatwg.org/multipage/parsing.html#parse-errors) instead of aborting. | `false` |
84
+ | `customAttrAssign` | Arrays of regex’es that allow to support custom attribute assign expressions (e.g. `'<div flex?="{{mode != cover}}"></div>'`) | `[ ]` |
85
+ | `customAttrCollapse` | Regex that specifies custom attribute to strip newlines from (e.g. `/ng-class/`) | |
86
+ | `customAttrSurround` | Arrays of regexes that allow to support custom attribute surround expressions (e.g. `<input {{#if value}}checked="checked"{{/if}}>`) | `[ ]` |
87
+ | `customEventAttributes` | Arrays of regexes that allow to support custom event attributes for `minifyJS` (e.g. `ng-click`) | `[ /^on[a-z]{3,}$/ ]` |
88
+ | `decodeEntities` | Use direct Unicode characters whenever possible | `false` |
89
+ | `html5` | Parse input according to HTML5 specifications | `true` |
90
+ | `ignoreCustomComments` | Array of regexes that allow to ignore certain comments, when matched | `[ /^!/, /^\s*#/ ]` |
91
+ | `ignoreCustomFragments` | Array of regexes that allow to ignore certain fragments, when matched (e.g. `<?php ... ?>`, `{{ ... }}`, etc.) | `[ /<%[\s\S]*?%>/, /<\?[\s\S]*?\?>/ ]` |
92
+ | `includeAutoGeneratedTags` | Insert tags generated by HTML parser | `true` |
93
+ | `keepClosingSlash` | Keep the trailing slash on singleton elements | `false` |
94
+ | `maxLineLength` | Specify a maximum line length. Compressed output will be split by newlines at valid HTML split-points |
95
+ | `minifyCSS` | Minify CSS in style elements and style attributes (uses [clean-css](https://github.com/jakubpawlowicz/clean-css)) | `false` (could be `true`, `Object`, `Function(text, type)`) |
96
+ | `minifyJS` | Minify JavaScript in script elements and event attributes (uses [Terser](https://github.com/terser/terser)) | `false` (could be `true`, `Object`, `Function(text, inline)`) |
97
+ | `minifyURLs` | Minify URLs in various attributes (uses [relateurl](https://github.com/stevenvachon/relateurl)) | `false` (could be `String`, `Object`, `Function(text)`) |
98
+ | `noNewlinesBeforeTagClose` | Never add a newline before a tag that closes an element | `false` |
99
+ | `preserveLineBreaks` | Always collapse to 1 line break (never remove it entirely) when whitespace between tags include a line break. Must be used in conjunction with `collapseWhitespace=true` | `false` |
100
+ | `preventAttributesEscaping` | Prevents the escaping of the values of attributes | `false` |
101
+ | `processConditionalComments` | Process contents of conditional comments through minifier | `false` |
102
+ | `processScripts` | Array of strings corresponding to types of script elements to process through minifier (e.g. `text/ng-template`, `text/x-handlebars-template`, etc.) | `[ ]` |
103
+ | `quoteCharacter` | Type of quote to use for attribute values (“'” or “"”) | |
104
+ | `removeAttributeQuotes` | [Remove quotes around attributes when possible](http://perfectionkills.com/experimenting-with-html-minifier#remove_attribute_quotes) | `false` |
105
+ | `removeComments` | [Strip HTML comments](http://perfectionkills.com/experimenting-with-html-minifier#remove_comments) | `false` |
106
+ | `removeEmptyAttributes` | [Remove all attributes with whitespace-only values](http://perfectionkills.com/experimenting-with-html-minifier#remove_empty_or_blank_attributes) | `false` (could be `true`, `Function(attrName, tag)`) |
107
+ | `removeEmptyElements` | [Remove all elements with empty contents](http://perfectionkills.com/experimenting-with-html-minifier#remove_empty_elements) | `false` |
108
+ | `removeOptionalTags` | [Remove optional tags](http://perfectionkills.com/experimenting-with-html-minifier#remove_optional_tags) | `false` |
109
+ | `removeRedundantAttributes` | [Remove attributes when value matches default.](http://perfectionkills.com/experimenting-with-html-minifier#remove_redundant_attributes) | `false` |
110
+ | `removeScriptTypeAttributes` | Remove `type="text/javascript"` from `script` tags. Other `type` attribute values are left intact | `false` |
111
+ | `removeStyleLinkTypeAttributes`| Remove `type="text/css"` from `style` and `link` tags. Other `type` attribute values are left intact | `false` |
112
+ | `removeTagWhitespace` | Remove space between attributes whenever possible. **Note that this will result in invalid HTML!** | `false` |
113
+ | `sortAttributes` | [Sort attributes by frequency](#sorting-attributes--style-classes) | `false` |
114
+ | `sortClassName` | [Sort style classes by frequency](#sorting-attributes--style-classes) | `false` |
115
+ | `trimCustomFragments` | Trim white space around `ignoreCustomFragments`. | `false` |
116
+ | `useShortDoctype` | [Replaces the `doctype` with the short (HTML5) doctype](http://perfectionkills.com/experimenting-with-html-minifier#use_short_doctype) | `false` |
117
+
118
+ ### Sorting attributes / style classes
119
+
120
+ Minifier options like `sortAttributes` and `sortClassName` won’t impact the plain-text size of the output. However, they form long repetitive chains of characters that should improve compression ratio of gzip used in HTTP compression.
121
+
122
+ ## Special cases
123
+
124
+ ### Ignoring chunks of markup
125
+
126
+ If you have chunks of markup you would like preserved, you can wrap them `<!-- htmlmin:ignore -->`.
127
+
128
+ ### Minifying JSON-LD
129
+
130
+ You can minify script tags with JSON-LD by setting the option `{ processScripts: ['application/ld+json'] }`. Note that this minification is very rudimentary, it is mainly useful for removing newlines and excessive whitespace.
131
+
132
+ ### Preserving SVG tags
133
+
134
+ SVG tags are automatically recognized, and when they are minified, both case-sensitivity and closing-slashes are preserved, regardless of the minification settings used for the rest of the file.
135
+
136
+ ### Working with invalid markup
137
+
138
+ HTMLMinifier **can’t work with invalid or partial chunks of markup**. This is because it parses markup into a tree structure, then modifies it (removing anything that was specified for removal, ignoring anything that was specified to be ignored, etc.), then it creates a markup out of that tree and returns it.
139
+
140
+ Input markup (e.g. `<p id="">foo`)
141
+
142
+
143
+
144
+ Internal representation of markup in a form of tree (e.g. `{ tag: "p", attr: "id", children: ["foo"] }`)
145
+
146
+
147
+
148
+ Transformation of internal representation (e.g. removal of `id` attribute)
149
+
150
+
151
+
152
+ Output of resulting markup (e.g. `<p>foo</p>`)
153
+
154
+ HTMLMinifier can’t know that original markup was only half of the tree; it does its best to try to parse it as a full tree and it loses information about tree being malformed or partial in the beginning. As a result, it can’t create a partial/malformed tree at the time of the output.
155
+
156
+ ## Running benchmarks
157
+
158
+ Benchmarks for minified HTML:
159
+
160
+ ```shell
161
+ cd benchmarks
162
+ npm install
163
+ npm run benchmark
164
+ ```
165
+
166
+ ## Running local server
167
+
168
+ ```shell
169
+ npm run serve
170
+ ```
package/cli.js ADDED
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * html-minifier-next CLI tool
4
+ *
5
+ * The MIT License (MIT)
6
+ *
7
+ * Copyright (c) 2014-2016 Zoltan Frombach
8
+ *
9
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
10
+ * this software and associated documentation files (the "Software"), to deal in
11
+ * the Software without restriction, including without limitation the rights to
12
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
13
+ * the Software, and to permit persons to whom the Software is furnished to do so,
14
+ * subject to the following conditions:
15
+ *
16
+ * The above copyright notice and this permission notice shall be included in all
17
+ * copies or substantial portions of the Software.
18
+ *
19
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
21
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
22
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
23
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
24
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+ *
26
+ */
27
+
28
+ import fs from 'fs';
29
+ import path from 'path';
30
+ import { createRequire } from 'module';
31
+ import { camelCase } from 'camel-case';
32
+ import { paramCase } from 'param-case';
33
+ import { Command } from 'commander';
34
+ import { minify } from './src/htmlminifier.js';
35
+
36
+ const require = createRequire(import.meta.url);
37
+
38
+ const pkg = require('./package.json');
39
+
40
+ const program = new Command();
41
+ program.name(pkg.name);
42
+ program.version(pkg.version);
43
+
44
+ function fatal(message) {
45
+ console.error(message);
46
+ process.exit(1);
47
+ }
48
+
49
+ /**
50
+ * JSON does not support regexes, so, e.g., JSON.parse() will not create
51
+ * a RegExp from the JSON value `[ "/matchString/" ]`, which is
52
+ * technically just an array containing a string that begins and end with
53
+ * a forward slash. To get a RegExp from a JSON string, it must be
54
+ * constructed explicitly in JavaScript.
55
+ *
56
+ * The likelihood of actually wanting to match text that is enclosed in
57
+ * forward slashes is probably quite rare, so if forward slashes were
58
+ * included in an argument that requires a regex, the user most likely
59
+ * thought they were part of the syntax for specifying a regex.
60
+ *
61
+ * In the unlikely case that forward slashes are indeed desired in the
62
+ * search string, the user would need to enclose the expression in a
63
+ * second set of slashes:
64
+ *
65
+ * --customAttrSrround "[\"//matchString//\"]"
66
+ */
67
+ function parseRegExp(value) {
68
+ if (value) {
69
+ return new RegExp(value.replace(/^\/(.*)\/$/, '$1'));
70
+ }
71
+ }
72
+
73
+ function parseJSON(value) {
74
+ if (value) {
75
+ try {
76
+ return JSON.parse(value);
77
+ } catch (e) {
78
+ if (/^{/.test(value)) {
79
+ fatal('Could not parse JSON value \'' + value + '\'');
80
+ }
81
+ return value;
82
+ }
83
+ }
84
+ }
85
+
86
+ function parseJSONArray(value) {
87
+ if (value) {
88
+ value = parseJSON(value);
89
+ return Array.isArray(value) ? value : [value];
90
+ }
91
+ }
92
+
93
+ function parseJSONRegExpArray(value) {
94
+ value = parseJSONArray(value);
95
+ return value && value.map(parseRegExp);
96
+ }
97
+
98
+ function parseString(value) {
99
+ return value;
100
+ }
101
+
102
+ const mainOptions = {
103
+ caseSensitive: 'Treat attributes in case sensitive manner (useful for SVG; e.g. viewBox)',
104
+ collapseBooleanAttributes: 'Omit attribute values from boolean attributes',
105
+ collapseInlineTagWhitespace: 'Collapse white space around inline tag',
106
+ collapseWhitespace: 'Collapse white space that contributes to text nodes in a document tree.',
107
+ conservativeCollapse: 'Always collapse to 1 space (never remove it entirely)',
108
+ continueOnParseError: 'Handle parse errors instead of aborting',
109
+ customAttrAssign: ['Arrays of regex\'es that allow to support custom attribute assign expressions (e.g. \'<div flex?="{{mode != cover}}"></div>\')', parseJSONRegExpArray],
110
+ customAttrCollapse: ['Regex that specifies custom attribute to strip newlines from (e.g. /ng-class/)', parseRegExp],
111
+ customAttrSurround: ['Arrays of regex\'es that allow to support custom attribute surround expressions (e.g. <input {{#if value}}checked="checked"{{/if}}>)', parseJSONRegExpArray],
112
+ customEventAttributes: ['Arrays of regex\'es that allow to support custom event attributes for minifyJS (e.g. ng-click)', parseJSONRegExpArray],
113
+ decodeEntities: 'Use direct Unicode characters whenever possible',
114
+ html5: 'Parse input according to HTML5 specifications',
115
+ ignoreCustomComments: ['Array of regex\'es that allow to ignore certain comments, when matched', parseJSONRegExpArray],
116
+ ignoreCustomFragments: ['Array of regex\'es that allow to ignore certain fragments, when matched (e.g. <?php ... ?>, {{ ... }})', parseJSONRegExpArray],
117
+ includeAutoGeneratedTags: 'Insert tags generated by HTML parser',
118
+ keepClosingSlash: 'Keep the trailing slash on singleton elements',
119
+ maxLineLength: ['Max line length', parseInt],
120
+ minifyCSS: ['Minify CSS in style elements and style attributes (uses clean-css)', parseJSON],
121
+ minifyJS: ['Minify Javascript in script elements and on* attributes', parseJSON],
122
+ minifyURLs: ['Minify URLs in various attributes (uses relateurl)', parseJSON],
123
+ noNewlinesBeforeTagClose: 'Never add a newline before a tag that closes an element',
124
+ preserveLineBreaks: 'Always collapse to 1 line break (never remove it entirely) when whitespace between tags include a line break.',
125
+ preventAttributesEscaping: 'Prevents the escaping of the values of attributes.',
126
+ processConditionalComments: 'Process contents of conditional comments through minifier',
127
+ 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],
128
+ quoteCharacter: ['Type of quote to use for attribute values (\' or ")', parseString],
129
+ removeAttributeQuotes: 'Remove quotes around attributes when possible.',
130
+ removeComments: 'Strip HTML comments',
131
+ removeEmptyAttributes: 'Remove all attributes with whitespace-only values',
132
+ removeEmptyElements: 'Remove all elements with empty contents',
133
+ removeOptionalTags: 'Remove unrequired tags',
134
+ removeRedundantAttributes: 'Remove attributes when value matches default.',
135
+ 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',
136
+ removeStyleLinkTypeAttributes: 'Remove type="text/css" from style and link tags. Other type attribute values are left intact.',
137
+ removeTagWhitespace: 'Remove space between attributes whenever possible',
138
+ sortAttributes: 'Sort attributes by frequency',
139
+ sortClassName: 'Sort style classes by frequency',
140
+ trimCustomFragments: 'Trim white space around ignoreCustomFragments.',
141
+ useShortDoctype: 'Replaces the doctype with the short (HTML5) doctype'
142
+ };
143
+
144
+ // configure commandline flags
145
+ const mainOptionKeys = Object.keys(mainOptions);
146
+ mainOptionKeys.forEach(function (key) {
147
+ const option = mainOptions[key];
148
+ if (Array.isArray(option)) {
149
+ key = key === 'minifyURLs' ? '--minify-urls' : '--' + paramCase(key);
150
+ key += option[1] === parseJSON ? ' [value]' : ' <value>';
151
+ program.option(key, option[0], option[1]);
152
+ } else if (~['html5', 'includeAutoGeneratedTags'].indexOf(key)) {
153
+ program.option('--no-' + paramCase(key), option);
154
+ } else {
155
+ program.option('--' + paramCase(key), option);
156
+ }
157
+ });
158
+ program.option('-o --output <file>', 'Specify output file (if not specified STDOUT will be used for output)');
159
+
160
+ function readFile(file) {
161
+ try {
162
+ return fs.readFileSync(file, { encoding: 'utf8' });
163
+ } catch (e) {
164
+ fatal('Cannot read ' + file + '\n' + e.message);
165
+ }
166
+ }
167
+
168
+ let config = {};
169
+ program.option('-c --config-file <file>', 'Use config file', function (configPath) {
170
+ const data = readFile(configPath);
171
+ try {
172
+ config = JSON.parse(data);
173
+ } catch (je) {
174
+ try {
175
+ config = require(path.resolve(configPath));
176
+ } catch (ne) {
177
+ fatal('Cannot read the specified config file.\nAs JSON: ' + je.message + '\nAs module: ' + ne.message);
178
+ }
179
+ }
180
+ mainOptionKeys.forEach(function (key) {
181
+ if (key in config) {
182
+ const option = mainOptions[key];
183
+ if (Array.isArray(option)) {
184
+ const value = config[key];
185
+ config[key] = option[1](typeof value === 'string' ? value : JSON.stringify(value));
186
+ }
187
+ }
188
+ });
189
+ });
190
+ program.option('--input-dir <dir>', 'Specify an input directory');
191
+ program.option('--output-dir <dir>', 'Specify an output directory');
192
+ program.option('--file-ext <text>', 'Specify an extension to be read, ex: html');
193
+
194
+ let content;
195
+ program.arguments('[files...]').action(function (files) {
196
+ content = files.map(readFile).join('');
197
+ }).parse(process.argv);
198
+
199
+ const programOptions = program.opts();
200
+
201
+ function createOptions() {
202
+ const options = {};
203
+
204
+ mainOptionKeys.forEach(function (key) {
205
+ const param = programOptions[key === 'minifyURLs' ? 'minifyUrls' : camelCase(key)];
206
+
207
+ if (typeof param !== 'undefined') {
208
+ options[key] = param;
209
+ } else if (key in config) {
210
+ options[key] = config[key];
211
+ }
212
+ });
213
+ return options;
214
+ }
215
+
216
+ function mkdir(outputDir, callback) {
217
+ fs.mkdir(outputDir, { recursive: true }, function (err) {
218
+ if (err) {
219
+ fatal('Cannot create directory ' + outputDir + '\n' + err.message);
220
+ }
221
+ callback();
222
+ });
223
+ }
224
+
225
+ function processFile(inputFile, outputFile) {
226
+ fs.readFile(inputFile, { encoding: 'utf8' }, async function (err, data) {
227
+ if (err) {
228
+ fatal('Cannot read ' + inputFile + '\n' + err.message);
229
+ }
230
+ let minified;
231
+ try {
232
+ minified = await minify(data, createOptions());
233
+ } catch (e) {
234
+ fatal('Minification error on ' + inputFile + '\n' + e.message);
235
+ }
236
+ fs.writeFile(outputFile, minified, { encoding: 'utf8' }, function (err) {
237
+ if (err) {
238
+ fatal('Cannot write ' + outputFile + '\n' + err.message);
239
+ }
240
+ });
241
+ });
242
+ }
243
+
244
+ function processDirectory(inputDir, outputDir, fileExt) {
245
+ fs.readdir(inputDir, function (err, files) {
246
+ if (err) {
247
+ fatal('Cannot read directory ' + inputDir + '\n' + err.message);
248
+ }
249
+
250
+ files.forEach(function (file) {
251
+ const inputFile = path.join(inputDir, file);
252
+ const outputFile = path.join(outputDir, file);
253
+
254
+ fs.stat(inputFile, function (err, stat) {
255
+ if (err) {
256
+ fatal('Cannot read ' + inputFile + '\n' + err.message);
257
+ } else if (stat.isDirectory()) {
258
+ processDirectory(inputFile, outputFile, fileExt);
259
+ } else if (!fileExt || path.extname(file) === '.' + fileExt) {
260
+ mkdir(outputDir, function () {
261
+ processFile(inputFile, outputFile);
262
+ });
263
+ }
264
+ });
265
+ });
266
+ });
267
+ }
268
+
269
+ const writeMinify = async () => {
270
+ const minifierOptions = createOptions();
271
+ let minified;
272
+
273
+ try {
274
+ minified = await minify(content, minifierOptions);
275
+ } catch (e) {
276
+ fatal('Minification error:\n' + e.message);
277
+ }
278
+
279
+ let stream = process.stdout;
280
+
281
+ if (programOptions.output) {
282
+ stream = fs.createWriteStream(programOptions.output)
283
+ .on('error', (e) => {
284
+ fatal('Cannot write ' + programOptions.output + '\n' + e.message);
285
+ });
286
+ }
287
+
288
+ stream.write(minified);
289
+ };
290
+
291
+ const { inputDir, outputDir, fileExt } = programOptions;
292
+
293
+ if (inputDir || outputDir) {
294
+ if (!inputDir) {
295
+ fatal('The option output-dir needs to be used with the option input-dir. If you are working with a single file, use -o.');
296
+ } else if (!outputDir) {
297
+ fatal('You need to specify where to write the output files with the option --output-dir');
298
+ }
299
+ processDirectory(inputDir, outputDir, fileExt);
300
+ } else if (content) { // Minifying one or more files specified on the CMD line
301
+ writeMinify();
302
+ } else { // Minifying input coming from STDIN
303
+ content = '';
304
+ process.stdin.setEncoding('utf8');
305
+ process.stdin.on('data', function (data) {
306
+ content += data;
307
+ }).on('end', writeMinify);
308
+ }