@learnpack/learnpack 5.0.6 → 5.0.8

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 (71) hide show
  1. package/README.md +17 -17
  2. package/bin/run +17 -17
  3. package/bin/run.cmd +3 -3
  4. package/lib/commands/audit.js +30 -21
  5. package/lib/commands/clean.js +3 -3
  6. package/lib/commands/download.js +3 -3
  7. package/lib/commands/init.js +29 -4
  8. package/lib/commands/login.js +3 -3
  9. package/lib/commands/logout.js +3 -3
  10. package/lib/utils/rigoActions.d.ts +5 -0
  11. package/lib/utils/rigoActions.js +15 -1
  12. package/oclif.manifest.json +1 -1
  13. package/package.json +152 -152
  14. package/src/commands/audit.ts +449 -443
  15. package/src/commands/clean.ts +29 -29
  16. package/src/commands/download.ts +61 -61
  17. package/src/commands/init.ts +40 -12
  18. package/src/commands/login.ts +42 -42
  19. package/src/commands/logout.ts +43 -43
  20. package/src/commands/test.ts +85 -85
  21. package/src/index.ts +1 -1
  22. package/src/managers/config/allowed_files.ts +29 -29
  23. package/src/managers/gitpod.ts +84 -84
  24. package/src/managers/server/index.ts +78 -78
  25. package/src/managers/telemetry.ts +353 -353
  26. package/src/managers/test.ts +83 -83
  27. package/src/models/audit.ts +16 -16
  28. package/src/models/config-manager.ts +23 -23
  29. package/src/models/counter.ts +11 -11
  30. package/src/models/errors.ts +22 -22
  31. package/src/models/exercise-obj.ts +29 -29
  32. package/src/models/file.ts +5 -5
  33. package/src/models/findings.ts +18 -18
  34. package/src/models/flags.ts +10 -10
  35. package/src/models/front-matter.ts +11 -11
  36. package/src/models/gitpod-data.ts +19 -19
  37. package/src/models/language.ts +4 -4
  38. package/src/models/package.ts +7 -7
  39. package/src/models/plugin-config.ts +17 -17
  40. package/src/models/success-types.ts +1 -1
  41. package/src/plugin/command/compile.ts +17 -17
  42. package/src/plugin/command/test.ts +30 -30
  43. package/src/plugin/index.ts +6 -6
  44. package/src/plugin/plugin.ts +94 -94
  45. package/src/plugin/utils.ts +87 -87
  46. package/src/types/node-fetch.d.ts +1 -1
  47. package/src/ui/download.ts +71 -71
  48. package/src/utils/BaseCommand.ts +48 -48
  49. package/src/utils/SessionCommand.ts +43 -43
  50. package/src/utils/audit.ts +393 -393
  51. package/src/utils/errors.ts +117 -117
  52. package/src/utils/exercisesQueue.ts +51 -51
  53. package/src/utils/fileQueue.ts +199 -199
  54. package/src/utils/misc.ts +23 -23
  55. package/src/utils/osOperations.ts +79 -79
  56. package/src/utils/rigoActions.ts +27 -0
  57. package/src/utils/templates/gitignore.txt +19 -19
  58. package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.es.md +24 -24
  59. package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.md +24 -24
  60. package/src/utils/templates/incremental/.vscode/schema.json +121 -121
  61. package/src/utils/templates/incremental/.vscode/settings.json +13 -13
  62. package/src/utils/templates/incremental/README.ejs +4 -4
  63. package/src/utils/templates/incremental/README.es.ejs +4 -4
  64. package/src/utils/templates/isolated/.vscode/schema.json +121 -121
  65. package/src/utils/templates/isolated/.vscode/settings.json +13 -13
  66. package/src/utils/templates/isolated/README.ejs +4 -4
  67. package/src/utils/templates/isolated/README.es.ejs +4 -4
  68. package/src/utils/templates/no-grading/README.ejs +4 -4
  69. package/src/utils/templates/no-grading/README.es.ejs +4 -4
  70. package/src/utils/validators.ts +18 -18
  71. package/src/utils/watcher.ts +27 -27
