htmlnano 1.1.0 → 2.0.1
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/CHANGELOG.md +28 -0
- package/README.md +8 -13
- package/docs/docs/050-modules.md +54 -2
- package/docs/package-lock.json +14644 -824
- package/docs/versioned_docs/{version-1.1.0 → version-1.1.1}/010-introduction.md +0 -0
- package/docs/versioned_docs/{version-1.1.0 → version-1.1.1}/020-usage.md +0 -0
- package/docs/versioned_docs/{version-1.1.0 → version-1.1.1}/030-config.md +0 -0
- package/docs/versioned_docs/{version-1.1.0 → version-1.1.1}/040-presets.md +0 -0
- package/docs/versioned_docs/{version-1.1.0 → version-1.1.1}/050-modules.md +0 -0
- package/docs/versioned_docs/{version-1.1.0 → version-1.1.1}/060-contribute.md +0 -0
- package/docs/versioned_docs/version-2.0.0/010-introduction.md +22 -0
- package/docs/versioned_docs/version-2.0.0/020-usage.md +77 -0
- package/docs/versioned_docs/version-2.0.0/030-config.md +21 -0
- package/docs/versioned_docs/version-2.0.0/040-presets.md +75 -0
- package/docs/versioned_docs/version-2.0.0/050-modules.md +838 -0
- package/docs/versioned_docs/version-2.0.0/060-contribute.md +16 -0
- package/docs/versioned_sidebars/{version-1.1.0-sidebars.json → version-1.1.1-sidebars.json} +1 -1
- package/docs/versioned_sidebars/version-2.0.0-sidebars.json +8 -0
- package/docs/versions.json +2 -1
- package/lib/helpers.js +15 -2
- package/lib/htmlnano.js +78 -4
- package/lib/modules/collapseAttributeWhitespace.js +21 -21
- package/lib/modules/collapseBooleanAttributes.js +13 -19
- package/lib/modules/deduplicateAttributeValues.js +10 -16
- package/lib/modules/minifyCss.js +8 -8
- package/lib/modules/minifyJs.js +6 -9
- package/lib/modules/minifyJson.js +12 -23
- package/lib/modules/minifySvg.js +5 -2
- package/lib/modules/minifyUrls.js +29 -22
- package/lib/modules/normalizeAttributeValues.js +9 -15
- package/lib/modules/removeComments.js +17 -9
- package/lib/modules/removeEmptyAttributes.js +13 -16
- package/lib/modules/removeRedundantAttributes.js +19 -23
- package/lib/modules/removeUnusedCss.js +10 -10
- package/lib/presets/max.js +2 -0
- package/lib/presets/safe.js +2 -2
- package/package.json +49 -13
- package/uncss-fork.patch +0 -13
|
@@ -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).
|
package/docs/versions.json
CHANGED
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,9 +55,19 @@ 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) {
|
|
68
|
+
const nodeHandlers = [];
|
|
69
|
+
const attrsHandlers = [];
|
|
70
|
+
const contentsHandlers = [];
|
|
61
71
|
options = { ...preset,
|
|
62
72
|
...options
|
|
63
73
|
};
|
|
@@ -73,15 +83,79 @@ function htmlnano(optionsRun, presetRun) {
|
|
|
73
83
|
throw new Error('Module "' + moduleName + '" is not defined');
|
|
74
84
|
}
|
|
75
85
|
|
|
76
|
-
|
|
86
|
+
(optionalDependencies[moduleName] || []).forEach(dependency => {
|
|
87
|
+
try {
|
|
88
|
+
require(dependency);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
if (e.code === 'MODULE_NOT_FOUND') {
|
|
91
|
+
console.warn(`You have to install "${dependency}" in order to use htmlnano's "${moduleName}" module`);
|
|
92
|
+
} else {
|
|
93
|
+
throw e;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const module = require('./modules/' + moduleName);
|
|
77
99
|
|
|
78
|
-
|
|
100
|
+
if (module.onAttrs) {
|
|
101
|
+
attrsHandlers.push(module.onAttrs(options, moduleOptions));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (module.onContent) {
|
|
105
|
+
contentsHandlers.push(module.onContent(options, moduleOptions));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (module.onNode) {
|
|
109
|
+
nodeHandlers.push(module.onNode(options, moduleOptions));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (module.default) {
|
|
113
|
+
promise = promise.then(tree => module.default(tree, options, moduleOptions));
|
|
114
|
+
}
|
|
79
115
|
}
|
|
80
116
|
|
|
81
|
-
|
|
117
|
+
if (attrsHandlers.length + contentsHandlers.length + nodeHandlers.length === 0) {
|
|
118
|
+
return promise;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return promise.then(tree => {
|
|
122
|
+
tree.walk(node => {
|
|
123
|
+
if (node.attrs) {
|
|
124
|
+
// Convert all attrs' key to lower case
|
|
125
|
+
let newAttrsObj = {};
|
|
126
|
+
Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
|
|
127
|
+
newAttrsObj[attrName.toLowerCase()] = attrValue;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
for (const handler of attrsHandlers) {
|
|
131
|
+
newAttrsObj = handler(newAttrsObj, node);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
node.attrs = newAttrsObj;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (node.content) {
|
|
138
|
+
for (const handler of contentsHandlers) {
|
|
139
|
+
node.content = handler(node.content, node);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const handler of nodeHandlers) {
|
|
144
|
+
node = handler(node, node);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return node;
|
|
148
|
+
});
|
|
149
|
+
return tree;
|
|
150
|
+
});
|
|
82
151
|
};
|
|
83
152
|
}
|
|
84
153
|
|
|
154
|
+
htmlnano.getRequiredOptionalDependencies = function (optionsRun, presetRun) {
|
|
155
|
+
const [options] = loadConfig(optionsRun, presetRun);
|
|
156
|
+
return [...new Set(Object.keys(options).filter(moduleName => options[moduleName]).map(moduleName => optionalDependencies[moduleName]).flat())];
|
|
157
|
+
};
|
|
158
|
+
|
|
85
159
|
htmlnano.process = function (html, options, preset, postHtmlOptions) {
|
|
86
160
|
return (0, _posthtml.default)([htmlnano(options, preset)]).process(html, postHtmlOptions);
|
|
87
161
|
};
|
|
@@ -3,15 +3,22 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.default = collapseAttributeWhitespace;
|
|
7
6
|
exports.attributesWithLists = void 0;
|
|
7
|
+
exports.onAttrs = onAttrs;
|
|
8
8
|
|
|
9
|
-
var _helpers = require("../helpers
|
|
9
|
+
var _helpers = require("../helpers");
|
|
10
10
|
|
|
11
11
|
const attributesWithLists = new Set(['class', 'dropzone', 'rel', // a, area, link
|
|
12
12
|
'ping', // a, area
|
|
13
13
|
'sandbox', // iframe
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* https://github.com/posthtml/htmlnano/issues/180
|
|
17
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-sizes
|
|
18
|
+
*
|
|
19
|
+
* "sizes" of <img> should not be modified, while "sizes" of <link> will only have one entry in most cases.
|
|
20
|
+
*/
|
|
21
|
+
// 'sizes', // link
|
|
15
22
|
'headers' // td, th
|
|
16
23
|
]);
|
|
17
24
|
/** @type Record<string, string[] | null> */
|
|
@@ -58,29 +65,22 @@ const attributesWithSingleValue = {
|
|
|
58
65
|
};
|
|
59
66
|
/** Collapse whitespaces inside list-like attributes (e.g. class, rel) */
|
|
60
67
|
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
|
|
68
|
-
const attrNameLower = attrName.toLowerCase();
|
|
69
|
-
|
|
70
|
-
if (attributesWithLists.has(attrNameLower)) {
|
|
68
|
+
function onAttrs() {
|
|
69
|
+
return (attrs, node) => {
|
|
70
|
+
const newAttrs = attrs;
|
|
71
|
+
Object.entries(attrs).forEach(([attrName, attrValue]) => {
|
|
72
|
+
if (attributesWithLists.has(attrName)) {
|
|
71
73
|
const newAttrValue = attrValue.replace(/\s+/g, ' ').trim();
|
|
72
|
-
|
|
73
|
-
return
|
|
74
|
+
newAttrs[attrName] = newAttrValue;
|
|
75
|
+
return;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
if ((0, _helpers.isEventHandler)(
|
|
77
|
-
|
|
78
|
-
return node;
|
|
78
|
+
if ((0, _helpers.isEventHandler)(attrName) || Object.hasOwnProperty.call(attributesWithSingleValue, attrName) && (attributesWithSingleValue[attrName] === null || attributesWithSingleValue[attrName].includes(node.tag))) {
|
|
79
|
+
newAttrs[attrName] = minifySingleAttributeValue(attrValue);
|
|
79
80
|
}
|
|
80
81
|
});
|
|
81
|
-
return
|
|
82
|
-
}
|
|
83
|
-
return tree;
|
|
82
|
+
return newAttrs;
|
|
83
|
+
};
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function minifySingleAttributeValue(value) {
|
|
@@ -3,42 +3,36 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.
|
|
6
|
+
exports.onAttrs = onAttrs;
|
|
7
7
|
// Source: https://github.com/kangax/html-minifier/issues/63
|
|
8
8
|
const htmlBooleanAttributes = new Set(['allowfullscreen', 'allowpaymentrequest', 'allowtransparency', 'async', 'autofocus', 'autoplay', 'checked', 'compact', 'controls', 'declare', 'default', 'defaultchecked', 'defaultmuted', 'defaultselected', 'defer', 'disabled', 'enabled', 'formnovalidate', 'hidden', 'indeterminate', 'inert', 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nohref', 'noresize', 'noshade', 'novalidate', 'nowrap', 'open', 'pauseonexit', 'readonly', 'required', 'reversed', 'scoped', 'seamless', 'selected', 'sortable', 'truespeed', 'typemustmatch', 'visible']);
|
|
9
9
|
const amphtmlBooleanAttributes = new Set(['⚡', 'amp', '⚡4ads', 'amp4ads', '⚡4email', 'amp4email', 'amp-custom', 'amp-boilerplate', 'amp4ads-boilerplate', 'amp4email-boilerplate', 'allow-blocked-ranges', 'amp-access-hide', 'amp-access-template', 'amp-keyframes', 'animate', 'arrows', 'data-block-on-consent', 'data-enable-refresh', 'data-multi-size', 'date-template', 'disable-double-tap', 'disable-session-states', 'disableremoteplayback', 'dots', 'expand-single-section', 'expanded', 'fallback', 'first', 'fullscreen', 'inline', 'lightbox', 'noaudio', 'noautoplay', 'noloading', 'once', 'open-after-clear', 'open-after-select', 'open-button', 'placeholder', 'preload', 'reset-on-refresh', 'reset-on-resize', 'resizable', 'rotate-to-fullscreen', 'second', 'standalone', 'stereo', 'submit-error', 'submit-success', 'submitting', 'subscriptions-actions', 'subscriptions-dialog']);
|
|
10
10
|
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
if (!node.
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (!node.tag) {
|
|
18
|
-
return node;
|
|
19
|
-
}
|
|
11
|
+
function onAttrs(options, moduleOptions) {
|
|
12
|
+
return (attrs, node) => {
|
|
13
|
+
if (!node.tag) return attrs;
|
|
14
|
+
const newAttrs = attrs;
|
|
20
15
|
|
|
21
|
-
for (const attrName of Object.keys(
|
|
16
|
+
for (const attrName of Object.keys(attrs)) {
|
|
22
17
|
if (attrName === 'visible' && node.tag.startsWith('a-')) {
|
|
23
18
|
continue;
|
|
24
19
|
}
|
|
25
20
|
|
|
26
21
|
if (htmlBooleanAttributes.has(attrName)) {
|
|
27
|
-
|
|
22
|
+
newAttrs[attrName] = true;
|
|
28
23
|
}
|
|
29
24
|
|
|
30
|
-
if (moduleOptions.amphtml && amphtmlBooleanAttributes.has(attrName) &&
|
|
31
|
-
|
|
25
|
+
if (moduleOptions.amphtml && amphtmlBooleanAttributes.has(attrName) && attrs[attrName] === '') {
|
|
26
|
+
newAttrs[attrName] = true;
|
|
32
27
|
} // collapse crossorigin attributes
|
|
33
28
|
// Specification: https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes
|
|
34
29
|
|
|
35
30
|
|
|
36
|
-
if (attrName.toLowerCase() === 'crossorigin' && (
|
|
37
|
-
|
|
31
|
+
if (attrName.toLowerCase() === 'crossorigin' && (attrs[attrName] === 'anonymous' || attrs[attrName] === '')) {
|
|
32
|
+
newAttrs[attrName] = true;
|
|
38
33
|
}
|
|
39
34
|
}
|
|
40
35
|
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
return tree;
|
|
36
|
+
return newAttrs;
|
|
37
|
+
};
|
|
44
38
|
}
|
|
@@ -3,25 +3,20 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.
|
|
6
|
+
exports.onAttrs = onAttrs;
|
|
7
7
|
|
|
8
8
|
var _collapseAttributeWhitespace = require("./collapseAttributeWhitespace");
|
|
9
9
|
|
|
10
10
|
/** Deduplicate values inside list-like attributes (e.g. class, rel) */
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Object.keys(node.attrs).forEach(attrName => {
|
|
18
|
-
const attrNameLower = attrName.toLowerCase();
|
|
19
|
-
|
|
20
|
-
if (!_collapseAttributeWhitespace.attributesWithLists.has(attrNameLower)) {
|
|
11
|
+
function onAttrs() {
|
|
12
|
+
return attrs => {
|
|
13
|
+
const newAttrs = attrs;
|
|
14
|
+
Object.keys(attrs).forEach(attrName => {
|
|
15
|
+
if (!_collapseAttributeWhitespace.attributesWithLists.has(attrName)) {
|
|
21
16
|
return;
|
|
22
17
|
}
|
|
23
18
|
|
|
24
|
-
const attrValues =
|
|
19
|
+
const attrValues = attrs[attrName].split(/\s/);
|
|
25
20
|
const uniqeAttrValues = new Set();
|
|
26
21
|
const deduplicatedAttrValues = [];
|
|
27
22
|
attrValues.forEach(attrValue => {
|
|
@@ -38,9 +33,8 @@ function collapseAttributeWhitespace(tree) {
|
|
|
38
33
|
deduplicatedAttrValues.push(attrValue);
|
|
39
34
|
uniqeAttrValues.add(attrValue);
|
|
40
35
|
});
|
|
41
|
-
|
|
36
|
+
newAttrs[attrName] = deduplicatedAttrValues.join(' ');
|
|
42
37
|
});
|
|
43
|
-
return
|
|
44
|
-
}
|
|
45
|
-
return tree;
|
|
38
|
+
return newAttrs;
|
|
39
|
+
};
|
|
46
40
|
}
|
package/lib/modules/minifyCss.js
CHANGED
|
@@ -7,12 +7,8 @@ exports.default = minifyCss;
|
|
|
7
7
|
|
|
8
8
|
var _helpers = require("../helpers");
|
|
9
9
|
|
|
10
|
-
|
|
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 (
|
|
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 (
|
|
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);
|
package/lib/modules/minifyJs.js
CHANGED
|
@@ -5,16 +5,15 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = minifyJs;
|
|
7
7
|
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
var _helpers = require("../helpers.es6");
|
|
8
|
+
var _helpers = require("../helpers");
|
|
11
9
|
|
|
12
10
|
var _removeRedundantAttributes = require("./removeRedundantAttributes");
|
|
13
11
|
|
|
14
|
-
|
|
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
|
|
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
|
|
|
@@ -3,31 +3,20 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.
|
|
6
|
+
exports.onContent = onContent;
|
|
7
|
+
const rNodeAttrsTypeJson = /(\/|\+)json/;
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
tree.match({
|
|
12
|
-
tag: 'script',
|
|
13
|
-
attrs: {
|
|
14
|
-
type: /(\/|\+)json/
|
|
15
|
-
}
|
|
16
|
-
}, node => {
|
|
17
|
-
let content = (node.content || []).join('');
|
|
18
|
-
|
|
19
|
-
if (!content) {
|
|
20
|
-
return node;
|
|
21
|
-
}
|
|
9
|
+
function onContent() {
|
|
10
|
+
return (content, node) => {
|
|
11
|
+
let newContent = content;
|
|
22
12
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
13
|
+
if (node.attrs && node.attrs.type && rNodeAttrsTypeJson.test(node.attrs.type)) {
|
|
14
|
+
try {
|
|
15
|
+
newContent = JSON.stringify(JSON.parse((content || []).join('')));
|
|
16
|
+
} catch (error) {// Invalid JSON
|
|
17
|
+
}
|
|
27
18
|
}
|
|
28
19
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
});
|
|
32
|
-
return tree;
|
|
20
|
+
return newContent;
|
|
21
|
+
};
|
|
33
22
|
}
|
package/lib/modules/minifySvg.js
CHANGED
|
@@ -5,10 +5,13 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = minifySvg;
|
|
7
7
|
|
|
8
|
-
var
|
|
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 =
|
|
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
|
|
8
|
+
var _helpers = require("../helpers");
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
148
|
+
return terser.minify(result, {}) // Default Option is good enough
|
|
142
149
|
.then(({
|
|
143
150
|
code
|
|
144
151
|
}) => {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.
|
|
6
|
+
exports.onAttrs = onAttrs;
|
|
7
7
|
const caseInsensitiveAttributes = {
|
|
8
8
|
autocomplete: ['form'],
|
|
9
9
|
charset: ['meta', 'script'],
|
|
@@ -29,20 +29,14 @@ const caseInsensitiveAttributes = {
|
|
|
29
29
|
wrap: ['textarea']
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
|
|
39
|
-
const attrNameLower = attrName.toLowerCase();
|
|
40
|
-
|
|
41
|
-
if (Object.hasOwnProperty.call(caseInsensitiveAttributes, attrNameLower) && (caseInsensitiveAttributes[attrNameLower] === null || caseInsensitiveAttributes[attrNameLower].includes(node.tag))) {
|
|
42
|
-
node.attrs[attrName] = attrValue.toLowerCase();
|
|
32
|
+
function onAttrs() {
|
|
33
|
+
return (attrs, node) => {
|
|
34
|
+
const newAttrs = attrs;
|
|
35
|
+
Object.entries(attrs).forEach(([attrName, attrValue]) => {
|
|
36
|
+
if (Object.hasOwnProperty.call(caseInsensitiveAttributes, attrName) && (caseInsensitiveAttributes[attrName] === null || caseInsensitiveAttributes[attrName].includes(node.tag))) {
|
|
37
|
+
newAttrs[attrName] = attrValue.toLowerCase ? attrValue.toLowerCase() : attrValue;
|
|
43
38
|
}
|
|
44
39
|
});
|
|
45
|
-
return
|
|
46
|
-
}
|
|
47
|
-
return tree;
|
|
40
|
+
return newAttrs;
|
|
41
|
+
};
|
|
48
42
|
}
|