htmlnano 2.0.1 → 2.0.3

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +2 -2
  3. package/docs/docs/010-introduction.md +4 -4
  4. package/docs/docs/020-usage.md +63 -23
  5. package/docs/docs/030-config.md +1 -1
  6. package/docs/docs/050-modules.md +500 -483
  7. package/docs/package-lock.json +289 -95
  8. package/docs/versioned_docs/version-1.1.1/010-introduction.md +4 -4
  9. package/docs/versioned_docs/version-1.1.1/030-config.md +1 -1
  10. package/docs/versioned_docs/version-2.0.0/010-introduction.md +4 -4
  11. package/docs/versioned_docs/version-2.0.0/030-config.md +2 -2
  12. package/index.d.ts +93 -0
  13. package/lib/helpers.js +4 -11
  14. package/lib/htmlnano.js +37 -55
  15. package/lib/modules/collapseAttributeWhitespace.js +11 -12
  16. package/lib/modules/collapseBooleanAttributes.js +33 -9
  17. package/lib/modules/collapseWhitespace.js +17 -19
  18. package/lib/modules/custom.js +0 -3
  19. package/lib/modules/deduplicateAttributeValues.js +3 -5
  20. package/lib/modules/mergeScripts.js +0 -11
  21. package/lib/modules/mergeStyles.js +2 -8
  22. package/lib/modules/minifyConditionalComments.js +4 -15
  23. package/lib/modules/minifyCss.js +5 -16
  24. package/lib/modules/minifyJs.js +8 -28
  25. package/lib/modules/minifyJson.js +5 -7
  26. package/lib/modules/minifySvg.js +13 -4
  27. package/lib/modules/minifyUrls.js +18 -34
  28. package/lib/modules/normalizeAttributeValues.js +85 -2
  29. package/lib/modules/removeAttributeQuotes.js +0 -2
  30. package/lib/modules/removeComments.js +10 -28
  31. package/lib/modules/removeEmptyAttributes.js +6 -6
  32. package/lib/modules/removeOptionalTags.js +9 -46
  33. package/lib/modules/removeRedundantAttributes.js +20 -68
  34. package/lib/modules/removeUnusedCss.js +7 -18
  35. package/lib/modules/sortAttributes.js +10 -25
  36. package/lib/modules/sortAttributesWithLists.js +7 -29
  37. package/lib/presets/ampSafe.js +2 -5
  38. package/lib/presets/max.js +2 -5
  39. package/lib/presets/safe.js +32 -15
  40. package/package.json +9 -15
  41. package/test.js +0 -48
@@ -4,17 +4,15 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = removeUnusedCss;
7
-
8
7
  var _helpers = require("../helpers");
9
-
10
8
  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.
9
+ const purgecss = (0, _helpers.optionalRequire)('purgecss');
12
10
 
11
+ // These options must be set and shouldn't be overriden to ensure uncss doesn't look at linked stylesheets.
13
12
  const uncssOptions = {
14
13
  ignoreSheets: [/\s*/],
15
14
  stylesheets: []
16
15
  };
17
-
18
16
  function processStyleNodeUnCSS(html, styleNode, uncssOptions) {
19
17
  const css = (0, _helpers.extractCssFromStyleNode)(styleNode);
20
18
  return runUncss(html, css, uncssOptions).then(css => {
@@ -24,17 +22,15 @@ function processStyleNodeUnCSS(html, styleNode, uncssOptions) {
24
22
  styleNode.content = [];
25
23
  return;
26
24
  }
27
-
28
25
  styleNode.content = [css];
29
26
  });
30
27
  }
31
-
32
28
  function runUncss(html, css, userOptions) {
33
29
  if (typeof userOptions !== 'object') {
34
30
  userOptions = {};
35
31
  }
36
-
37
- const options = { ...userOptions,
32
+ const options = {
33
+ ...userOptions,
38
34
  ...uncssOptions
39
35
  };
40
36
  return new Promise((resolve, reject) => {
@@ -44,12 +40,10 @@ function runUncss(html, css, userOptions) {
44
40
  reject(error);
45
41
  return;
46
42
  }
47
-
48
43
  resolve(output);
49
44
  });
50
45
  });
