@learnpack/learnpack 5.0.8 → 5.0.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) 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 +15 -15
  5. package/lib/commands/clean.js +3 -3
  6. package/lib/commands/download.js +3 -3
  7. package/lib/commands/login.js +3 -3
  8. package/lib/commands/logout.js +3 -3
  9. package/lib/commands/publish.js +8 -7
  10. package/lib/managers/config/index.js +10 -0
  11. package/lib/managers/session.js +1 -1
  12. package/lib/utils/checkNotInstalled.js +3 -5
  13. package/oclif.manifest.json +1 -1
  14. package/package.json +152 -152
  15. package/src/commands/audit.ts +449 -449
  16. package/src/commands/clean.ts +29 -29
  17. package/src/commands/download.ts +61 -61
  18. package/src/commands/login.ts +42 -42
  19. package/src/commands/logout.ts +43 -43
  20. package/src/commands/publish.ts +9 -7
  21. package/src/commands/test.ts +85 -85
  22. package/src/index.ts +1 -1
  23. package/src/managers/config/allowed_files.ts +29 -29
  24. package/src/managers/config/index.ts +12 -0
  25. package/src/managers/gitpod.ts +84 -84
  26. package/src/managers/server/index.ts +78 -78
  27. package/src/managers/session.ts +2 -1
  28. package/src/managers/telemetry.ts +353 -353
  29. package/src/managers/test.ts +83 -83
  30. package/src/models/audit.ts +16 -16
  31. package/src/models/config-manager.ts +23 -23
  32. package/src/models/counter.ts +11 -11
  33. package/src/models/errors.ts +22 -22
  34. package/src/models/exercise-obj.ts +29 -29
  35. package/src/models/file.ts +5 -5
  36. package/src/models/findings.ts +18 -18
  37. package/src/models/flags.ts +10 -10
  38. package/src/models/front-matter.ts +11 -11
  39. package/src/models/gitpod-data.ts +19 -19
  40. package/src/models/language.ts +4 -4
  41. package/src/models/package.ts +7 -7
  42. package/src/models/plugin-config.ts +17 -17
  43. package/src/models/success-types.ts +1 -1
  44. package/src/plugin/command/compile.ts +17 -17
  45. package/src/plugin/command/test.ts +30 -30
  46. package/src/plugin/index.ts +6 -6
  47. package/src/plugin/plugin.ts +94 -94
  48. package/src/plugin/utils.ts +87 -87
  49. package/src/types/node-fetch.d.ts +1 -1
  50. package/src/ui/download.ts +71 -71
  51. package/src/utils/BaseCommand.ts +48 -48
  52. package/src/utils/SessionCommand.ts +43 -43
  53. package/src/utils/audit.ts +393 -393
  54. package/src/utils/checkNotInstalled.ts +10 -12
  55. package/src/utils/errors.ts +117 -117
  56. package/src/utils/exercisesQueue.ts +51 -51
  57. package/src/utils/fileQueue.ts +199 -199
  58. package/src/utils/misc.ts +23 -23
  59. package/src/utils/osOperations.ts +79 -79
  60. package/src/utils/templates/gitignore.txt +19 -19
  61. package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.es.md +24 -24
  62. package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.md +24 -24
  63. package/src/utils/templates/incremental/.vscode/schema.json +121 -121
  64. package/src/utils/templates/incremental/.vscode/settings.json +13 -13
  65. package/src/utils/templates/incremental/README.ejs +4 -4
  66. package/src/utils/templates/incremental/README.es.ejs +4 -4
  67. package/src/utils/templates/isolated/.vscode/schema.json +121 -121
  68. package/src/utils/templates/isolated/.vscode/settings.json +13 -13
  69. package/src/utils/templates/isolated/README.ejs +4 -4
  70. package/src/utils/templates/isolated/README.es.ejs +4 -4
  71. package/src/utils/templates/no-grading/README.ejs +4 -4
  72. package/src/utils/templates/no-grading/README.es.ejs +4 -4
  73. package/src/utils/validators.ts +18 -18
  74. 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
+ }