@@ -1,393 +1,393 @@
1
- import { IAuditErrors } from "../models/audit"
2
- import { IConfigObj } from "../models/config"
3
- import { ICounter } from "../models/counter"
4
- import { IFindings } from "../models/findings"
5
- import { IExercise } from "../models/exercise-obj"
6
- import { IFrontmatter } from "../models/front-matter"
7
- import Console from "./console"
8
- import * as fs from "fs"
9
- import * as path from "path"
10
-
11
- // eslint-disable-next-line
12
- const fetch = require("node-fetch");
13
- // eslint-disable-next-line
14
- const fm = require("front-matter");
15
-
16
- // This function checks if a url is valid.
17
- const isUrl = async (
18
- url: string,
19
- errors: IAuditErrors[],
20
- counter: ICounter
21
- ) => {
22
- const regexUrl = /(https?:\/\/[\w./-]+)/gm
23
- counter.links.total++
24
- if (!regexUrl.test(url)) {
25
- counter.links.error++
26
- errors.push({
27
- exercise: undefined,
28
- msg: `The repository value of the configuration file is not a link: ${url}`,
29
- })
30
- return false
31
- }
32
-
33
- const res = await fetch(url, { method: "HEAD" })
34
- if (!res.ok) {
35
- counter.links.error++
36
- errors.push({
37
- exercise: undefined,
38
- msg: `The link of the repository is broken: ${url}`,
39
- })
40
- }
41
-
42
- return true
43
- }
44
-
45
- const checkForEmptySpaces = (str: string) => {
46
- const isEmpty = true
47
- for (const letter of str) {
48
- if (letter !== " ") {
49
- return false
50
- }
51
- }
52
-
53
- return isEmpty
54
- }
55
-
56
- const checkLearnpackClean = (configObj: IConfigObj, errors: IAuditErrors[]) => {
57
- if (
58
- (configObj.config?.outputPath &&
59
- fs.existsSync(configObj.config?.outputPath)) ||
60
- fs.existsSync(`${configObj.config?.dirPath}/_app`) ||
61
- fs.existsSync(`${configObj.config?.dirPath}/reports`) ||
62
- fs.existsSync(`${configObj.config?.dirPath}/resets`) ||
63
- fs.existsSync(`${configObj.config?.dirPath}/app.tar.gz`) ||
64
- fs.existsSync(`${configObj.config?.dirPath}/config.json`) ||
65
- fs.existsSync(`${configObj.config?.dirPath}/vscode_queue.json`) ||
66
- fs.existsSync(`${configObj.config?.dirPath}/telemetry.json`)
67
- ) {
68
- errors.push({
69
- exercise: undefined,
70
- msg: "You have to run learnpack clean command",
71
- })
72
- }
73
- }
74
-
75
- const findInFile = (types: string[], content: string) => {
76
- const regex: any = {
77
- relativeImages:
78
- /!\[.*]\s*\((((\.\/)?(\.{2}\/){1,5})(.*\/)*(.[^\s/]*\.[A-Za-z]{2,4})\S*)\)/gm,
79
- externalImages: /!\[.*]\((https?:\/(\/[^)/]+)+\/?)\)/gm,
80
- markdownLinks: /(\s)+\[.*]\((https?:\/(\/[^)/]+)+\/?)\)/gm,
81
- url: /(https?:\/\/[\w./-]+)/gm,
82
- uploadcare: /https:\/\/ucarecdn.com\/(?:.*\/)*([\w./-]+)/gm,
83
- }
84
-
85
- const validTypes = Object.keys(regex)
86
- if (!Array.isArray(types))
87
- types = [types]
88
-
89
- const findings: IFindings = {}
90
- type findingsType =
91
- | "relativeImages"
92
- | "externalImages"
93
- | "markdownLinks"
94
- | "url"
95
- | "uploadcare";
96
-
97
- for (const type of types) {
98
- if (!validTypes.includes(type))
99
- throw new Error("Invalid type: " + type)
100
- else
101
- findings[type as findingsType] = {}
102
- }
103
-
104
- for (const type of types) {
105
- let m: RegExpExecArray
106
- while ((m = regex[type].exec(content)) !== null) {
107
- // This is necessary to avoid infinite loops with zero-width matches
108
- if (m.index === regex.lastIndex) {
109
- regex.lastIndex++
110
- }
111
-
112
- // The result can be accessed through the `m`-variable.
113
- // m.forEach((match, groupIndex) => values.push(match));
114
-
115
- findings[type as findingsType]![m[0]] = {
116
- content: m[0],
117
- absUrl: m[1],
118
- mdUrl: m[2],
119
- relUrl: m[6],
120
- }
121
- }
122
- }
123
-
124
- return findings
125
- }
126
-
127
- // This function checks that each of the url's are working.
128
- const checkUrl = async (
129
- config: IConfigObj,
130
- filePath: string,
131
- fileName: string,
132
- exercise: IExercise | undefined,
133
- errors: IAuditErrors[],
134
- warnings: IAuditErrors[],
135
- counter: ICounter | undefined
136
- ) => {
137
- if (!fs.existsSync(filePath))
138
- return false
139
- const content: string = fs.readFileSync(filePath).toString()
140
- const isEmpty = checkForEmptySpaces(content)
141
- if (isEmpty || !content)
142
- errors.push({
143
- exercise: exercise?.title!,
144
- msg: `This file (${fileName}) doesn't have any content inside.`,
145
- })
146
-
147
- const frontmatter: IFrontmatter = fm(content)
148
- for (const attribute in frontmatter.attributes) {
149
- if (
150
- Object.prototype.hasOwnProperty.call(frontmatter.attributes, attribute) &&
151
- (attribute === "intro" || attribute === "tutorial")
152
- ) {
153
- counter && counter.links.total++
154
- try {
155
- // eslint-disable-next-line
156
- let res = await fetch(frontmatter.attributes[attribute], {
157
- method: "HEAD",
158
- })
159
- if (!res.ok) {
160
- counter && counter.links.error++
161
- errors.push({
162
- exercise: exercise?.title!,
163
- msg: `This link is broken (${res.ok}): ${frontmatter.attributes[attribute]}`,
164
- })
165
- }
166
- } catch {
167
- counter && counter.links.error++
168
- errors.push({
169
- exercise: exercise?.title,
170
- msg: `This link is broken: ${frontmatter.attributes[attribute]}`,
171
- })
172
- }
173
- }
174
- }
175
-
176
- // Check url's of each README file.
177
- const findings: IFindings = findInFile(
178
- ["relativeImages", "externalImages", "markdownLinks"],
179
- content
180
- )
181
- type findingsType =
182
- | "relativeImages"
183
- | "externalImages"
184
- | "markdownLinks"
185
- | "url"
186
- | "uploadcare";
187
- for (const finding in findings) {
188
- if (Object.prototype.hasOwnProperty.call(findings, finding)) {
189
- const obj = findings[finding as findingsType]
190
- // Valdites all the relative path images.
191
- if (finding === "relativeImages" && Object.keys(obj!).length > 0) {
192
- for (const img in obj) {
193
- if (Object.prototype.hasOwnProperty.call(obj, img)) {
194
- // Validates if the image is in the assets folder.
195
- counter && counter.images.total++
196
- const relativePath = path
197
- .relative(
198
- exercise ? exercise.path.replace(/\\/gm, "/") : "./",
199
- `${config!.config?.dirPath}/assets/${obj[img].relUrl}`
200
- )
201
- .replace(/\\/gm, "/")
202
- if (relativePath !== obj[img].absUrl.split("?").shift()) {
203
- counter && counter.images.error++
204
- errors.push({
205
- exercise: exercise?.title,
206
- msg: `This relative path (${obj[img].relUrl}) is not pointing to the assets folder.`,
207
- })
208
- }
209
-
210
- if (
211
- !fs.existsSync(
212
- `${config!.config?.dirPath}/assets/${obj[img].relUrl}`
213
- )
214
- ) {
215
- counter && counter.images.error++
216
- errors.push({
217
- exercise: exercise?.title,
218
- msg: `The file ${obj[img].relUrl} doesn't exist in the assets folder.`,
219
- })
220
- }
221
- }
222
- }
223
- } else if (finding === "externalImages" && Object.keys(obj!).length > 0) {
224
- // Valdites all the aboslute path images.
225
- for (const img in obj) {
226
- if (Object.prototype.hasOwnProperty.call(obj, img)) {
227
- counter && counter.images.total++
228
- if (
229
- fs.existsSync(
230
- `${config!.config?.dirPath}/assets${obj[img].mdUrl
231
- .split("?")
232
- .shift()}`
233
- )
234
- ) {
235
- const relativePath = path
236
- .relative(
237
- exercise ? exercise.path.replace(/\\/gm, "/") : "./",
238
- `${config!.config?.dirPath}/assets/${obj[img].mdUrl}`
239
- )
240
- .replace(/\\/gm, "/")
241
- warnings.push({
242
- exercise: exercise?.title,
243
- msg: `On this exercise you have an image with an absolute path "${obj[img].absUrl}". We recommend you to replace it by the relative path: "${relativePath}".`,
244
- })
245
- }
246
-
247
- try {
248
- // eslint-disable-next-line
249
- let res = await fetch(obj[img].absUrl, {
250
- method: "HEAD",
251
- })
252
- if (!res.ok) {
253
- counter && counter.images.error++
254
- errors.push({
255
- exercise: exercise?.title,
256
- msg: `This link is broken: ${obj[img].absUrl}`,
257
- })
258
- }
259
- } catch {
260
- counter && counter.images.error++
261
- errors.push({
262
- exercise: exercise?.title,
263
- msg: `This link is broken: ${obj[img].absUrl}`,
264
- })
265
- }
266
- }
267
- }
268
- } else if (finding === "markdownLinks" && Object.keys(obj!).length > 0) {
269
- for (const link in obj) {
270
- if (Object.prototype.hasOwnProperty.call(obj, link)) {
271
- counter && counter.links.total++
272
- if (!obj[link].mdUrl.includes("twitter")) {
273
- try {
274
- // eslint-disable-next-line
275
- let res = await fetch(obj[link].mdUrl, {
276
- method: "HEAD",
277
- })
278
- if (res.status > 399 && res.status < 500) {
279
- counter && counter.links.error++
280
- errors.push({
281
- exercise: exercise?.title,
282
- msg: `This link is broken: ${obj[link].mdUrl}`,
283
- })
284
- }
285
- } catch {
286
- counter && counter.links.error++
287
- errors.push({
288
- exercise: exercise?.title,
289
- msg: `This link is broken: ${obj[link].mdUrl}`,
290
- })
291
- }
292
- }
293
- }
294
- }
295
- }
296
- }
297
- }
298
-
299
- return true
300
- }
301
-
302
- // This function writes a given file with the given content.
303
- const writeFile = async (content: string, filePath: string) => {
304
- try {
305
- await fs.promises.writeFile(filePath, content)
306
- } catch (error) {
307
- if (error)
308
- Console.error(
309
- `We weren't able to write the file in this path "${filePath}".`,
310
- error
311
- )
312
- }
313
- }
314
-
315
- // This function checks if there are errors, and show them in the console at the end.
316
- const showErrors = (errors: IAuditErrors[], counter: ICounter | undefined) => {
317
- return new Promise((resolve, reject) => {
318
- if (errors) {
319
- if (errors.length > 0) {
320
- Console.log("Checking for errors...")
321
- for (const [i, error] of errors.entries())
322
- Console.error(
323
- `${i + 1}) ${error.msg} ${
324
- error.exercise ? `(Exercise: ${error.exercise})` : ""
325
- }`
326
- )
327
- if (counter) {
328
- Console.error(
329
- ` We found ${errors.length} error${
330
- errors.length > 1 ? "s" : ""
331
- } among ${counter.images.total} images, ${
332
- counter.links.total
333
- } link, ${counter.readmeFiles} README files and ${
334
- counter.exercises
335
- } exercises.`
336
- )
337
- } else {
338
- Console.error(
339
- ` We found ${errors.length} error${
340
- errors.length > 1 ? "s" : ""
341
- } related with the project integrity.`
342
- )
343
- }
344
-
345
- process.exit(1)
346
- } else {
347
- if (counter) {
348
- Console.success(
349
- `We didn't find any errors in this repository among ${counter.images.total} images, ${counter.links.total} link, ${counter.readmeFiles} README files and ${counter.exercises} exercises.`
350
- )
351
- } else {
352
- Console.success(`We didn't find any errors in this repository.`)
353
- }
354
-
355
- process.exit(0)
356
- }
357
- } else {
358
- reject("Failed")
359
- }
360
- })
361
- }
362
-
363
- // This function checks if there are warnings, and show them in the console at the end.
364
- const showWarnings = (warnings: IAuditErrors[]) => {
365
- return new Promise((resolve, reject) => {
366
- if (warnings) {
367
- if (warnings.length > 0) {
368
- Console.log("Checking for warnings...")
369
- for (const [i, warning] of warnings.entries())
370
- Console.warning(
371
- `${i + 1}) ${warning.msg} ${
372
- warning.exercise ? `File: ${warning.exercise}` : ""
373
- }`
374
- )
375
- }
376
-
377
- resolve("SUCCESS")
378
- } else {
379
- reject("Failed")
380
- }
381
- })
382
- }
383
-
384
- export default {
385
- isUrl,
386
- checkForEmptySpaces,
387
- checkLearnpackClean,
388
- findInFile,
389
- checkUrl,
390
- writeFile,
391
- showErrors,
392
- showWarnings,
393
- }
1
+ import { IAuditErrors } from "../models/audit"
2
+ import { IConfigObj } from "../models/config"
3
+ import { ICounter } from "../models/counter"
4
+ import { IFindings } from "../models/findings"
5
+ import { IExercise } from "../models/exercise-obj"
6
+ import { IFrontmatter } from "../models/front-matter"
7
+ import Console from "./console"
8
+ import * as fs from "fs"
9
+ import * as path from "path"
10
+
11
+ // eslint-disable-next-line
12
+ const fetch = require("node-fetch");
13
+ // eslint-disable-next-line
14
+ const fm = require("front-matter");
15
+
16
+ // This function checks if a url is valid.
17
+ const isUrl = async (
18
+ url: string,
19
+ errors: IAuditErrors[],
20
+ counter: ICounter
21
+ ) => {
22
+ const regexUrl = /(https?:\/\/[\w./-]+)/gm
23
+ counter.links.total++
24
+ if (!regexUrl.test(url)) {
25
+ counter.links.error++
26
+ errors.push({
27
+ exercise: undefined,
28
+ msg: `The repository value of the configuration file is not a link: ${url}`,
29
+ })
30
+ return false
31
+ }
32
+
33
+ const res = await fetch(url, { method: "HEAD" })
34
+ if (!res.ok) {
35
+ counter.links.error++
36
+ errors.push({
37
+ exercise: undefined,
38
+ msg: `The link of the repository is broken: ${url}`,
39
+ })
40
+ }
41
+
42
+ return true
43
+ }
44
+
45
+ const checkForEmptySpaces = (str: string) => {
46
+ const isEmpty = true
47
+ for (const letter of str) {
48
+ if (letter !== " ") {
49
+ return false
50
+ }
51
+ }
52
+
53
+ return isEmpty
54
+ }
55
+
56
+ const checkLearnpackClean = (configObj: IConfigObj, errors: IAuditErrors[]) => {
57
+ if (
58
+ (configObj.config?.outputPath &&
59
+ fs.existsSync(configObj.config?.outputPath)) ||
60
+ fs.existsSync(`${configObj.config?.dirPath}/_app`) ||
61
+ fs.existsSync(`${configObj.config?.dirPath}/reports`) ||
62
+ fs.existsSync(`${configObj.config?.dirPath}/resets`) ||
63
+ fs.existsSync(`${configObj.config?.dirPath}/app.tar.gz`) ||
64
+ fs.existsSync(`${configObj.config?.dirPath}/config.json`) ||
65
+ fs.existsSync(`${configObj.config?.dirPath}/vscode_queue.json`) ||
66
+ fs.existsSync(`${configObj.config?.dirPath}/telemetry.json`)
67
+ ) {
68
+ errors.push({
69
+ exercise: undefined,
70
+ msg: "You have to run learnpack clean command",
71
+ })
72
+ }
73
+ }
74
+
75
+ const findInFile = (types: string[], content: string) => {
76
+ const regex: any = {
77
+ relativeImages:
78
+ /!\[.*]\s*\((((\.\/)?(\.{2}\/){1,5})(.*\/)*(.[^\s/]*\.[A-Za-z]{2,4})\S*)\)/gm,
79
+ externalImages: /!\[.*]\((https?:\/(\/[^)/]+)+\/?)\)/gm,
80
+ markdownLinks: /(\s)+\[.*]\((https?:\/(\/[^)/]+)+\/?)\)/gm,
81
+ url: /(https?:\/\/[\w./-]+)/gm,
82
+ uploadcare: /https:\/\/ucarecdn.com\/(?:.*\/)*([\w./-]+)/gm,
83
+ }
84
+
85
+ const validTypes = Object.keys(regex)
86
+ if (!Array.isArray(types))
87
+ types = [types]
88
+
89
+ const findings: IFindings = {}
90
+ type findingsType =
91
+ | "relativeImages"
92
+ | "externalImages"
93
+ | "markdownLinks"
94
+ | "url"
95
+ | "uploadcare";
96
+
97
+ for (const type of types) {
98
+ if (!validTypes.includes(type))
99
+ throw new Error("Invalid type: " + type)
100
+ else
101
+ findings[type as findingsType] = {}
102
+ }
103
+
104
+ for (const type of types) {
105
+ let m: RegExpExecArray
106
+ while ((m = regex[type].exec(content)) !== null) {
107
+ // This is necessary to avoid infinite loops with zero-width matches
108
+ if (m.index === regex.lastIndex) {
109
+ regex.lastIndex++
110
+ }
111
+
112
+ // The result can be accessed through the `m`-variable.
113
+ // m.forEach((match, groupIndex) => values.push(match));
114
+
115
+ findings[type as findingsType]![m[0]] = {
116
+ content: m[0],
117
+ absUrl: m[1],
118
+ mdUrl: m[2],
119
+ relUrl: m[6],
120
+ }
121
+ }
122
+ }
123
+
124
+ return findings
125
+ }
126
+
127
+ // This function checks that each of the url's are working.
128
+ const checkUrl = async (
129
+ config: IConfigObj,
130
+ filePath: string,
131
+ fileName: string,
132
+ exercise: IExercise | undefined,
133
+ errors: IAuditErrors[],
134
+ warnings: IAuditErrors[],
135
+ counter: ICounter | undefined
136
+ ) => {
137
+ if (!fs.existsSync(filePath))
138
+ return false
139
+ const content: string = fs.readFileSync(filePath).toString()
140
+ const isEmpty = checkForEmptySpaces(content)
141
+ if (isEmpty || !content)
142
+ errors.push({
143
+ exercise: exercise?.title!,
144
+ msg: `This file (${fileName}) doesn't have any content inside.`,
145
+ })
146
+
147
+ const frontmatter: IFrontmatter = fm(content)
148
+ for (const attribute in frontmatter.attributes) {
149
+ if (
150
+ Object.prototype.hasOwnProperty.call(frontmatter.attributes, attribute) &&
151
+ (attribute === "intro" || attribute === "tutorial")
152
+ ) {
153
+ counter && counter.links.total++
154
+ try {
155
+ // eslint-disable-next-line
156
+ let res = await fetch(frontmatter.attributes[attribute], {
157
+ method: "HEAD",
158
+ })
159
+ if (!res.ok) {
160
+ counter && counter.links.error++
161
+ errors.push({
162
+ exercise: exercise?.title!,
163
+ msg: `This link is broken (${res.ok}): ${frontmatter.attributes[attribute]}`,
164
+ })
165
+ }
166
+ } catch {
167
+ counter && counter.links.error++
168
+ errors.push({
169
+ exercise: exercise?.title,
170
+ msg: `This link is broken: ${frontmatter.attributes[attribute]}`,
171
+ })
172
+ }
173
+ }
174
+ }
175
+
176
+ // Check url's of each README file.
177
+ const findings: IFindings = findInFile(
178
+ ["relativeImages", "externalImages", "markdownLinks"],
179
+ content
180
+ )
181
+ type findingsType =
182
+ | "relativeImages"
183
+ | "externalImages"
184
+ | "markdownLinks"
185
+ | "url"
186
+ | "uploadcare";
187
+ for (const finding in findings) {
188
+ if (Object.prototype.hasOwnProperty.call(findings, finding)) {
189
+ const obj = findings[finding as findingsType]
190
+ // Valdites all the relative path images.
191
+ if (finding === "relativeImages" && Object.keys(obj!).length > 0) {
192
+ for (const img in obj) {
193
+ if (Object.prototype.hasOwnProperty.call(obj, img)) {
194
+ // Validates if the image is in the assets folder.
195
+ counter && counter.images.total++
196
+ const relativePath = path
197
+ .relative(
198
+ exercise ? exercise.path.replace(/\\/gm, "/") : "./",
199
+ `${config!.config?.dirPath}/assets/${obj[img].relUrl}`
200
+ )
201
+ .replace(/\\/gm, "/")
202
+ if (relativePath !== obj[img].absUrl.split("?").shift()) {
203
+ counter && counter.images.error++
204
+ errors.push({
205
+ exercise: exercise?.title,
206
+ msg: `This relative path (${obj[img].relUrl}) is not pointing to the assets folder.`,
207
+ })
208
+ }
209
+
210
+ if (
211
+ !fs.existsSync(
212
+ `${config!.config?.dirPath}/assets/${obj[img].relUrl}`
213
+ )
214
+ ) {
215
+ counter && counter.images.error++
216
+ errors.push({
217
+ exercise: exercise?.title,
218
+ msg: `The file ${obj[img].relUrl} doesn't exist in the assets folder.`,
219
+ })
220
+ }
221
+ }
222
+ }
223
+ } else if (finding === "externalImages" && Object.keys(obj!).length > 0) {
224
+ // Valdites all the aboslute path images.
225
+ for (const img in obj) {
226
+ if (Object.prototype.hasOwnProperty.call(obj, img)) {
227
+ counter && counter.images.total++
228
+ if (
229
+ fs.existsSync(
230
+ `${config!.config?.dirPath}/assets${obj[img].mdUrl
231
+ .split("?")
232
+ .shift()}`
233
+ )
234
+ ) {
235
+ const relativePath = path
236
+ .relative(
237
+ exercise ? exercise.path.replace(/\\/gm, "/") : "./",
238
+ `${config!.config?.dirPath}/assets/${obj[img].mdUrl}`
239
+ )
240
+ .replace(/\\/gm, "/")
241
+ warnings.push({
242
+ exercise: exercise?.title,
243
+ msg: `On this exercise you have an image with an absolute path "${obj[img].absUrl}". We recommend you to replace it by the relative path: "${relativePath}".`,
244
+ })
245
+ }
246
+
247
+ try {
248
+ // eslint-disable-next-line
249
+ let res = await fetch(obj[img].absUrl, {
250
+ method: "HEAD",
251
+ })
252
+ if (!res.ok) {
253
+ counter && counter.images.error++
254
+ errors.push({
255
+ exercise: exercise?.title,
256
+ msg: `This link is broken: ${obj[img].absUrl}`,
257
+ })
258
+ }
259
+ } catch {
260
+ counter && counter.images.error++
261
+ errors.push({
262
+ exercise: exercise?.title,
263
+ msg: `This link is broken: ${obj[img].absUrl}`,
264
+ })
265
+ }
266
+ }
267
+ }
268
+ } else if (finding === "markdownLinks" && Object.keys(obj!).length > 0) {
269
+ for (const link in obj) {
270
+ if (Object.prototype.hasOwnProperty.call(obj, link)) {
271
+ counter && counter.links.total++
272
+ if (!obj[link].mdUrl.includes("twitter")) {
273
+ try {
274
+ // eslint-disable-next-line
275
+ let res = await fetch(obj[link].mdUrl, {
276
+ method: "HEAD",
277
+ })
278
+ if (res.status > 399 && res.status < 500) {
279
+ counter && counter.links.error++
280
+ errors.push({
281
+ exercise: exercise?.title,
282
+ msg: `This link is broken: ${obj[link].mdUrl}`,
283
+ })
284
+ }
285
+ } catch {
286
+ counter && counter.links.error++
287
+ errors.push({
288
+ exercise: exercise?.title,
289
+ msg: `This link is broken: ${obj[link].mdUrl}`,
290
+ })
291
+ }
292
+ }
293
+ }
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ return true
300
+ }
301
+
302
+ // This function writes a given file with the given content.
303
+ const writeFile = async (content: string, filePath: string) => {
304
+ try {
305
+ await fs.promises.writeFile(filePath, content)
306
+ } catch (error) {
307
+ if (error)
308
+ Console.error(
309
+ `We weren't able to write the file in this path "${filePath}".`,
310
+ error
311
+ )
312
+ }
313
+ }
314
+
315
+ // This function checks if there are errors, and show them in the console at the end.
316
+ const showErrors = (errors: IAuditErrors[], counter: ICounter | undefined) => {
317
+ return new Promise((resolve, reject) => {
318
+ if (errors) {
319
+ if (errors.length > 0) {
320
+ Console.log("Checking for errors...")
321
+ for (const [i, error] of errors.entries())
322
+ Console.error(
323
+ `${i + 1}) ${error.msg} ${
324
+ error.exercise ? `(Exercise: ${error.exercise})` : ""
325
+ }`
326
+ )
327
+ if (counter) {
328
+ Console.error(
329
+ ` We found ${errors.length} error${
330
+ errors.length > 1 ? "s" : ""
331
+ } among ${counter.images.total} images, ${
332
+ counter.links.total
333
+ } link, ${counter.readmeFiles} README files and ${
334
+ counter.exercises
335
+ } exercises.`
336
+ )
337
+ } else {
338
+ Console.error(
339
+ ` We found ${errors.length} error${
340
+ errors.length > 1 ? "s" : ""
341
+ } related with the project integrity.`
342
+ )
343
+ }
344
+
345
+ process.exit(1)
346
+ } else {
347
+ if (counter) {
348
+ Console.success(
349
+ `We didn't find any errors in this repository among ${counter.images.total} images, ${counter.links.total} link, ${counter.readmeFiles} README files and ${counter.exercises} exercises.`
350
+ )
351
+ } else {
352
+ Console.success(`We didn't find any errors in this repository.`)
353
+ }
354
+
355
+ process.exit(0)
356
+ }
357
+ } else {
358
+ reject("Failed")
359
+ }
360
+ })
361
+ }
362
+
363
+ // This function checks if there are warnings, and show them in the console at the end.
364
+ const showWarnings = (warnings: IAuditErrors[]) => {
365
+ return new Promise((resolve, reject) => {
366
+ if (warnings) {
367
+ if (warnings.length > 0) {
368
+ Console.log("Checking for warnings...")
369
+ for (const [i, warning] of warnings.entries())
370
+ Console.warning(
371
+ `${i + 1}) ${warning.msg} ${
372
+ warning.exercise ? `File: ${warning.exercise}` : ""
373
+ }`
374
+ )
375
+ }
376
+
377
+ resolve("SUCCESS")
378
+ } else {
379
+ reject("Failed")
380
+ }
381
+ })
382
+ }
383
+
384
+ export default {
385
+ isUrl,
386
+ checkForEmptySpaces,
387
+ checkLearnpackClean,
388
+ findInFile,
389
+ checkUrl,
390
+ writeFile,
391
+ showErrors,
392
+ showWarnings,
393
+ }