@uxf/scripts 1.5.0 → 1.5.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.
package/.eslintrc.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "parser": "babel-eslint"
2
+ "parser": "@babel/eslint-parser"
3
3
  }
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ /**
3
+ babel.config.js with useful plugins.
4
+ */
5
+ module.exports = function (api) {
6
+ api.cache(true);
7
+ api.assertVersion("^7.4.5");
8
+
9
+ return {
10
+ presets: [
11
+ [
12
+ "@babel/preset-env",
13
+ {
14
+ targets: {
15
+ esmodules: true,
16
+ node: true,
17
+ },
18
+ },
19
+ ],
20
+ ],
21
+ };
22
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uxf/scripts",
3
- "version": "1.5.0",
3
+ "version": "1.5.3",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -23,8 +23,9 @@
23
23
  "yargs": "^16.0.3"
24
24
  },
25
25
  "devDependencies": {
26
- "babel-eslint": "^10.1.0",
27
- "eslint": "^7.20.0",
26
+ "@babel/eslint-parser": "^7.17.0",
27
+ "@babel/preset-env": "^7.16.11",
28
+ "eslint": "^8.8.0",
28
29
  "prettier": "^2.1.2"
29
30
  },
30
31
  "scripts": {
package/src/Sitemap.js CHANGED
@@ -4,31 +4,32 @@ const cheerio = require("cheerio");
4
4
  const { HTTP_USERNAME, HTTP_PASSWORD } = process.env;
5
5
 
6
6
  async function getSitemap(xml) {
7
- const { data } = await axios.get(xml);
8
- const $ = cheerio.load(data, { xmlMode: true });
7
+ const { data } = await axios.get(xml);
8
+ const $ = cheerio.load(data, { xmlMode: true });
9
9
 
10
- const urls = [];
10
+ const urls = [];
11
11
 
12
- $("loc").each(function () {
13
- urls.push($(this).text());
14
- });
12
+ $("loc").each(function () {
13
+ urls.push($(this).text());
14
+ });
15
15
 
16
- return urls;
16
+ return urls;
17
17
  }
18
18
 
19
19
  const axios = create({
20
- auth:
21
- HTTP_PASSWORD && HTTP_USERNAME
22
- ? {
23
- username: HTTP_USERNAME,
24
- password: HTTP_PASSWORD,
25
- }
26
- : undefined,
27
- withCredentials: true,
28
- maxRedirects: 0,
20
+ auth:
21
+ HTTP_PASSWORD && HTTP_USERNAME
22
+ ? {
23
+ username: HTTP_USERNAME,
24
+ password: HTTP_PASSWORD,
25
+ }
26
+ : undefined,
27
+ withCredentials: true,
28
+ maxRedirects: 0,
29
+ timeout: 20000,
29
30
  });
30
31
 
31
32
  module.exports = {
32
- getSitemap,
33
- axios
33
+ getSitemap,
34
+ axios,
34
35
  };
package/src/Slack.js CHANGED
@@ -15,12 +15,14 @@ const axios = create({
15
15
  async function chatPostMessage(channel, data, dryRun = false) {
16
16
  if (env.SLACK_TOKEN && !dryRun) {
17
17
  const res = await axios.post("/chat.postMessage", { ...data, channel });
18
- if (res.data.ok !== true) {
18
+ if (res.data.ok === false) {
19
19
  process.stdout.write("SLACK: chat.postMessage error - " + JSON.stringify(res.data));
20
+ return;
20
21
  }
21
- } else {
22
- process.stdout.write("SLACK: chat.postMessage - skipped\n");
22
+ process.stdout.write("SLACK: chat.postMessage - done\n");
23
+ return;
23
24
  }
25
+ process.stdout.write("SLACK: chat.postMessage - skipped\n");
24
26
  }
25
27
 
26
28
  module.exports = {
@@ -44,6 +44,11 @@ Environment variables:
44
44
  type: "string",
45
45
  group: "Options",
46
46
  })
47
+ .option("test-nested", {
48
+ describe: "If nested urls should be tested.",
49
+ type: "boolean",
50
+ group: "Options",
51
+ })
47
52
  .option("h", { alias: "help", group: "Options" })
48
53
  .strict(false)
49
54
  .exitProcess(false);
@@ -62,7 +67,7 @@ Environment variables:
62
67
  env.HTTP_PASSWORD = options["http-password"];
63
68
  }
64
69
 
65
- await require("./index")(url, webUrl, options["slack-channel"], skip);
70
+ await require("./index")(url, webUrl, options["slack-channel"], skip, options["test-nested"]);
66
71
  } catch (e) {
67
72
  console.error(e);
68
73
  return 1;
@@ -2,11 +2,114 @@ const Slack = require("../Slack");
2
2
  const Sitemap = require("../Sitemap");
3
3
  const { performance } = require("perf_hooks");
4
4
  const { env, stdout } = require("process");
5
+ const { axios } = require("../Sitemap");
6
+ const cheerio = require("cheerio");
7
+
8
+ /**
9
+ *
10
+ * @typedef {{parentUrl: (string | undefined), isImg: boolean, time: number, ttl: number, url: string, status: number}} UrlCheckResponse
11
+ */
5
12
 
6
13
  const MAX_TTL = 3;
14
+ const TESTED_URLS = [];
15
+ const IMAGES_LABEL = "🏞 Images:";
16
+ const URLS_LABEL = "🔗 Links:";
17
+
18
+ /**
19
+ *
20
+ * @param length {number}
21
+ * @return {string}
22
+ */
23
+ function createTabSpace(length = 1) {
24
+ return "".padStart(2 * length);
25
+ }
26
+
27
+ /**
28
+ *
29
+ * @param url {string}
30
+ * @return {boolean}
31
+ */
32
+ function isImageUrl(url) {
33
+ return new RegExp("[a-z].*(\\.jpg|\\.png|\\.webp|\\.avif|\\.gif)$", "gim").test(url);
34
+ }
35
+
36
+ /**
37
+ *
38
+ * @param errors {UrlCheckResponse[]}
39
+ * @return {string}
40
+ */
41
+ function createErrorList(errors) {
42
+ return errors.map(err => `${createTabSpace(3)}${err.url}${createTabSpace()}${err.status}`).join("\n");
43
+ }
44
+
45
+ /**
46
+ *
47
+ * @param errors {UrlCheckResponse[]}
48
+ * @return {string}
49
+ */
50
+ function createErrorResult(errors) {
51
+ let parentPages = "";
52
+ let nestedPages = "";
53
+
54
+ const parentPagesErrors = errors.filter(url => url.parentUrl === undefined);
55
+ if (parentPagesErrors.length > 0) {
56
+ parentPages = `${createTabSpace()}Pages from sitemap:\n${createErrorList(parentPagesErrors)}\n`;
57
+ }
58
+
59
+ const nestedPagesErrors = errors
60
+ .filter(url => url.parentUrl !== undefined)
61
+ .sort((prev, curr) => prev.parentUrl.localeCompare(curr.parentUrl));
62
+ for (let i = 0; i < nestedPagesErrors.length; i++) {
63
+ if (i === 0) {
64
+ nestedPages = `${createTabSpace()}Nested pages:\n`;
65
+ nestedPages += `${createTabSpace(1)}Page: ${nestedPagesErrors[i].parentUrl}\n`;
66
+ } else {
67
+ if (nestedPagesErrors[i].parentUrl === nestedPagesErrors[i - 1].parentUrl) {
68
+ continue;
69
+ } else {
70
+ nestedPages += `${createTabSpace(1)}Page: ${nestedPagesErrors[i].parentUrl}\n`;
71
+ }
72
+ }
73
+ const images = nestedPagesErrors.filter(err => err.parentUrl === nestedPagesErrors[i].parentUrl && err.isImg);
74
+ const links = nestedPagesErrors.filter(err => err.parentUrl === nestedPagesErrors[i].parentUrl && !err.isImg);
75
+ if (images.length > 0) {
76
+ nestedPages += `${createTabSpace(2)}${IMAGES_LABEL}\n${createErrorList(images)}\n`;
77
+ }
78
+ if (links.length > 0) {
79
+ nestedPages += `${createTabSpace(2)}${URLS_LABEL}\n${createErrorList(links)}\n`;
80
+ }
81
+ }
82
+
83
+ return parentPages + nestedPages;
84
+ }
7
85
 
8
- async function tryUrl(url, ttl = 1) {
86
+ /**
87
+ *
88
+ * @param incorrectLinks {string[]}
89
+ * @param webUrl {string}
90
+ * @return {string[]}
91
+ */
92
+ function createCorrectLinks(incorrectLinks, webUrl) {
93
+ return [
94
+ ...new Set(
95
+ incorrectLinks
96
+ .filter((i, url) => url && new RegExp("^(\\/|http)").test(url))
97
+ .map((i, url) => (url.startsWith("http") ? url : webUrl + url)),
98
+ ),
99
+ ];
100
+ }
101
+
102
+ /**
103
+ *
104
+ * @param url {string}
105
+ * @param parentUrl {string | undefined}
106
+ * @param ttl {number}
107
+ * @return {Promise<UrlCheckResponse>}
108
+ */
109
+ async function fetchUrl(url, parentUrl = undefined, ttl = 1) {
9
110
  try {
111
+ Sitemap.axios.defaults.maxRedirects = parentUrl ? 1 : 0;
112
+
10
113
  const t0 = performance.now();
11
114
  const { status } = await Sitemap.axios.get(url);
12
115
  const t1 = performance.now();
@@ -17,17 +120,21 @@ async function tryUrl(url, ttl = 1) {
17
120
 
18
121
  return {
19
122
  url,
20
- status,
123
+ parentUrl,
124
+ isImg: isImageUrl(url),
21
125
  ttl,
126
+ status,
22
127
  time: Math.ceil(t1 - t0),
23
128
  };
24
129
  } catch (e) {
25
130
  if (ttl < MAX_TTL) {
26
- return tryUrl(url, ttl + 1);
131
+ return fetchUrl(url, parentUrl, ttl + 1);
27
132
  }
28
133
 
29
134
  return {
30
135
  url,
136
+ parentUrl,
137
+ isImg: isImageUrl(url),
31
138
  ttl,
32
139
  status: Number.parseInt((e && e.response && e.response.status) || "0"),
33
140
  time: 0,
@@ -35,66 +142,214 @@ async function tryUrl(url, ttl = 1) {
35
142
  }
36
143
  }
37
144
 
38
- module.exports = async function run(sitemapUrl, webUrl, slackChannel, skip) {
39
- if (!sitemapUrl) {
40
- stdout.write("⛔ Required parameter --url is empty.\n");
41
- return process.exit(1);
145
+ /**
146
+ *
147
+ * @param url {string}
148
+ * @param parentUrl {string | undefined}
149
+ * @return {UrlCheckResponse}
150
+ */
151
+ async function testUrl(url, parentUrl = undefined) {
152
+ const indexInChecked = TESTED_URLS.findIndex(result => result.url === url);
153
+ if (indexInChecked === -1) {
154
+ const result = await fetchUrl(url, parentUrl);
155
+ TESTED_URLS.push(result);
156
+ return result;
42
157
  }
158
+ return TESTED_URLS[indexInChecked];
159
+ }
43
160
 
44
- if (slackChannel && !env.SLACK_TOKEN) {
45
- stdout.write("⛔ Environment variable SLACK_TOKEN is empty.\n");
46
- return process.exit(1);
161
+ /**
162
+ *
163
+ * @param urls {string[]}
164
+ * @param webUrl {string}
165
+ * @param sitemapUrl {string}
166
+ * @param skip {number}
167
+ * @param testNested {boolean}
168
+ * @return {Promise<void>}
169
+ */
170
+ async function testSitemapUrls(urls, webUrl, sitemapUrl, skip, testNested) {
171
+ for (let i = skip || 0; i < urls.length; i++) {
172
+ const url = urls[i];
173
+ const changedUrl = webUrl ? `${webUrl}${new URL(url).pathname}` : null;
174
+
175
+ printUrlInfo(changedUrl ?? url, i, urls.length);
176
+ printUrlResult(await testUrl(changedUrl ?? url));
177
+
178
+ if (testNested) {
179
+ await testAllNestedUrls(changedUrl ?? url, i, webUrl ?? sitemapUrl.split("/").slice(0, 3).join("/"));
180
+ }
47
181
  }
182
+ }
48
183
 
49
- if (webUrl) {
50
- stdout.write(` Sitemap url: ${sitemapUrl}\n`);
51
- stdout.write(`❗ Web url is defined: ${webUrl}\n\n`);
184
+ /**
185
+ *
186
+ * @param parentUrl {string}
187
+ * @param parentIndex {number}
188
+ * @param webUrl {string}
189
+ * @return {Promise<void>}
190
+ */
191
+ async function testAllNestedUrls(parentUrl, parentIndex, webUrl) {
192
+ const { data } = await axios.get(parentUrl);
193
+ const $ = cheerio.load(data);
194
+ const urls = createCorrectLinks(
195
+ $("a").map((i, node) => $(node).attr("href")),
196
+ webUrl,
197
+ );
198
+ const images = createCorrectLinks(
199
+ $("img").map((i, node) => $(node).attr("src")),
200
+ webUrl,
201
+ );
202
+
203
+ await testNested(images, parentIndex, parentUrl, createTabSpace() + IMAGES_LABEL);
204
+ await testNested(urls, parentIndex, parentUrl, createTabSpace() + URLS_LABEL);
205
+ }
206
+
207
+ /**
208
+ *
209
+ * @param urls {string[]}
210
+ * @param parentIndex {number}
211
+ * @param parentUrl {string}
212
+ * @param label {string}
213
+ * @return {Promise<void>}
214
+ */
215
+ async function testNested(urls, parentIndex, parentUrl, label) {
216
+ if (urls.length === 0) {
217
+ return;
218
+ }
219
+
220
+ stdout.write(label + "\n");
221
+ for (let i = 0; i < urls.length; i++) {
222
+ if (TESTED_URLS.findIndex(result => result.url === urls[i]) !== -1) {
223
+ continue;
224
+ }
225
+ printUrlInfo(urls[i], i, urls.length, `${createTabSpace(2)}(${parentIndex + 1}) `);
226
+ const result = await testUrl(urls[i], parentUrl);
227
+ printUrlResult(result);
52
228
  }
229
+ }
53
230
 
54
- const urls = await Sitemap.getSitemap(sitemapUrl);
231
+ /**
232
+ *
233
+ * @param url {string}
234
+ * @param urlIndex {number}
235
+ * @param allUrlsCount {number}
236
+ * @param prefix {string}
237
+ */
238
+ function printUrlInfo(url, urlIndex, allUrlsCount, prefix = "") {
239
+ stdout.write(`${prefix}${urlIndex + 1} / ${allUrlsCount}${createTabSpace()}${url}`);
240
+ }
55
241
 
56
- const results = [];
242
+ /**
243
+ *
244
+ * @param result {UrlCheckResponse}
245
+ */
246
+ function printUrlResult(result) {
247
+ const { ttl, status, time } = result;
248
+ stdout.write(`${createTabSpace()}${status} (${time}ms) ttl=${ttl} ${status === 200 ? "✅ " : "❌ "}\n`);
249
+ }
57
250
 
58
- for (let i = skip || 0; i < urls.length; i++) {
59
- const url = urls[i];
60
- const changedUrl = webUrl ? `${webUrl}${new URL(url).pathname}` : null;
251
+ /**
252
+ *
253
+ * @param errorText {string}
254
+ */
255
+ function logErrors(errorText) {
256
+ stdout.write("\nErrors:\n");
257
+ stdout.write(`${errorText}\n\n`);
258
+ }
259
+
260
+ /**
261
+ *
262
+ * @param millis {number}
263
+ * @return {string}
264
+ */
265
+ function convertTime(millis) {
266
+ let minutes = Math.floor(millis / 60000);
267
+ let seconds = ((millis % 60000) / 1000).toFixed(0);
268
+ return `${minutes} min ${seconds < 10 ? "0" : ""}${seconds} sec`;
269
+ }
270
+
271
+ /**
272
+ *
273
+ * @param okResults {UrlCheckResponse[]}
274
+ * @param time {number}
275
+ */
276
+ function logStatistics(okResults, time) {
277
+ const avgTime = Math.round(okResults.reduce((prev, curr) => prev + curr.time, 0) / TESTED_URLS.length);
278
+ const maxTime = okResults.reduce((prev, curr) => (curr.time > prev.time ? curr : prev));
279
+ const minTime = okResults.reduce((prev, curr) => (curr.time < prev.time ? curr : prev));
61
280
 
62
- stdout.write(`${i + 1} / ${urls.length} ${url}${changedUrl ? ` => ${changedUrl}` : ""}`);
281
+ stdout.write("\nSummary:\n");
282
+ stdout.write(createTabSpace() + `Time ${convertTime(time)}\n`);
283
+ stdout.write(
284
+ createTabSpace() + "Images tested:" + createTabSpace() + TESTED_URLS.filter(url => url.isImg).length + "\n",
285
+ );
286
+ stdout.write(
287
+ createTabSpace() + "Links tested:" + createTabSpace() + TESTED_URLS.filter(url => !url.isImg).length + "\n",
288
+ );
289
+ stdout.write(createTabSpace() + "Avg time:" + createTabSpace() + avgTime + "ms\n");
290
+ stdout.write(
291
+ createTabSpace() + "Min time:" + createTabSpace() + minTime.time + "ms" + createTabSpace() + minTime.url + "\n",
292
+ );
293
+ stdout.write(
294
+ createTabSpace() + "Max time" + createTabSpace() + maxTime.time + "ms" + createTabSpace() + maxTime.url + "\n",
295
+ );
296
+ }
63
297
 
64
- const result = await tryUrl(changedUrl || url);
65
- results.push(result);
66
- const { ttl, status, time } = result;
298
+ /**
299
+ *
300
+ * @param errorText {string}
301
+ * @param slackChannel {string}
302
+ * @return {Promise<void>}
303
+ */
304
+ async function sendSlackMessage(errorText, slackChannel) {
305
+ await Slack.chatPostMessage(slackChannel, {
306
+ text: ":warning: Odkazy uvedené v sitemap.xml nejsou dostupné",
307
+ attachments: [
308
+ {
309
+ text: errorText,
310
+ },
311
+ ],
312
+ });
313
+ }
67
314
 
68
- stdout.write(` ${status} (${time}ms) ttl=${ttl} ${status === 200 ? "✅ " : "❌ "}\n`);
315
+ /**
316
+ *
317
+ * @param sitemapUrl {string}
318
+ * @param webUrl {string}
319
+ * @param slackChannel {string}
320
+ * @param skip {number}
321
+ * @param testNested {boolean}
322
+ * @return {Promise<*>}
323
+ */
324
+ module.exports = async function run(sitemapUrl, webUrl, slackChannel, skip, testNested) {
325
+ if (!sitemapUrl) {
326
+ stdout.write("⛔ Required parameter --url is empty.\n");
327
+ return process.exit(1);
69
328
  }
70
329
 
71
- const avgTime = Math.round(results.reduce((prev, curr) => prev + curr.time, 0) / results.length);
72
- const maxTime = Math.round(results.reduce((prev, curr) => (curr.time > prev ? curr.time : prev), 0));
73
- const minTime = Math.round(results.reduce((prev, curr) => (curr.time < prev ? curr.time : prev), Number.MAX_VALUE));
74
- const resultErrors = results.filter(r => r.status !== 200);
330
+ if (slackChannel && !env.SLACK_TOKEN) {
331
+ stdout.write("⛔ Environment variable SLACK_TOKEN is empty.\n");
332
+ return process.exit(1);
333
+ }
75
334
 
76
- if (resultErrors.length > 0) {
77
- stdout.write("\nErrors:\n");
78
- stdout.write(resultErrors.map(re => ` ${re.url} ${re.status}`)).join("\n");
79
- stdout.write("\n");
335
+ if (webUrl) {
336
+ stdout.write(`${createTabSpace()}Sitemap url: ${sitemapUrl}\n`);
337
+ stdout.write(`❗${createTabSpace()}Web url is defined: ${webUrl}\n\n`);
80
338
  }
81
339
 
82
- stdout.write("\n");
83
- stdout.write("Summary:\n");
84
- stdout.write(` Average time ${avgTime}ms\n`);
85
- stdout.write(` Min time ${minTime}ms\n`);
86
- stdout.write(` Max time ${maxTime}ms\n\n`);
87
-
88
- if (resultErrors.length > 0) {
89
- await Slack.chatPostMessage(slackChannel, {
90
- text: ":warning: Odkazy uvedené v sitemap.xml nejsou dostupné",
91
- attachments: [
92
- {
93
- text: resultErrors.map(r => `${r.url} ${r.status}`).join("\n"),
94
- },
95
- ],
96
- });
340
+ const startTime = performance.now();
341
+ await testSitemapUrls(await Sitemap.getSitemap(sitemapUrl), webUrl, sitemapUrl, skip, testNested);
342
+ const finishTime = performance.now();
343
+
344
+ const errors = TESTED_URLS.filter(r => r.status !== 200);
345
+ const ok = TESTED_URLS.filter(r => r.status === 200);
346
+
347
+ if (errors.length > 0) {
348
+ const errorText = createErrorResult(errors);
349
+ logErrors(errorText);
350
+ await sendSlackMessage(errorText, slackChannel);
97
351
  }
352
+ logStatistics(ok, Math.ceil(finishTime - startTime));
98
353
 
99
- process.exit(resultErrors.length > 0 ? 1 : 0);
354
+ process.exit(errors.length > 0 ? 1 : 0);
100
355
  };