@uxf/scripts 1.4.2 → 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/.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.4.2",
3
+ "version": "1.5.2",
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");
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 = {
@@ -39,12 +39,23 @@ Environment variables:
39
39
  type: "string",
40
40
  group: "Options",
41
41
  })
42
+ .option("skip", {
43
+ describe: "Number of skipped urls.",
44
+ type: "string",
45
+ group: "Options",
46
+ })
47
+ .option("test-nested", {
48
+ describe: "If nested urls should be tested.",
49
+ type: "boolean",
50
+ group: "Options",
51
+ })
42
52
  .option("h", { alias: "help", group: "Options" })
43
53
  .strict(false)
44
54
  .exitProcess(false);
45
55
 
46
56
  try {
47
57
  const { help, url, ...options } = cli.parse(argv.slice(2));
58
+ const skip = options.skip ? Number.parseInt(options.skip) : null;
48
59
  const webUrl = options["web-url"] || null;
49
60
 
50
61
  if (Boolean(help)) {
@@ -56,7 +67,7 @@ Environment variables:
56
67
  env.HTTP_PASSWORD = options["http-password"];
57
68
  }
58
69
 
59
- await require("./index")(url, webUrl, options["slack-channel"]);
70
+ await require("./index")(url, webUrl, options["slack-channel"], skip, options["test-nested"]);
60
71
  } catch (e) {
61
72
  console.error(e);
62
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
+ }
7
82
 
8
- async function tryUrl(url, ttl = 1) {
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
+ }
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,7 +142,183 @@ async function tryUrl(url, ttl = 1) {
35
142
  }
36
143
  }
37
144
 
38
- module.exports = async function run(sitemapUrl, webUrl, slackChannel) {
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;
157
+ }
158
+ return TESTED_URLS[indexInChecked];
159
+ }
160
+
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
+ }
181
+ }
182
+ }
183
+
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
+ printUrlInfo(urls[i], i, urls.length, `${createTabSpace(2)}(${parentIndex + 1}) `);
223
+ const result = await testUrl(urls[i], parentUrl);
224
+ printUrlResult(result);
225
+ }
226
+ }
227
+
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
+ }
238
+
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));
277
+
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
+ }
294
+
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
+ }
311
+
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) {
39
322
  if (!sitemapUrl) {
40
323
  stdout.write("⛔ Required parameter --url is empty.\n");
41
324
  return process.exit(1);
@@ -47,48 +330,23 @@ module.exports = async function run(sitemapUrl, webUrl, slackChannel) {
47
330
  }
48
331
 
49
332
  if (webUrl) {
50
- stdout.write(` Sitemap url: ${sitemapUrl}\n`);
51
- stdout.write(`❗ Web url is defined: ${webUrl}\n\n`);
333
+ stdout.write(`${createTabSpace()}Sitemap url: ${sitemapUrl}\n`);
334
+ stdout.write(`❗${createTabSpace()}Web url is defined: ${webUrl}\n\n`);
52
335
  }
53
336
 
54
- const urls = await Sitemap.getSitemap(sitemapUrl);
55
-
56
- const results = [];
57
-
58
- let i = 0;
59
- for (const url of urls) {
60
- const changedUrl = webUrl ? `${webUrl}${new URL(url).pathname}` : null;
337
+ const startTime = performance.now();
338
+ await testSitemapUrls(await Sitemap.getSitemap(sitemapUrl), webUrl, sitemapUrl, skip, testNested);
339
+ const finishTime = performance.now();
61
340
 
62
- stdout.write(`${++i} / ${urls.length} ${url}${changedUrl ? ` => ${changedUrl}` : ""}`);
63
-
64
- const result = await tryUrl(changedUrl || url);
65
- results.push(result);
66
- const { ttl, status, time } = result;
67
-
68
- stdout.write(` ${status} (${time}ms) ttl=${ttl} ${status === 200 ? "✅ " : "❌ "}\n`);
69
- }
341
+ const errors = TESTED_URLS.filter(r => r.status !== 200);
342
+ const ok = TESTED_URLS.filter(r => r.status === 200);
70
343
 
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
- stdout.write("\n");
75
- stdout.write("Summary:\n");
76
- stdout.write(` Average time ${avgTime}ms\n`);
77
- stdout.write(` Min time ${minTime}ms\n`);
78
- stdout.write(` Max time ${maxTime}ms\n\n`);
79
-
80
- const resultErrors = results.filter(r => r.status !== 200);
81
-
82
- if (resultErrors.length > 0) {
83
- await Slack.chatPostMessage(slackChannel, {
84
- text: ":warning: Odkazy uvedené v sitemap.xml nejsou dostupné",
85
- attachments: [
86
- {
87
- text: resultErrors.map(r => `${r.url} ${r.status}`).join("\n"),
88
- },
89
- ],
90
- });
344
+ if (errors.length > 0) {
345
+ const errorText = createErrorResult(errors);
346
+ logErrors(errorText);
347
+ await sendSlackMessage(errorText, slackChannel);
91
348
  }
349
+ logStatistics(ok, Math.ceil(finishTime - startTime));
92
350
 
93
- process.exit(resultErrors.length > 0 ? 1 : 0);
351
+ process.exit(errors.length > 0 ? 1 : 0);
94
352
  };