51
46
  }
52
-
53
47
  const purgeFromHtml = function (tree) {
54
48
  // content is not used as we can directly used the parsed HTML,
55
49
  // making the process faster
@@ -63,7 +57,6 @@ const purgeFromHtml = function (tree) {
63
57
  });
64
58
  return () => selectors;
65
59
  };
66
-
67
60
  function processStyleNodePurgeCSS(tree, styleNode, purgecssOptions) {
68
61
  const css = (0, _helpers.extractCssFromStyleNode)(styleNode);
69
62
  return runPurgecss(tree, css, purgecssOptions).then(css => {
@@ -72,17 +65,15 @@ function processStyleNodePurgeCSS(tree, styleNode, purgecssOptions) {
72
65
  styleNode.content = [];
73
66
  return;
74
67
  }
75
-
76
68
  styleNode.content = [css];
77
69
  });
78
70
  }
79
-
80
71
  function runPurgecss(tree, css, userOptions) {
81
72
  if (typeof userOptions !== 'object') {
82
73
  userOptions = {};
83
74
  }
84
-
85
- const options = { ...userOptions,
75
+ const options = {
76
+ ...userOptions,
86
77
  content: [{
87
78
  raw: tree,
88
79
  extension: 'html'
@@ -100,9 +91,8 @@ function runPurgecss(tree, css, userOptions) {
100
91
  return result[0].css;
101
92
  });
102
93
  }
103
- /** Remove unused CSS */
104
-
105
94
 
95
+ /** Remove unused CSS */
106
96
  function removeUnusedCss(tree, options, userOptions) {
107
97
  const promises = [];
108
98
  const html = userOptions.tool !== 'purgeCSS' && tree.render(tree);
@@ -118,7 +108,6 @@ function removeUnusedCss(tree, options, userOptions) {
118
108
  }
119
109
  }
120
110
  }
121
-
122
111
  return node;
123
112
  });
124
113
  return Promise.all(promises).then(() => tree);
@@ -4,16 +4,12 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = sortAttributes;
7
-
8
7
  var _timsort = require("timsort");
9
-
10
8
  const validOptions = new Set(['frequency', 'alphabetical']);
11
-
12
9
  const processModuleOptions = options => {
13
10
  if (options === true) return 'alphabetical';
14
11
  return validOptions.has(options) ? options : false;
15
12
  };
