@learnpack/learnpack 5.0.275 → 5.0.277

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