htmlnano 1.1.1 → 2.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.
@@ -0,0 +1,16 @@
1
+ # Contribute
2
+
3
+ Since the minifier is modular, it's very easy to add new modules:
4
+
5
+ 1. Create a ES6-file inside `lib/modules/` with a function that does some minification. For example you can check [`lib/modules/example.es6`](https://github.com/posthtml/htmlnano/blob/master/lib/modules/example.es6).
6
+
7
+ 2. Add the module's name into one of those [presets](https://github.com/posthtml/htmlnano/tree/master/lib/presets). You can choose either `ampSafe`, `max`, or `safe`.
8
+
9
+ 3. Create a JS-file inside `test/modules/` with some unit-tests.
10
+
11
+ 4. Describe your module in the section "[Modules](https://github.com/posthtml/htmlnano/blob/master/README.md#modules)".
12
+
13
+ 5. Send me a pull request.
14
+
15
+ Other types of contribution (bug fixes, documentation improves, etc) are also welcome!
16
+ Would like to contribute, but don't have any ideas what to do? Check out [our issues](https://github.com/posthtml/htmlnano/labels/help%20wanted).
@@ -0,0 +1,8 @@
1
+ {
2
+ "version-2.0.0/tutorialSidebar": [
3
+ {
4
+ "type": "autogenerated",
5
+ "dirName": "."
6
+ }
7
+ ]
8
+ }
@@ -1,3 +1,4 @@
1
1
  [
2
+ "2.0.0",
2
3
  "1.1.1"
3
4
  ]
package/lib/helpers.js CHANGED
@@ -3,12 +3,13 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.extractCssFromStyleNode = extractCssFromStyleNode;
6
7
  exports.isAmpBoilerplate = isAmpBoilerplate;
7
8
  exports.isComment = isComment;
8
9
  exports.isConditionalComment = isConditionalComment;
9
- exports.isStyleNode = isStyleNode;
10
- exports.extractCssFromStyleNode = extractCssFromStyleNode;
11
10
  exports.isEventHandler = isEventHandler;
11
+ exports.isStyleNode = isStyleNode;
12
+ exports.optionalRequire = optionalRequire;
12
13
  const ampBoilerplateAttributes = ['amp-boilerplate', 'amp4ads-boilerplate', 'amp4email-boilerplate'];
13
14
 
14
15
  function isAmpBoilerplate(node) {
@@ -43,4 +44,16 @@ function extractCssFromStyleNode(node) {
43
44
 
44
45
  function isEventHandler(attributeName) {
45
46
  return attributeName && attributeName.slice && attributeName.slice(0, 2).toLowerCase() === 'on' && attributeName.length >= 5;
47
+ }
48
+
49
+ function optionalRequire(moduleName) {
50
+ try {
51
+ return require(moduleName);
52
+ } catch (e) {
53
+ if (e.code === 'MODULE_NOT_FOUND') {
54
+ return null;
55
+ }
56
+
57
+ throw e;
58
+ }
46
59
  }
package/lib/htmlnano.js CHANGED
@@ -3,8 +3,8 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.loadConfig = loadConfig;
7
6
  exports.default = void 0;
7
+ exports.loadConfig = loadConfig;
8
8
 
9
9
  var _posthtml = _interopRequireDefault(require("posthtml"));
10
10
 
@@ -55,6 +55,13 @@ function loadConfig(options, preset, configPath) {
55
55
  return [options || {}, preset || _safe.default];
56
56
  }
57
57
 
58
+ const optionalDependencies = {
59
+ minifyCss: ['cssnano', 'postcss'],
60
+ minifyJs: ['terser'],
61
+ minifyUrl: ['relateurl', 'srcset', 'terser'],
62
+ minifySvg: ['svgo']
63
+ };
64
+
58
65
  function htmlnano(optionsRun, presetRun) {
59
66
  let [options, preset] = loadConfig(optionsRun, presetRun);
60
67
  return function minifier(tree) {
@@ -73,6 +80,18 @@ function htmlnano(optionsRun, presetRun) {
73
80
  throw new Error('Module "' + moduleName + '" is not defined');
74
81
  }
75
82
 
83
+ (optionalDependencies[moduleName] || []).forEach(dependency => {
84
+ try {
85
+ require(dependency);
86
+ } catch (e) {
87
+ if (e.code === 'MODULE_NOT_FOUND') {
88
+ console.warn(`You have to install "${dependency}" in order to use htmlnano's "${moduleName}" module`);
89
+ } else {
90
+ throw e;
91
+ }
92
+ }
93
+ });
94
+
76
95
  let module = require('./modules/' + moduleName);
77
96
 
78
97
  promise = promise.then(tree => module.default(tree, options, moduleOptions));
@@ -82,6 +101,11 @@ function htmlnano(optionsRun, presetRun) {
82
101
  };
83
102
  }
84
103
 
104
+ htmlnano.getRequiredOptionalDependencies = function (optionsRun, presetRun) {
105
+ const [options] = loadConfig(optionsRun, presetRun);
106
+ return [...new Set(Object.keys(options).filter(moduleName => options[moduleName]).map(moduleName => optionalDependencies[moduleName]).flat())];
107
+ };
108
+
85
109
  htmlnano.process = function (html, options, preset, postHtmlOptions) {
86
110
  return (0, _posthtml.default)([htmlnano(options, preset)]).process(html, postHtmlOptions);
87
111
  };
@@ -3,8 +3,8 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = collapseAttributeWhitespace;
7
6
  exports.attributesWithLists = void 0;
7
+ exports.default = collapseAttributeWhitespace;
8
8
 
9
9
  var _helpers = require("../helpers");
10
10
 
@@ -7,12 +7,8 @@ exports.default = minifyCss;
7
7
 
8
8
  var _helpers = require("../helpers");
9
9
 
10
- var _postcss = _interopRequireDefault(require("postcss"));
11
-
12
- var _cssnano = _interopRequireDefault(require("cssnano"));
13
-
14
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
-
10
+ const cssnano = (0, _helpers.optionalRequire)('cssnano');
11
+ const postcss = (0, _helpers.optionalRequire)('postcss');
16
12
  const postcssOptions = {
17
13
  // Prevent the following warning from being shown:
18
14
  // > Without `from` option PostCSS could generate wrong source map and will not find Browserslist config.
@@ -22,6 +18,10 @@ const postcssOptions = {
22
18
  /** Minify CSS with cssnano */
23
19
 
24
20
  function minifyCss(tree, options, cssnanoOptions) {
21
+ if (!cssnano || !postcss) {
22
+ return tree;
23
+ }
24
+
25
25
  let promises = [];
26
26
  tree.walk(node => {
27
27
  if ((0, _helpers.isStyleNode)(node)) {
@@ -46,7 +46,7 @@ function processStyleNode(styleNode, cssnanoOptions) {
46
46
  css = strippedCss;
47
47
  }
48
48
 
49
- return (0, _postcss.default)([(0, _cssnano.default)(cssnanoOptions)]).process(css, postcssOptions).then(result => {
49
+ return postcss([cssnano(cssnanoOptions)]).process(css, postcssOptions).then(result => {
50
50
  if (isCdataWrapped) {
51
51
  return styleNode.content = ['<![CDATA[' + result + ']]>'];
52
52
  }
@@ -61,7 +61,7 @@ function processStyleAttr(node, cssnanoOptions) {
61
61
  const wrapperStart = 'a{';
62
62
  const wrapperEnd = '}';
63
63
  const wrappedStyle = wrapperStart + (node.attrs.style || '') + wrapperEnd;
64
- return (0, _postcss.default)([(0, _cssnano.default)(cssnanoOptions)]).process(wrappedStyle, postcssOptions).then(result => {
64
+ return postcss([cssnano(cssnanoOptions)]).process(wrappedStyle, postcssOptions).then(result => {
65
65
  const minifiedCss = result.css; // Remove wrapperStart at the start and wrapperEnd at the end of minifiedCss
66
66
 
67
67
  node.attrs.style = minifiedCss.substring(wrapperStart.length, minifiedCss.length - wrapperEnd.length);
@@ -5,16 +5,15 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = minifyJs;
7
7
 
8
- var _terser = _interopRequireDefault(require("terser"));
9
-
10
8
  var _helpers = require("../helpers");
11
9
 
12
10
  var _removeRedundantAttributes = require("./removeRedundantAttributes");
13
11
 
14
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
-
12
+ const terser = (0, _helpers.optionalRequire)('terser');
16
13
  /** Minify JS with Terser */
14
+
17
15
  function minifyJs(tree, options, terserOptions) {
16
+ if (!terser) return tree;
18
17
  let promises = [];
19
18
  tree.walk(node => {
20
19
  if (node.tag && node.tag === 'script') {
@@ -62,7 +61,7 @@ function processScriptNode(scriptNode, terserOptions) {
62
61
  js = strippedJs;
63
62
  }
64
63
 
65
- return _terser.default.minify(js, terserOptions).then(result => {
64
+ return terser.minify(js, terserOptions).then(result => {
66
65
  if (result.error) {
67
66
  throw new Error(result.error);
68
67
  }
@@ -96,14 +95,12 @@ function processNodeWithOnAttrs(node, terserOptions) {
96
95
 
97
96
 
98
97
  let wrappedJs = jsWrapperStart + node.attrs[attrName] + jsWrapperEnd;
99
-
100
- let promise = _terser.default.minify(wrappedJs, terserOptions).then(({
98
+ let promise = terser.minify(wrappedJs, terserOptions).then(({
101
99
  code
102
100
  }) => {
103
101
  let minifiedJs = code.substring(jsWrapperStart.length, code.length - jsWrapperEnd.length);
104
102
  node.attrs[attrName] = minifiedJs;
105
103
  });
106
-
107
104
  promises.push(promise);
108
105
  }
109
106
 
@@ -5,10 +5,13 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = minifySvg;
7
7
 
8
- var _svgo = require("svgo");
8
+ var _helpers = require("../helpers");
9
9
 
10
+ const svgo = (0, _helpers.optionalRequire)('svgo');
10
11
  /** Minify SVG with SVGO */
12
+
11
13
  function minifySvg(tree, options, svgoOptions = {}) {
14
+ if (!svgo) return tree;
12
15
  tree.match({
13
16
  tag: 'svg'
14
17
  }, node => {
@@ -16,7 +19,7 @@ function minifySvg(tree, options, svgoOptions = {}) {
16
19
  closingSingleTag: 'slash',
17
20
  quoteAllAttributes: true
18
21
  });
19
- const result = (0, _svgo.optimize)(svgStr, svgoOptions);
22
+ const result = svgo.optimize(svgStr, svgoOptions);
20
23
  node.tag = false;
21
24
  node.attrs = {};
22
25
  node.content = result.data;
@@ -5,15 +5,12 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = minifyUrls;
7
7
 
8
- var _relateurl = _interopRequireDefault(require("relateurl"));
8
+ var _helpers = require("../helpers");
9
9
 
10
- var _srcset = _interopRequireDefault(require("srcset"));
10
+ const RelateUrl = (0, _helpers.optionalRequire)('relateurl');
11
+ const srcset = (0, _helpers.optionalRequire)('srcset');
12
+ const terser = (0, _helpers.optionalRequire)('terser'); // Adopts from https://github.com/kangax/html-minifier/blob/51ce10f4daedb1de483ffbcccecc41be1c873da2/src/htmlminifier.js#L209-L221
11
13
 
12
- var _terser = _interopRequireDefault(require("terser"));
13
-
14
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
-
16
- // Adopts from https://github.com/kangax/html-minifier/blob/51ce10f4daedb1de483ffbcccecc41be1c873da2/src/htmlminifier.js#L209-L221
17
14
  const tagsHaveUriValuesForAttributes = new Set(['a', 'area', 'link', 'base', 'object', 'blockquote', 'q', 'del', 'ins', 'form', 'input', 'head', 'audio', 'embed', 'iframe', 'img', 'script', 'track', 'video']);
18
15
  const tagsHasHrefAttributes = new Set(['a', 'area', 'link', 'base']);
19
16
  const attributesOfImgTagHasUriValues = new Set(['src', 'longdesc', 'usemap']);
@@ -77,7 +74,10 @@ function minifyUrls(tree, options, moduleOptions) {
77
74
  */
78
75
 
79
76
  if (!relateUrlInstance || STORED_URL_BASE !== urlBase) {
80
- relateUrlInstance = new _relateurl.default(urlBase);
77
+ if (RelateUrl) {
78
+ relateUrlInstance = new RelateUrl(urlBase);
79
+ }
80
+
81
81
  STORED_URL_BASE = urlBase;
82
82
  }
83
83
 
@@ -96,25 +96,31 @@ function minifyUrls(tree, options, moduleOptions) {
96
96
  if (isJavaScriptUrl(attrValue)) {
97
97
  promises.push(minifyJavaScriptUrl(node, attrName));
98
98
  } else {
99
- // FIXME!
100
- // relateurl@1.0.0-alpha only supports URL while stable version (0.2.7) only supports string
101
- // the WHATWG URL API is very strict while attrValue might not be a valid URL
102
- // new URL should be used, and relateUrl#relate should be wrapped in try...catch after relateurl@1 is stable
103
- node.attrs[attrName] = relateUrlInstance.relate(attrValue);
99
+ if (relateUrlInstance) {
100
+ // FIXME!
101
+ // relateurl@1.0.0-alpha only supports URL while stable version (0.2.7) only supports string
102
+ // the WHATWG URL API is very strict while attrValue might not be a valid URL
103
+ // new URL should be used, and relateUrl#relate should be wrapped in try...catch after relateurl@1 is stable
104
+ node.attrs[attrName] = relateUrlInstance.relate(attrValue);
105
+ }
104
106
  }
105
107
 
106
108
  continue;
107
109
  }
108
110
 
109
111
  if (isSrcsetAttribute(node.tag, attrNameLower)) {
110
- try {
111
- const parsedSrcset = _srcset.default.parse(attrValue);
112
-
113
- node.attrs[attrName] = _srcset.default.stringify(parsedSrcset.map(srcset => {
114
- srcset.url = relateUrlInstance.relate(srcset.url);
115
- return srcset;
116
- }));
117
- } catch (e) {// srcset will throw an Error for invalid srcset.
112
+ if (srcset) {
113
+ try {
114
+ const parsedSrcset = srcset.parse(attrValue);
115
+ node.attrs[attrName] = srcset.stringify(parsedSrcset.map(srcset => {
116
+ if (relateUrlInstance) {
117
+ srcset.url = relateUrlInstance.relate(srcset.url);
118
+ }
119
+
120
+ return srcset;
121
+ }));
122
+ } catch (e) {// srcset will throw an Error for invalid srcset.
123
+ }
118
124
  }
119
125
 
120
126
  continue;
@@ -132,13 +138,14 @@ function isJavaScriptUrl(url) {
132
138
  }
133
139
 
134
140
  function minifyJavaScriptUrl(node, attrName) {
141
+ if (!terser) return Promise.resolve();
135
142
  const jsWrapperStart = 'function a(){';
136
143
  const jsWrapperEnd = '}a();';
137
144
  let result = node.attrs[attrName];
138
145
 
139
146
  if (result) {
140
147
  result = result.slice(JAVASCRIPT_URL_PROTOCOL.length);
141
- return _terser.default.minify(result, {}) // Default Option is good enough
148
+ return terser.minify(result, {}) // Default Option is good enough
142
149
  .then(({
143
150
  code
144
151
  }) => {
@@ -63,7 +63,8 @@ function removeEmptyAttributes(tree) {
63
63
  Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
64
64
  const attrNameLower = attrName.toLowerCase();
65
65
 
66
- if (attrNameLower.slice(0, 2).toLowerCase() === 'on' && attrName.length >= 5 || Object.hasOwnProperty.call(safeToRemoveAttrs, attrNameLower) && (safeToRemoveAttrs[attrNameLower] === null || safeToRemoveAttrs[attrNameLower].includes(node.tag))) {
66
+ if (attrNameLower.slice(0, 2).toLowerCase() === 'on' && attrName.length >= 5 // Event Handler
67
+ || Object.hasOwnProperty.call(safeToRemoveAttrs, attrNameLower) && (safeToRemoveAttrs[attrNameLower] === null || safeToRemoveAttrs[attrNameLower].includes(node.tag))) {
67
68
  if (attrValue === '' || (attrValue || '').match(/^\s+$/)) {
68
69
  delete node.attrs[attrName];
69
70
  }
@@ -7,13 +7,9 @@ exports.default = removeUnusedCss;
7
7
 
8
8
  var _helpers = require("../helpers");
9
9
 
10
- var _uncss = _interopRequireDefault(require("uncss"));
10
+ const uncss = (0, _helpers.optionalRequire)('uncss');
11
+ const purgecss = (0, _helpers.optionalRequire)('purgecss'); // These options must be set and shouldn't be overriden to ensure uncss doesn't look at linked stylesheets.
11
12
 
12
- var _purgecss = _interopRequireDefault(require("purgecss"));
13
-
14
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
-
16
- // These options must be set and shouldn't be overriden to ensure uncss doesn't look at linked stylesheets.
17
13
  const uncssOptions = {
18
14
  ignoreSheets: [/\s*/],
19
15
  stylesheets: []
@@ -43,7 +39,7 @@ function runUncss(html, css, userOptions) {
43
39
  };
44
40
  return new Promise((resolve, reject) => {
45
41
  options.raw = css;
46
- (0, _uncss.default)(html, options, (error, output) => {
42
+ uncss(html, options, (error, output) => {
47
43
  if (error) {
48
44
  reject(error);
49
45
  return;
@@ -100,7 +96,7 @@ function runPurgecss(tree, css, userOptions) {
100
96
  extensions: ['html']
101
97
  }]
102
98
  };
103
- return new _purgecss.default().purge(options).then(result => {
99
+ return new purgecss.PurgeCSS().purge(options).then(result => {
104
100
  return result[0].css;
105
101
  });
106
102
  }
@@ -113,9 +109,13 @@ function removeUnusedCss(tree, options, userOptions) {
113
109
  tree.walk(node => {
114
110
  if ((0, _helpers.isStyleNode)(node)) {
115
111
  if (userOptions.tool === 'purgeCSS') {
116
- promises.push(processStyleNodePurgeCSS(tree, node, userOptions));
112
+ if (purgecss) {
113
+ promises.push(processStyleNodePurgeCSS(tree, node, userOptions));
114
+ }
117
115
  } else {
118
- promises.push(processStyleNodeUnCSS(html, node, userOptions));
116
+ if (uncss) {
117
+ promises.push(processStyleNodeUnCSS(html, node, userOptions));
118
+ }
119
119
  }
120
120
  }
121
121
 
@@ -22,6 +22,8 @@ var _default = { ..._safe.default,
22
22
  removeComments: 'all',
23
23
  removeAttributeQuotes: true,
24
24
  removeRedundantAttributes: true,
25
+ mergeScripts: true,
26
+ mergeStyles: true,
25
27
  removeUnusedCss: {},
26
28
  minifyCss: {
27
29
  preset: 'default'
@@ -17,8 +17,8 @@ var _default = {
17
17
  collapseWhitespace: 'conservative',
18
18
  custom: [],
19
19
  deduplicateAttributeValues: true,
20
- mergeScripts: true,
21
- mergeStyles: true,
20
+ mergeScripts: false,
21
+ mergeStyles: false,
22
22
  removeUnusedCss: false,
23
23
  minifyCss: {
24
24
  preset: 'default'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "htmlnano",
3
- "version": "1.1.1",
3
+ "version": "2.0.0",
4
4
  "description": "Modular HTML minifier, built on top of the PostHTML",
5
5
  "main": "index.js",
6
6
  "author": "Kirill Maltsev <maltsevkirill@gmail.com>",
@@ -40,16 +40,8 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "cosmiconfig": "^7.0.1",
43
- "cssnano": "^5.0.8",
44
- "postcss": "^8.3.6",
45
43
  "posthtml": "^0.16.5",
46
- "purgecss": "^4.0.0",
47
- "relateurl": "^0.2.7",
48
- "srcset": "^4.0.0",
49
- "svgo": "^2.6.1",
50
- "terser": "^5.8.0",
51
- "timsort": "^0.3.0",
52
- "uncss": "^0.17.3"
44
+ "timsort": "^0.3.0"
53
45
  },
54
46
  "devDependencies": {
55
47
  "@babel/cli": "^7.15.7",
@@ -57,11 +49,55 @@
57
49
  "@babel/preset-env": "^7.15.6",
58
50
  "@babel/register": "^7.15.3",
59
51
  "babel-eslint": "^10.1.0",
52
+ "cssnano": "^5.0.11",
60
53
  "eslint": "^7.32.0",
61
54
  "expect": "^27.2.0",
62
55
  "mocha": "^9.1.0",
56
+ "postcss": "^8.3.11",
57
+ "purgecss": "^4.0.3",
58
+ "relateurl": "^0.2.7",
63
59
  "release-it": "^14.11.5",
64
- "rimraf": "^3.0.2"
60
+ "rimraf": "^3.0.2",
61
+ "srcset": "^5.0.0",
62
+ "svgo": "^2.8.0",
63
+ "terser": "^5.10.0",
64
+ "uncss": "^0.17.3"
65
+ },
66
+ "peerDependencies": {
67
+ "cssnano": "^5.0.11",
68
+ "postcss": "^8.3.11",
69
+ "purgecss": "^4.0.3",
70
+ "relateurl": "^0.2.7",
71
+ "srcset": "^5.0.0",
72
+ "svgo": "^2.8.0",
73
+ "terser": "^5.10.0",
74
+ "uncss": "^0.17.3"
75
+ },
76
+ "peerDependenciesMeta": {
77
+ "cssnano": {
78
+ "optional": true
79
+ },
80
+ "postcss": {
81
+ "optional": true
82
+ },
83
+ "purgecss": {
84
+ "optional": true
85
+ },
86
+ "relateurl": {
87
+ "optional": true
88
+ },
89
+ "srcset": {
90
+ "optional": true
91
+ },
92
+ "svgo": {
93
+ "optional": true
94
+ },
95
+ "terser": {
96
+ "optional": true
97
+ },
98
+ "uncss": {
99
+ "optional": true
100
+ }
65
101
  },
66
102
  "repository": {
67
103
  "type": "git",
package/uncss-fork.patch DELETED
@@ -1,13 +0,0 @@
1
- diff --git a/package.json b/package.json
2
- index a127c0f..66455bb 100644
3
- --- a/package.json
4
- +++ b/package.json
5
- @@ -49,7 +49,7 @@
6
- "svgo": "^2.4.0",
7
- "terser": "^5.7.0",
8
- "timsort": "^0.3.0",
9
- - "uncss": "^0.17.3"
10
- + "@novaatwarren/uncss": "^0.17.4"
11
- },
12
- "devDependencies": {
13
- "@babel/cli": "^7.14.3",