16
-
17
13
  class AttributeTokenChain {
18
14
  constructor() {
19
15
  this.freqData = new Map(); // <attr, frequency>[]
@@ -22,7 +18,6 @@ class AttributeTokenChain {
22
18
  addFromNodeAttrs(nodeAttrs) {
23
19
  Object.keys(nodeAttrs).forEach(attrName => {
24
20
  const attrNameLower = attrName.toLowerCase();
25
-
26
21
  if (this.freqData.has(attrNameLower)) {
27
22
  this.freqData.set(attrNameLower, this.freqData.get(attrNameLower) + 1);
28
23
  } else {
@@ -30,59 +25,50 @@ class AttributeTokenChain {
30
25
  }
31
26
  });
32
27
  }
33
-
34
28
  createSortOrder() {
35
29
  let _sortOrder = [...this.freqData.entries()];
36
30
  (0, _timsort.sort)(_sortOrder, (a, b) => b[1] - a[1]);
37
31
  this.sortOrder = _sortOrder.map(i => i[0]);
38
32
  }
39
-
40
33
  sortFromNodeAttrs(nodeAttrs) {
41
- const newAttrs = {}; // Convert node.attrs attrName into lower case.
34
+ const newAttrs = {};
42
35
 
36
+ // Convert node.attrs attrName into lower case.
43
37
  const loweredNodeAttrs = {};
44
38
  Object.entries(nodeAttrs).forEach(([attrName, attrValue]) => {
45
39
  loweredNodeAttrs[attrName.toLowerCase()] = attrValue;
46
40
  });
47
-
48
41
  if (!this.sortOrder) {
49
42
  this.createSortOrder();
50
43
  }
51
-
52
44
  this.sortOrder.forEach(attrNameLower => {
53
45
  // The attrName inside "sortOrder" has been lowered
54
- if (loweredNodeAttrs[attrNameLower]) {
46
+ if (loweredNodeAttrs[attrNameLower] != null) {
55
47
  newAttrs[attrNameLower] = loweredNodeAttrs[attrNameLower];
56
48
  }
57
49
  });
58
50
  return newAttrs;
59
51
  }
60
-
61
52
  }
62
- /** Sort attibutes */
63
-
64
53
 
54
+ /** Sort attibutes */
65
55
  function sortAttributes(tree, options, moduleOptions) {
66
56
  const sortType = processModuleOptions(moduleOptions);
67
-
68
57
  if (sortType === 'alphabetical') {
69
58
  return sortAttributesInAlphabeticalOrder(tree);
70
59
  }
71
-
72
60
  if (sortType === 'frequency') {
73
61
  return sortAttributesByFrequency(tree);
74
- } // Invalid configuration
75
-
62
+ }
76
63
 
64
+ // Invalid configuration
77
65
  return tree;
78
66
  }
79
-
80
67
  function sortAttributesInAlphabeticalOrder(tree) {
81
68
  tree.walk(node => {
82
69
  if (!node.attrs) {
83
70
  return node;
84
71
  }
85
-
86
72
  const newAttrs = {};
87
73
  Object.keys(node.attrs).sort((a, b) => typeof a.localeCompare === 'function' ? a.localeCompare(b) : a - b).forEach(attr => newAttrs[attr] = node.attrs[attr]);
88
74
  node.attrs = newAttrs;
@@ -90,24 +76,23 @@ function sortAttributesInAlphabeticalOrder(tree) {
90
76
  });
91
77
  return tree;
92
78
  }
93
-
94
79
  function sortAttributesByFrequency(tree) {
95
- const tokenchain = new AttributeTokenChain(); // Traverse through tree to get frequency
80
+ const tokenchain = new AttributeTokenChain();
96
81
 
82
+ // Traverse through tree to get frequency
97
83
  tree.walk(node => {
98
84
  if (!node.attrs) {
99
85
  return node;
100
86
  }
101
-
102
87
  tokenchain.addFromNodeAttrs(node.attrs);
103
88
  return node;
104
- }); // Traverse through tree again, this time sort the attributes
89
+ });
105
90
 
91
+ // Traverse through tree again, this time sort the attributes
106
92
  tree.walk(node => {
107
93
  if (!node.attrs) {
108
94
  return node;
109
95
  }
110
-
111
96
  node.attrs = tokenchain.sortFromNodeAttrs(node.attrs);
112
97
  return node;
113
98
  });
@@ -4,19 +4,15 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = collapseAttributeWhitespace;
7
-
8
7
  var _timsort = require("timsort");
9
-
10
8
  var _collapseAttributeWhitespace = require("./collapseAttributeWhitespace");
11
-
12
9
  // class, rel, ping
13
- const validOptions = new Set(['frequency', 'alphabetical']);
14
10
 
11
+ const validOptions = new Set(['frequency', 'alphabetical']);
15
12
  const processModuleOptions = options => {
16
13
  if (options === true) return 'alphabetical';
17
14
  return validOptions.has(options) ? options : false;
18
15
  };
19
-
20
16
  class AttributeTokenChain {
21
17
  constructor() {
22
18
  this.freqData = new Map(); // <attrValue, frequency>[]
@@ -31,20 +27,16 @@ class AttributeTokenChain {
31
27
  }
32
28
  });
33
29
  }
34
-
35
30
  createSortOrder() {
36
31
  let _sortOrder = [...this.freqData.entries()];
37
32
  (0, _timsort.sort)(_sortOrder, (a, b) => b[1] - a[1]);
38
33
  this.sortOrder = _sortOrder.map(i => i[0]);
39
34
  }
40
-
41
35
  sortFromNodeAttrsArray(attrValuesArray) {
42
36
  const resultArray = [];
43
-
44
37
  if (!this.sortOrder) {
45
38
  this.createSortOrder();
46
39
  }
47
-
48
40
  this.sortOrder.forEach(k => {
49
41
  if (attrValuesArray.includes(k)) {
50
42
  resultArray.push(k);
@@ -52,39 +44,31 @@ class AttributeTokenChain {
52
44
  });
53
45
  return resultArray;
54
46
  }
55
-
56
47
  }
57
- /** Sort values inside list-like attributes (e.g. class, rel) */
58
-
59
48
 
49
+ /** Sort values inside list-like attributes (e.g. class, rel) */
60
50
  function collapseAttributeWhitespace(tree, options, moduleOptions) {
61
51
  const sortType = processModuleOptions(moduleOptions);
62
-
63
52
  if (sortType === 'alphabetical') {
64
53
  return sortAttributesWithListsInAlphabeticalOrder(tree);
65
54
  }
66
-
67
55
  if (sortType === 'frequency') {
68
56
  return sortAttributesWithListsByFrequency(tree);
69
- } // Invalid configuration
70
-
57
+ }
71
58
 
59
+ // Invalid configuration
72
60
  return tree;
73
61
  }
74
-
75
62
  function sortAttributesWithListsInAlphabeticalOrder(tree) {
76
63
  tree.walk(node => {
77
64
  if (!node.attrs) {
78
65
  return node;
79
66
  }
80
-
81
67
  Object.keys(node.attrs).forEach(attrName => {
82
68
  const attrNameLower = attrName.toLowerCase();
83
-
84
69
  if (!_collapseAttributeWhitespace.attributesWithLists.has(attrNameLower)) {
85
70
  return;
86
71
  }
87
-
88
72
  const attrValues = node.attrs[attrName].split(/\s/);
89
73
  node.attrs[attrName] = attrValues.sort((a, b) => {
90
74
  return typeof a.localeCompare === 'function' ? a.localeCompare(b) : a - b;
@@ -94,41 +78,35 @@ function sortAttributesWithListsInAlphabeticalOrder(tree) {
94
78
  });
95
79
  return tree;
96
80
  }
97
-
98
81
  function sortAttributesWithListsByFrequency(tree) {
99
82
  const tokenChainObj = {}; // <attrNameLower: AttributeTokenChain>[]
100
- // Traverse through tree to get frequency
101
83
 
84
+ // Traverse through tree to get frequency
102
85
  tree.walk(node => {
103
86
  if (!node.attrs) {
104
87
  return node;
105
88
  }
106
-
107
89
  Object.entries(node.attrs).forEach(([attrName, attrValues]) => {
108
90
  const attrNameLower = attrName.toLowerCase();
109
-
110
91
  if (!_collapseAttributeWhitespace.attributesWithLists.has(attrNameLower)) {
111
92
  return;
112
93
  }
113
-
114
94
  tokenChainObj[attrNameLower] = tokenChainObj[attrNameLower] || new AttributeTokenChain();
115
95
  tokenChainObj[attrNameLower].addFromNodeAttrsArray(attrValues.split(/\s/));
116
96
  });
117
97
  return node;
118
- }); // Traverse through tree again, this time sort the attribute values
98
+ });
119
99
 
100
+ // Traverse through tree again, this time sort the attribute values
120
101
  tree.walk(node => {
121
102
  if (!node.attrs) {
122
103
  return node;
123
104
  }
124
-
125
105
  Object.entries(node.attrs).forEach(([attrName, attrValues]) => {
126
106
  const attrNameLower = attrName.toLowerCase();
127
-
128
107
  if (!_collapseAttributeWhitespace.attributesWithLists.has(attrNameLower)) {
129
108
  return;
130
109
  }
131
-
132
110
  if (tokenChainObj[attrNameLower]) {
133
111
  node.attrs[attrName] = tokenChainObj[attrNameLower].sortFromNodeAttrsArray(attrValues.split(/\s/)).join(' ');
134
112
  }
@@ -4,9 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
-
8
7
  var _safe = _interopRequireDefault(require("./safe"));
9
-
10
8
  function _interopRequireDefault(obj) {
11
9
  return obj && obj.__esModule ? obj : {
12
10
  default: obj
@@ -15,9 +13,8 @@ function _interopRequireDefault(obj) {
15
13
  /**
16
14
  * A safe preset for AMP pages (https://www.ampproject.org)
17
15
  */
18
-
19
-
20
- var _default = { ..._safe.default,
16
+ var _default = {
17
+ ..._safe.default,
21
18
  collapseBooleanAttributes: {
22
19
  amphtml: true
23
20
  },
@@ -4,9 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
-
8
7
  var _safe = _interopRequireDefault(require("./safe"));
9
-
10
8
  function _interopRequireDefault(obj) {
11
9
  return obj && obj.__esModule ? obj : {
12
10
  default: obj
@@ -15,9 +13,8 @@ function _interopRequireDefault(obj) {
15
13
  /**
16
14
  * Maximal minification (might break some pages)
17
15
  */
18
-
19
-
20
- var _default = { ..._safe.default,
16
+ var _default = {
17
+ ..._safe.default,
21
18
  collapseWhitespace: 'all',
22
19
  removeComments: 'all',
23
20
  removeAttributeQuotes: true,
@@ -7,19 +7,40 @@ exports.default = void 0;
7
7
  /**
8
8
  * Minify HTML in a safe way without breaking anything.
9
9
  */
10
-
11
10
  var _default = {
12
- sortAttributes: false,
11
+ /* ----------------------------------------
12
+ * Attributes
13
+ * ---------------------------------------- */
14
+ // normalize the case of attribute names and values
15
+ // normalizeAttributeValues will also normalize property value with invalid value default
16
+ // See https://html.spec.whatwg.org/#invalid-value-default
17
+ normalizeAttributeValues: true,
18
+ removeEmptyAttributes: true,
13
19
  collapseAttributeWhitespace: true,
20
+ // removeRedundantAttributes will remove attributes when missing value default matches the attribute's value
21
+ // See https://html.spec.whatwg.org/#missing-value-default
22
+ removeRedundantAttributes: false,
23
+ // collapseBooleanAttributes will also collapse those default state can be omitted
14
24
  collapseBooleanAttributes: {
15
25
  amphtml: false
16
26
  },
17
- collapseWhitespace: 'conservative',
18
- custom: [],
19
27
  deduplicateAttributeValues: true,
20
- mergeScripts: false,
28
+ minifyUrls: false,
29
+ sortAttributes: false,
30
+ sortAttributesWithLists: 'alphabetical',
31
+ /* ----------------------------------------
32
+ * Minify HTML content
33
+ * ---------------------------------------- */
34
+ collapseWhitespace: 'conservative',
35
+ removeComments: 'safe',
36
+ minifyConditionalComments: false,
37
+ removeOptionalTags: false,
38
+ removeAttributeQuotes: false,
39
+ /* ----------------------------------------
40
+ * Minify inline <style>, <script> and <svg> tag
41
+ * ---------------------------------------- */
21
42
  mergeStyles: false,
22
- removeUnusedCss: false,
43
+ mergeScripts: false,
23
44
  minifyCss: {
24
45
  preset: 'default'
25
46
  },
@@ -36,14 +57,10 @@ var _default = {
36
57
  }
37
58
  }]
38
59
  },
39
- minifyConditionalComments: false,
40
- removeRedundantAttributes: false,
41
- normalizeAttributeValues: true,
42
- removeEmptyAttributes: true,
43
- removeComments: 'safe',
44
- removeAttributeQuotes: false,
45
- sortAttributesWithLists: 'alphabetical',
46
- minifyUrls: false,
47
- removeOptionalTags: false
60
+ removeUnusedCss: false,
61
+ /* ----------------------------------------
62
+ * Miscellaneous
63
+ * ---------------------------------------- */
64
+ custom: []
48
65
  };
49
66
  exports.default = _default;
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "htmlnano",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Modular HTML minifier, built on top of the PostHTML",
5
5
  "main": "index.js",
6
+ "types": "index.d.ts",
6
7
  "author": "Kirill Maltsev <maltsevkirill@gmail.com>",
7
8
  "license": "MIT",
8
9
  "scripts": {
@@ -11,13 +12,7 @@
11
12
  "pretest": "npm run lint && npm run compile",
12
13
  "test": ":",
13
14
  "posttest": "mocha --timeout 5000 --require @babel/register --recursive --check-leaks --globals addresses",
14
- "prepare": "npm run compile",
15
- "release:patch": "release-it patch -n"
16
- },
17
- "release-it": {
18
- "hooks": {
19
- "before:init": "npm test"
20
- }
15
+ "prepare": "npm run compile"
21
16
  },
22
17
  "keywords": [
23
18
  "posthtml",
@@ -49,16 +44,15 @@
49
44
  "@babel/eslint-parser": "^7.17.0",
50
45
  "@babel/preset-env": "^7.15.6",
51
46
  "@babel/register": "^7.15.3",
52
- "cssnano": "^5.0.11",
47
+ "cssnano": "^5.1.12",
53
48
  "eslint": "^8.12.0",
54
49
  "expect": "^27.2.0",
55
- "mocha": "^9.1.0",
50
+ "mocha": "^10.1.0",
56
51
  "postcss": "^8.3.11",
57
- "purgecss": "^4.0.3",
52
+ "purgecss": "^5.0.0",
58
53
  "relateurl": "^0.2.7",
59
- "release-it": "^14.11.5",
60
54
  "rimraf": "^3.0.2",
61
- "srcset": "^5.0.0",
55
+ "srcset": "4.0.0",
62
56
  "svgo": "^2.8.0",
63
57
  "terser": "^5.10.0",
64
58
  "uncss": "^0.17.3"
@@ -66,9 +60,9 @@
66
60
  "peerDependencies": {
67
61
  "cssnano": "^5.0.11",
68
62
  "postcss": "^8.3.11",
69
- "purgecss": "^4.0.3",
63
+ "purgecss": "^5.0.0",
70
64
  "relateurl": "^0.2.7",
71
- "srcset": "^5.0.0",
65
+ "srcset": "4.0.0",
72
66
  "svgo": "^2.8.0",
73
67
  "terser": "^5.10.0",
74
68
  "uncss": "^0.17.3"
package/test.js DELETED
@@ -1,48 +0,0 @@
1
- const htmlnano = require('.');
2
- // const posthtml = require('posthtml');
3
- const safePreset = require('./lib/presets/safe');
4
- // const options = {
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
- // };
14
-
15
- // const html = `
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
- // `;
30
-
31
- const options = {
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
- `;
37
-
38
- htmlnano
39
- // "preset" arg might be skipped (see "Presets" section below for more info)
40
- // "postHtmlOptions" arg might be skipped
41
- .process(html)
42
- .then(function (result) {
43
- // result.html is minified
44
- console.log(result.html);
45
- })
46
- .catch(function (err) {
47
- console.error(err);
48
- });