@uxf/scripts 1.5.1 → 1.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uxf/scripts",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
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(4 * 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
+ }
85
+
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
+ }
7
101
 
8
- async function tryUrl(url, ttl = 1) {
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,211 @@ 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;
52
218
  }
53
219
 
54
- const urls = await Sitemap.getSitemap(sitemapUrl);
220
+ stdout.write(label + "\n");
221
+ for (let i = 0; i < urls.length; i++) {
222
+ printUrlInfo(urls[i], i, urls.length, `${createTabSpace(2)}(${parentIndex + 1}) `);
223
+ const result = await testUrl(urls[i], parentUrl);
224
+ printUrlResult(result);
225
+ }
226
+ }
55
227
 
56
- const results = [];
228
+ /**
229
+ *
230
+ * @param url {string}
231
+ * @param urlIndex {number}
232
+ * @param allUrlsCount {number}
233
+ * @param prefix {string}
234
+ */
235
+ function printUrlInfo(url, urlIndex, allUrlsCount, prefix = "") {
236
+ stdout.write(`${prefix}${urlIndex + 1} / ${allUrlsCount}${createTabSpace()}${url}`);
237
+ }
57
238
 
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;
239
+ /**
240
+ *
241
+ * @param result {UrlCheckResponse}
242
+ */
243
+ function printUrlResult(result) {
244
+ const { ttl, status, time } = result;
245
+ stdout.write(`${createTabSpace()}${status} (${time}ms) ttl=${ttl} ${status === 200 ? "✅ " : "❌ "}\n`);
246
+ }
247
+
248
+ /**
249
+ *
250
+ * @param errorText {string}
251
+ */
252
+ function logErrors(errorText) {
253
+ stdout.write("\nErrors:\n");
254
+ stdout.write(`${errorText}\n\n`);
255
+ }
256
+
257
+ /**
258
+ *
259
+ * @param millis {number}
260
+ * @return {string}
261
+ */
262
+ function convertTime(millis) {
263
+ let minutes = Math.floor(millis / 60000);
264
+ let seconds = ((millis % 60000) / 1000).toFixed(0);
265
+ return `${minutes} min ${seconds < 10 ? "0" : ""}${seconds} sec`;
266
+ }
267
+
268
+ /**
269
+ *
270
+ * @param okResults {UrlCheckResponse[]}
271
+ * @param time {number}
272
+ */
273
+ function logStatistics(okResults, time) {
274
+ const avgTime = Math.round(okResults.reduce((prev, curr) => prev + curr.time, 0) / TESTED_URLS.length);
275
+ const maxTime = okResults.reduce((prev, curr) => (curr.time > prev.time ? curr : prev));
276
+ const minTime = okResults.reduce((prev, curr) => (curr.time < prev.time ? curr : prev));
61
277
 
62
- stdout.write(`${i + 1} / ${urls.length} ${url}${changedUrl ? ` => ${changedUrl}` : ""}`);
278
+ stdout.write("\nSummary:\n");
279
+ stdout.write(createTabSpace() + `Time ${convertTime(time)}\n`);
280
+ stdout.write(
281
+ createTabSpace() + "Images tested:" + createTabSpace() + TESTED_URLS.filter(url => url.isImg).length + "\n",
282
+ );
283
+ stdout.write(
284
+ createTabSpace() + "Links tested:" + createTabSpace() + TESTED_URLS.filter(url => !url.isImg).length + "\n",
285
+ );
286
+ stdout.write(createTabSpace() + "Avg time:" + createTabSpace() + avgTime + "ms\n");
287
+ stdout.write(
288
+ createTabSpace() + "Min time:" + createTabSpace() + minTime.time + "ms" + createTabSpace() + minTime.url + "\n",
289
+ );
290
+ stdout.write(
291
+ createTabSpace() + "Max time" + createTabSpace() + maxTime.time + "ms" + createTabSpace() + maxTime.url + "\n",
292
+ );
293
+ }
63
294
 
64
- const result = await tryUrl(changedUrl || url);
65
- results.push(result);
66
- const { ttl, status, time } = result;
295
+ /**
296
+ *
297
+ * @param errorText {string}
298
+ * @param slackChannel {string}
299
+ * @return {Promise<void>}
300
+ */
301
+ async function sendSlackMessage(errorText, slackChannel) {
302
+ await Slack.chatPostMessage(slackChannel, {
303
+ text: ":warning: Odkazy uvedené v sitemap.xml nejsou dostupné",
304
+ attachments: [
305
+ {
306
+ text: errorText,
307
+ },
308
+ ],
309
+ });
310
+ }
67
311
 
68
- stdout.write(` ${status} (${time}ms) ttl=${ttl} ${status === 200 ? "✅ " : "❌ "}\n`);
312
+ /**
313
+ *
314
+ * @param sitemapUrl {string}
315
+ * @param webUrl {string}
316
+ * @param slackChannel {string}
317
+ * @param skip {number}
318
+ * @param testNested {boolean}
319
+ * @return {Promise<*>}
320
+ */
321
+ module.exports = async function run(sitemapUrl, webUrl, slackChannel, skip, testNested) {
322
+ if (!sitemapUrl) {
323
+ stdout.write("⛔ Required parameter --url is empty.\n");
324
+ return process.exit(1);
69
325
  }
70
326
 
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);
327
+ if (slackChannel && !env.SLACK_TOKEN) {
328
+ stdout.write("⛔ Environment variable SLACK_TOKEN is empty.\n");
329
+ return process.exit(1);
330
+ }
75
331
 
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");
332
+ if (webUrl) {
333
+ stdout.write(`${createTabSpace()}Sitemap url: ${sitemapUrl}\n`);
334
+ stdout.write(`❗${createTabSpace()}Web url is defined: ${webUrl}\n\n`);
80
335
  }
81
336
 
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
- });
337
+ const startTime = performance.now();
338
+ await testSitemapUrls(await Sitemap.getSitemap(sitemapUrl), webUrl, sitemapUrl, skip, testNested);
339
+ const finishTime = performance.now();
340
+
341
+ const errors = TESTED_URLS.filter(r => r.status !== 200);
342
+ const ok = TESTED_URLS.filter(r => r.status === 200);
343
+
344
+ if (errors.length > 0) {
345
+ const errorText = createErrorResult(errors);
346
+ logErrors(errorText);
347
+ await sendSlackMessage(errorText, slackChannel);
97
348
  }
349
+ logStatistics(ok, Math.ceil(finishTime - startTime));
98
350
 
99
- process.exit(resultErrors.length > 0 ? 1 : 0);
351
+ process.exit(errors.length > 0 ? 1 : 0);
100
352
  };