htmlnano 2.1.0 → 2.1.2
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 +20 -1
- package/docs/docs/050-modules.md +2 -2
- package/docs/package-lock.json +886 -868
- package/index.d.ts +2 -1
- package/lib/helpers.cjs +3 -2
- package/lib/helpers.mjs +2 -1
- package/lib/htmlnano.cjs +5 -3
- package/lib/htmlnano.mjs +3 -1
- package/lib/modules/minifyConditionalComments.cjs +1 -1
- package/lib/modules/minifyUrls.cjs +4 -4
- package/lib/modules/minifyUrls.mjs +4 -4
- package/lib/modules/sortAttributes.cjs +1 -3
- package/lib/modules/sortAttributes.mjs +1 -3
- package/lib/modules/sortAttributesWithLists.cjs +1 -3
- package/lib/modules/sortAttributesWithLists.mjs +1 -2
- package/lib/presets/ampSafe.cjs +1 -1
- package/lib/presets/max.cjs +1 -1
- package/package.json +15 -13
- package/test.js +8 -33
package/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { Config as SvgoOptimizeOptions } from "svgo";
|
|
|
5
5
|
|
|
6
6
|
export interface HtmlnanoOptions {
|
|
7
7
|
skipConfigLoading?: boolean;
|
|
8
|
+
skipInternalWarnings?: boolean;
|
|
8
9
|
collapseAttributeWhitespace?: boolean;
|
|
9
10
|
collapseBooleanAttributes?: {
|
|
10
11
|
amphtml?: boolean;
|
|
@@ -22,7 +23,7 @@ export interface HtmlnanoOptions {
|
|
|
22
23
|
minifySvg?: SvgoOptimizeOptions | boolean;
|
|
23
24
|
normalizeAttributeValues?: boolean;
|
|
24
25
|
removeAttributeQuotes?: boolean;
|
|
25
|
-
removeComments?: boolean | "safe" | "all" | RegExp | (() => boolean);
|
|
26
|
+
removeComments?: boolean | "safe" | "all" | RegExp | ((comment: string) => boolean);
|
|
26
27
|
removeEmptyAttributes?: boolean;
|
|
27
28
|
removeRedundantAttributes?: boolean;
|
|
28
29
|
removeOptionalTags?: boolean;
|
package/lib/helpers.cjs
CHANGED
|
@@ -11,7 +11,7 @@ exports.isEventHandler = isEventHandler;
|
|
|
11
11
|
exports.isStyleNode = isStyleNode;
|
|
12
12
|
exports.optionalImport = optionalImport;
|
|
13
13
|
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
|
14
|
-
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u &&
|
|
14
|
+
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
|
15
15
|
function __transformExtension(filepath, extMapping) {
|
|
16
16
|
if (!filepath.startsWith('./') && !filepath.startsWith('../')) {
|
|
17
17
|
// Package import
|
|
@@ -52,7 +52,8 @@ function isComment(content) {
|
|
|
52
52
|
return false;
|
|
53
53
|
}
|
|
54
54
|
function isConditionalComment(content) {
|
|
55
|
-
|
|
55
|
+
const clean = (content || '').trim();
|
|
56
|
+
return clean.startsWith('<!--[if') || clean === '<!--<![endif]-->';
|
|
56
57
|
}
|
|
57
58
|
function isStyleNode(node) {
|
|
58
59
|
return node.tag === 'style' && !isAmpBoilerplate(node) && 'content' in node && node.content.length > 0;
|
package/lib/helpers.mjs
CHANGED
|
@@ -24,7 +24,8 @@ export function isComment(content) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function isConditionalComment(content) {
|
|
27
|
-
|
|
27
|
+
const clean = (content || '').trim();
|
|
28
|
+
return clean.startsWith('<!--[if') || clean === '<!--<![endif]-->';
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export function isStyleNode(node) {
|
package/lib/htmlnano.cjs
CHANGED
|
@@ -10,7 +10,7 @@ var _cosmiconfig = require("cosmiconfig");
|
|
|
10
10
|
var _safe = _interopRequireDefault(require("./presets/safe.cjs"));
|
|
11
11
|
var _ampSafe = _interopRequireDefault(require("./presets/ampSafe.cjs"));
|
|
12
12
|
var _max = _interopRequireDefault(require("./presets/max.cjs"));
|
|
13
|
-
function _interopRequireDefault(
|
|
13
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
14
14
|
function __transformExtension(filepath, extMapping) {
|
|
15
15
|
if (!filepath.startsWith('./') && !filepath.startsWith('../')) {
|
|
16
16
|
// Package import
|
|
@@ -33,7 +33,7 @@ function __transformExtension(filepath, extMapping) {
|
|
|
33
33
|
return filepath;
|
|
34
34
|
}
|
|
35
35
|
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
|
36
|
-
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u &&
|
|
36
|
+
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
|
37
37
|
const presets = {
|
|
38
38
|
safe: _safe.default,
|
|
39
39
|
ampSafe: _ampSafe.default,
|
|
@@ -121,7 +121,9 @@ function htmlnano(optionsRun, presetRun) {
|
|
|
121
121
|
}));
|
|
122
122
|
} catch (e) {
|
|
123
123
|
if (e.code === 'MODULE_NOT_FOUND' || e.code === 'ERR_MODULE_NOT_FOUND') {
|
|
124
|
-
|
|
124
|
+
if (!options.skipInternalWarnings) {
|
|
125
|
+
console.warn(`You have to install "${dependency}" in order to use htmlnano's "${moduleName}" module`);
|
|
126
|
+
}
|
|
125
127
|
} else {
|
|
126
128
|
throw e;
|
|
127
129
|
}
|
package/lib/htmlnano.mjs
CHANGED
|
@@ -98,7 +98,9 @@ function htmlnano(optionsRun, presetRun) {
|
|
|
98
98
|
await import(dependency);
|
|
99
99
|
} catch (e) {
|
|
100
100
|
if (e.code === 'MODULE_NOT_FOUND' || e.code === 'ERR_MODULE_NOT_FOUND') {
|
|
101
|
-
|
|
101
|
+
if (!options.skipInternalWarnings){
|
|
102
|
+
console.warn(`You have to install "${dependency}" in order to use htmlnano's "${moduleName}" module`);
|
|
103
|
+
}
|
|
102
104
|
} else {
|
|
103
105
|
throw e;
|
|
104
106
|
}
|
|
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.default = minifyConditionalComments;
|
|
7
7
|
var _htmlnano = _interopRequireDefault(require("../htmlnano.cjs"));
|
|
8
8
|
var _helpers = require("../helpers.cjs");
|
|
9
|
-
function _interopRequireDefault(
|
|
9
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
10
|
// Spec: https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/compatibility/ms537512(v=vs.85)
|
|
11
11
|
const CONDITIONAL_COMMENT_REGEXP = /(<!--\[if\s+?[^<>[\]]+?]>)([\s\S]+?)(<!\[endif\]-->)/gm;
|
|
12
12
|
|
|
@@ -98,14 +98,14 @@ async function minifyUrls(tree, options, moduleOptions) {
|
|
|
98
98
|
if (isSrcsetAttribute(node.tag, attrNameLower)) {
|
|
99
99
|
if (srcset) {
|
|
100
100
|
try {
|
|
101
|
-
const parsedSrcset = srcset.
|
|
101
|
+
const parsedSrcset = srcset.parseSrcset(attrValue, {
|
|
102
102
|
strict: true
|
|
103
103
|
});
|
|
104
|
-
node.attrs[attrName] = srcset.
|
|
104
|
+
node.attrs[attrName] = srcset.stringifySrcset(parsedSrcset.map(item => {
|
|
105
105
|
if (relateUrlInstance) {
|
|
106
|
-
|
|
106
|
+
item.url = relateUrlInstance.relate(item.url);
|
|
107
107
|
}
|
|
108
|
-
return
|
|
108
|
+
return item;
|
|
109
109
|
}));
|
|
110
110
|
} catch (e) {
|
|
111
111
|
// srcset will throw an Error for invalid srcset.
|
|
@@ -175,14 +175,14 @@ export default async function minifyUrls(tree, options, moduleOptions) {
|
|
|
175
175
|
if (isSrcsetAttribute(node.tag, attrNameLower)) {
|
|
176
176
|
if (srcset) {
|
|
177
177
|
try {
|
|
178
|
-
const parsedSrcset = srcset.
|
|
178
|
+
const parsedSrcset = srcset.parseSrcset(attrValue, { strict: true });
|
|
179
179
|
|
|
180
|
-
node.attrs[attrName] = srcset.
|
|
180
|
+
node.attrs[attrName] = srcset.stringifySrcset(parsedSrcset.map(item => {
|
|
181
181
|
if (relateUrlInstance) {
|
|
182
|
-
|
|
182
|
+
item.url = relateUrlInstance.relate(item.url);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
return
|
|
185
|
+
return item;
|
|
186
186
|
}));
|
|
187
187
|
} catch (e) {
|
|
188
188
|
// srcset will throw an Error for invalid srcset.
|
|
@@ -4,7 +4,6 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = sortAttributes;
|
|
7
|
-
var _timsort = require("timsort");
|
|
8
7
|
const validOptions = new Set(['frequency', 'alphabetical']);
|
|
9
8
|
const processModuleOptions = options => {
|
|
10
9
|
if (options === true) return 'alphabetical';
|
|
@@ -14,7 +13,6 @@ class AttributeTokenChain {
|
|
|
14
13
|
constructor() {
|
|
15
14
|
this.freqData = new Map(); // <attr, frequency>[]
|
|
16
15
|
}
|
|
17
|
-
|
|
18
16
|
addFromNodeAttrs(nodeAttrs) {
|
|
19
17
|
Object.keys(nodeAttrs).forEach(attrName => {
|
|
20
18
|
const attrNameLower = attrName.toLowerCase();
|
|
@@ -27,7 +25,7 @@ class AttributeTokenChain {
|
|
|
27
25
|
}
|
|
28
26
|
createSortOrder() {
|
|
29
27
|
let _sortOrder = [...this.freqData.entries()];
|
|
30
|
-
|
|
28
|
+
_sortOrder.sort((a, b) => b[1] - a[1]);
|
|
31
29
|
this.sortOrder = _sortOrder.map(i => i[0]);
|
|
32
30
|
}
|
|
33
31
|
sortFromNodeAttrs(nodeAttrs) {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { sort as timSort } from 'timsort';
|
|
2
|
-
|
|
3
1
|
const validOptions = new Set(['frequency', 'alphabetical']);
|
|
4
2
|
|
|
5
3
|
const processModuleOptions = options => {
|
|
@@ -27,7 +25,7 @@ class AttributeTokenChain {
|
|
|
27
25
|
|
|
28
26
|
createSortOrder() {
|
|
29
27
|
let _sortOrder = [...this.freqData.entries()];
|
|
30
|
-
|
|
28
|
+
_sortOrder.sort((a, b) => b[1] - a[1]);
|
|
31
29
|
|
|
32
30
|
this.sortOrder = _sortOrder.map(i => i[0]);
|
|
33
31
|
}
|
|
@@ -4,7 +4,6 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = collapseAttributeWhitespace;
|
|
7
|
-
var _timsort = require("timsort");
|
|
8
7
|
var _collapseAttributeWhitespace = require("./collapseAttributeWhitespace.cjs");
|
|
9
8
|
// class, rel, ping
|
|
10
9
|
|
|
@@ -17,7 +16,6 @@ class AttributeTokenChain {
|
|
|
17
16
|
constructor() {
|
|
18
17
|
this.freqData = new Map(); // <attrValue, frequency>[]
|
|
19
18
|
}
|
|
20
|
-
|
|
21
19
|
addFromNodeAttrsArray(attrValuesArray) {
|
|
22
20
|
attrValuesArray.forEach(attrValue => {
|
|
23
21
|
if (this.freqData.has(attrValue)) {
|
|
@@ -29,7 +27,7 @@ class AttributeTokenChain {
|
|
|
29
27
|
}
|
|
30
28
|
createSortOrder() {
|
|
31
29
|
let _sortOrder = [...this.freqData.entries()];
|
|
32
|
-
|
|
30
|
+
_sortOrder.sort((a, b) => b[1] - a[1]);
|
|
33
31
|
this.sortOrder = _sortOrder.map(i => i[0]);
|
|
34
32
|
}
|
|
35
33
|
sortFromNodeAttrsArray(attrValuesArray) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// class, rel, ping
|
|
2
|
-
import { sort as timSort } from 'timsort';
|
|
3
2
|
import { attributesWithLists } from './collapseAttributeWhitespace.mjs';
|
|
4
3
|
|
|
5
4
|
const validOptions = new Set(['frequency', 'alphabetical']);
|
|
@@ -26,7 +25,7 @@ class AttributeTokenChain {
|
|
|
26
25
|
|
|
27
26
|
createSortOrder() {
|
|
28
27
|
let _sortOrder = [...this.freqData.entries()];
|
|
29
|
-
|
|
28
|
+
_sortOrder.sort((a, b) => b[1] - a[1]);
|
|
30
29
|
|
|
31
30
|
this.sortOrder = _sortOrder.map(i => i[0]);
|
|
32
31
|
}
|
package/lib/presets/ampSafe.cjs
CHANGED
|
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
7
|
var _safe = _interopRequireDefault(require("./safe.cjs"));
|
|
8
|
-
function _interopRequireDefault(
|
|
8
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
9
|
/**
|
|
10
10
|
* A safe preset for AMP pages (https://www.ampproject.org)
|
|
11
11
|
*/
|
package/lib/presets/max.cjs
CHANGED
|
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
7
|
var _safe = _interopRequireDefault(require("./safe.cjs"));
|
|
8
|
-
function _interopRequireDefault(
|
|
8
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
9
|
/**
|
|
10
10
|
* Maximal minification (might break some pages)
|
|
11
11
|
*/
|
package/package.json
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "htmlnano",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "Modular HTML minifier, built on top of the PostHTML",
|
|
5
5
|
"main": "index.cjs",
|
|
6
6
|
"module": "index.mjs",
|
|
7
7
|
"source": "index.mjs",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
10
11
|
"require": "./index.cjs",
|
|
11
12
|
"import": "./index.mjs"
|
|
12
13
|
},
|
|
13
14
|
"./index.mjs": {
|
|
15
|
+
"types": "./index.d.mts",
|
|
14
16
|
"import": "./index.mjs"
|
|
15
17
|
},
|
|
16
18
|
"./index.cjs": {
|
|
19
|
+
"types": "./index.d.cts",
|
|
17
20
|
"require": "./index.cjs"
|
|
18
21
|
}
|
|
19
22
|
},
|
|
@@ -59,38 +62,37 @@
|
|
|
59
62
|
]
|
|
60
63
|
},
|
|
61
64
|
"dependencies": {
|
|
62
|
-
"cosmiconfig": "^
|
|
63
|
-
"posthtml": "^0.16.5"
|
|
64
|
-
"timsort": "^0.3.0"
|
|
65
|
+
"cosmiconfig": "^9.0.0",
|
|
66
|
+
"posthtml": "^0.16.5"
|
|
65
67
|
},
|
|
66
68
|
"devDependencies": {
|
|
69
|
+
"@aminya/babel-plugin-replace-import-extension": "1.2.0",
|
|
67
70
|
"@babel/cli": "^7.15.7",
|
|
68
71
|
"@babel/core": "^7.15.5",
|
|
69
72
|
"@babel/eslint-parser": "^7.17.0",
|
|
70
73
|
"@babel/preset-env": "^7.15.6",
|
|
71
74
|
"@babel/register": "^7.15.3",
|
|
72
|
-
"
|
|
73
|
-
"cssnano": "^6.0.0",
|
|
75
|
+
"cssnano": "^7.0.0",
|
|
74
76
|
"eslint": "^8.12.0",
|
|
75
77
|
"eslint-plugin-import": "^2.28.1",
|
|
76
78
|
"eslint-plugin-path-import-extension": "^0.9.0",
|
|
77
79
|
"expect": "^29.0.0",
|
|
78
|
-
"mocha": "^
|
|
80
|
+
"mocha": "^11.0.1",
|
|
79
81
|
"postcss": "^8.3.11",
|
|
80
|
-
"purgecss": "^
|
|
82
|
+
"purgecss": "^7.0.2",
|
|
81
83
|
"relateurl": "^0.2.7",
|
|
82
|
-
"rimraf": "^
|
|
83
|
-
"srcset": "
|
|
84
|
+
"rimraf": "^6.0.0",
|
|
85
|
+
"srcset": "5.0.1",
|
|
84
86
|
"svgo": "^3.0.2",
|
|
85
87
|
"terser": "^5.21.0",
|
|
86
88
|
"uncss": "^0.17.3"
|
|
87
89
|
},
|
|
88
90
|
"peerDependencies": {
|
|
89
|
-
"cssnano": "^
|
|
91
|
+
"cssnano": "^7.0.0",
|
|
90
92
|
"postcss": "^8.3.11",
|
|
91
|
-
"purgecss": "^
|
|
93
|
+
"purgecss": "^7.0.2",
|
|
92
94
|
"relateurl": "^0.2.7",
|
|
93
|
-
"srcset": "
|
|
95
|
+
"srcset": "5.0.1",
|
|
94
96
|
"svgo": "^3.0.2",
|
|
95
97
|
"terser": "^5.10.0",
|
|
96
98
|
"uncss": "^0.17.3"
|
package/test.js
CHANGED
|
@@ -1,39 +1,13 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
1
3
|
const htmlnano = require('.');
|
|
2
4
|
// const posthtml = require('posthtml');
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
// minifySvg: false,
|
|
6
|
-
// minifyJs: false,
|
|
7
|
-
// };
|
|
8
|
-
// // posthtml, posthtml-render, and posthtml-parse options
|
|
9
|
-
// const postHtmlOptions = {
|
|
10
|
-
// sync: true, // https://github.com/posthtml/posthtml#usage
|
|
11
|
-
// lowerCaseTags: true, // https://github.com/posthtml/posthtml-parser#options
|
|
12
|
-
// quoteAllAttributes: false, // https://github.com/posthtml/posthtml-render#options
|
|
13
|
-
// };
|
|
5
|
+
const preset = require('./lib/presets/max.cjs');
|
|
6
|
+
|
|
14
7
|
|
|
15
|
-
|
|
16
|
-
// <!doctype html>
|
|
17
|
-
// <html lang="en">
|
|
18
|
-
// <head>
|
|
19
|
-
// <meta charset="utf-8">
|
|
20
|
-
// <title></title>
|
|
21
|
-
// <script class="fob">alert(1)</script>
|
|
22
|
-
// <script>alert(2)</script>
|
|
23
|
-
// </head>
|
|
24
|
-
// <body>
|
|
25
|
-
// <script>alert(3)</script>
|
|
26
|
-
// <script>alert(4)</script>
|
|
27
|
-
// </body>
|
|
28
|
-
// </html>
|
|
29
|
-
// `;
|
|
8
|
+
const html = fs.readFileSync('./test.html', 'utf8');
|
|
30
9
|
|
|
31
|
-
const
|
|
32
|
-
minifySvg: safePreset.minifySvg,
|
|
33
|
-
};
|
|
34
|
-
const html = `
|
|
35
|
-
<input type="text" class="form-control" name="testInput" autofocus="" autocomplete="off" id="testId"><a id="testId" href="#" class="testClass"></a><img width="20" src="../images/image.png" height="40" alt="image" class="cls" id="id2">
|
|
36
|
-
`;
|
|
10
|
+
const startTime = Date.now();
|
|
37
11
|
|
|
38
12
|
htmlnano
|
|
39
13
|
// "preset" arg might be skipped (see "Presets" section below for more info)
|
|
@@ -41,7 +15,8 @@ htmlnano
|
|
|
41
15
|
.process(html)
|
|
42
16
|
.then(function (result) {
|
|
43
17
|
// result.html is minified
|
|
44
|
-
console.log(result.html);
|
|
18
|
+
// console.log(result.html);
|
|
19
|
+
console.log(`Time taken: ${Date.now() - startTime}ms`);
|
|
45
20
|
})
|
|
46
21
|
.catch(function (err) {
|
|
47
22
|
console.error(err);
|