@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,449 +1,449 @@
1
- import * as fs from "fs"
2
- import { validateExerciseDirectoryName } from "../managers/config/exercise"
3
- import Console from "../utils/console"
4
- import Audit from "../utils/audit"
5
- import SessionCommand from "../utils/SessionCommand"
6
- import * as path from "path"
7
- import { IFile } from "../models/file"
8
- import { IExercise } from "../models/exercise-obj"
9
- import { IFrontmatter } from "../models/front-matter"
10
- import { IAuditErrors } from "../models/audit"
11
- import { ICounter } from "../models/counter"
12
- import { IFindings } from "../models/findings"
13
-
14
- // eslint-disable-next-line
15
- const fetch = require("node-fetch");
16
-
17
- class AuditCommand extends SessionCommand {
18
- async init() {
19
- const { flags } = this.parse(AuditCommand)
20
- await this.initSession(flags)
21
- }
22
-
23
- async run() {
24
- Console.log("Running command audit...")
25
-
26
- // Get configuration object.
27
- let config = this.configManager?.get()
28
-
29
- if (config) {
30
- const errors: IAuditErrors[] = []
31
- const warnings: IAuditErrors[] = []
32
- if (config?.config?.projectType === "tutorial") {
33
- const counter: ICounter = {
34
- images: {
35
- error: 0,
36
- total: 0,
37
- },
38
- links: {
39
- error: 0,
40
- total: 0,
41
- },
42
- exercises: 0,
43
- readmeFiles: 0,
44
- }
45
-
46
- // Checks if learnpack clean has been run
47
- Audit.checkLearnpackClean(config, errors)
48
-
49
- // Build exercises if they are not built yet.
50
- this.configManager?.buildIndex()
51
- config = this.configManager?.get()
52
-
53
- // Check if the exercises folder has some files within any ./exercise
54
- const exercisesPath: string = config!.config!.exercisesPath
55
-
56
- fs.readdir(exercisesPath, (err, files) => {
57
- if (err) {
58
- return console.log("Unable to scan directory: " + err)
59
- }
60
-
61
- // listing all files using forEach
62
- for (const file of files) {
63
- // Do whatever you want to do with the file
64
- const filePath: string = path.join(exercisesPath, file)
65
- if (fs.statSync(filePath).isFile())
66
- warnings.push({
67
- exercise: file!,
68
- msg: "This file is not inside any exercise folder.",
69
- })
70
- }
71
- })
72
-
73
- // This function is being created because the find method doesn't work with promises.
74
- const find = async (file: IFile, lang: string, exercise: IExercise) => {
75
- if (file.name === lang) {
76
- await Audit.checkUrl(
77
- config!,
78
- file.path,
79
- file.name,
80
- exercise,
81
- errors,
82
- warnings,
83
- counter
84
- )
85
- return true
86
- }
87
-
88
- return false
89
- }
90
-
91
- Console.debug("config", config)
92
-
93
- Console.info(" Checking if the config file is fine...")
94
- // These two lines check if the 'slug' property is inside the configuration object.
95
- Console.debug(
96
- "Checking if the slug property is inside the configuration object..."
97
- )
98
- // check if the slug property is in the configuration object
99
- if (!config!.config?.slug)
100
- errors.push({
101
- exercise: undefined,
102
- msg: "The slug property is not in the configuration object",
103
- })
104
- // check if the duration property is in the configuration object
105
- if (!config!.config?.duration)
106
- warnings.push({
107
- exercise: undefined,
108
- msg: "The duration property is not in the configuration object",
109
- })
110
- // check if the difficulty property is in the configuration object
111
- if (!config!.config?.difficulty)
112
- warnings.push({
113
- exercise: undefined,
114
- msg: "The difficulty property is not in the configuration object",
115
- })
116
- // check if the bugs_link property is in the configuration object
117
- if (!config!.config?.bugsLink)
118
- errors.push({
119
- exercise: undefined,
120
- msg: "The bugsLink property is not in the configuration object",
121
- })
122
- // check if the video_solutions property is in the configuration object
123
- if (config!.config?.videoSolutions === undefined)
124
- warnings.push({
125
- exercise: undefined,
126
- msg: "The videoSolutions property is not in the configuration object",
127
- })
128
-
129
- // These two lines check if the 'repository' property is inside the configuration object.
130
- Console.debug(
131
- "Checking if the repository property is inside the configuration object..."
132
- )
133
- if (!config!.config?.repository)
134
- errors.push({
135
- exercise: undefined,
136
- msg: "The repository property is not in the configuration object",
137
- })
138
- else
139
- Audit.isUrl(config!.config?.repository, errors, counter)
140
-
141
- // These two lines check if the 'description' property is inside the configuration object.
142
- Console.debug(
143
- "Checking if the description property is inside the configuration object..."
144
- )
145
- if (!config!.config?.description)
146
- errors.push({
147
- exercise: undefined,
148
- msg: "The description property is not in the configuration object",
149
- })
150
- if (!config!.config?.projectType)
151
- errors.push({
152
- exercise: undefined,
153
- msg: "The projectType property mandatory in the configuration object",
154
- })
155
-
156
- if (errors.length === 0)
157
- Console.log("The config file is ok")
158
-
159
- // Validates if images and links are working at every README file.
160
- const exercises = config!.exercises
161
- const readmeFiles = []
162
-
163
- if (exercises && exercises.length > 0) {
164
- Console.info(" Checking if the images are working...")
165
- for (const index in exercises) {
166
- if (Object.prototype.hasOwnProperty.call(exercises, index)) {
167
- const exercise = exercises[index]
168
- if (!validateExerciseDirectoryName(exercise.title))
169
- errors.push({
170
- exercise: exercise.title,
171
- msg: `The exercise ${exercise.title} has an invalid name.`,
172
- })
173
- let readmeFilesCount = { exercise: exercise.title, count: 0 }
174
- if (Object.keys(exercise.translations!).length === 0)
175
- errors.push({
176
- exercise: exercise.title,
177
- msg: `The exercise ${exercise.title} doesn't have a README.md file.`,
178
- })
179
-
180
- if (
181
- exercise.language === "python3" ||
182
- exercise.language === "python"
183
- ) {
184
- for (const f of exercise.files.map(f => f)) {
185
- if (
186
- f.path.includes("test.py") ||
187
- f.path.includes("tests.py")
188
- ) {
189
- const content = fs.readFileSync(f.path).toString()
190
- const isEmpty = Audit.checkForEmptySpaces(content)
191
- if (isEmpty || !content)
192
- errors.push({
193
- exercise: exercise.title,
194
- msg: `This file (${f.name}) doesn't have any content inside.`,
195
- })
196
- }
197
- }
198
- } else {
199
- for (const f of exercise.files.map(f => f)) {
200
- if (
201
- f.path.includes("test.js") ||
202
- f.path.includes("tests.js")
203
- ) {
204
- const content = fs.readFileSync(f.path).toString()
205
- const isEmpty: boolean = Audit.checkForEmptySpaces(content)
206
- if (isEmpty || !content)
207
- errors.push({
208
- exercise: exercise.title,
209
- msg: `This file (${f.name}) doesn't have any content inside.`,
210
- })
211
- }
212
- }
213
- }
214
-
215
- for (const lang in exercise.translations) {
216
- if (
217
- Object.prototype.hasOwnProperty.call(
218
- exercise.translations,
219
- lang
220
- )
221
- ) {
222
- const files: any[] = []
223
- const findResultPromises = []
224
- for (const file of exercise.files) {
225
- const found = find(
226
- file,
227
- exercise.translations[lang],
228
- exercise
229
- )
230
- findResultPromises.push(found)
231
- }
232
- // eslint-disable-next-line
233
- let findResults = await Promise.all(findResultPromises);
234
- for (const found of findResults) {
235
- if (found) {
236
- readmeFilesCount = {
237
- ...readmeFilesCount,
238
- count: readmeFilesCount.count + 1,
239
- }
240
- files.push(found)
241
- }
242
- }
243
-
244
- if (!files.includes(true))
245
- errors.push({
246
- exercise: exercise.title,
247
- msg: "This exercise doesn't have a README.md file.",
248
- })
249
- }
250
- }
251
-
252
- readmeFiles.push(readmeFilesCount)
253
- }
254
- }
255
- } else
256
- errors.push({
257
- exercise: undefined,
258
- msg: "The exercises array is empty.",
259
- })
260
-
261
- Console.log(
262
- `${counter.images.total - counter.images.error} images ok from ${
263
- counter.images.total
264
- }`
265
- )
266
-
267
- Console.info(
268
- " Checking if important files are missing... (README's, translations, gitignore...)"
269
- )
270
- // Check if all the exercises has the same ammount of README's, this way we can check if they have the same ammount of translations.
271
- const files: string[] = []
272
- let count = 0
273
- for (const item of readmeFiles) {
274
- if (count < item.count)
275
- count = item.count
276
- }
277
-
278
- for (const item of readmeFiles) {
279
- if (item.count !== count)
280
- files.push(` ${item.exercise}`)
281
- }
282
-
283
- if (files.length > 0) {
284
- const filesString: string = files.join(",")
285
- warnings.push({
286
- exercise: undefined,
287
- msg:
288
- files.length === 1 ?
289
- `This exercise is missing translations:${filesString}` :
290
- `These exercises are missing translations:${filesString}`,
291
- })
292
- }
293
-
294
- // Checks if the .gitignore file exists.
295
- if (!fs.existsSync(".gitignore"))
296
- warnings.push({
297
- exercise: undefined,
298
- msg: ".gitignore file doesn't exist",
299
- })
300
-
301
- counter.exercises = exercises!.length
302
- for (const readme of readmeFiles) {
303
- counter.readmeFiles += readme.count
304
- }
305
- } else {
306
- // This is the audit code for Projects
307
-
308
- // Checking the "learn.json" file:
309
- let learnjson
310
- try {
311
- learnjson = JSON.parse(fs.readFileSync("./learn.json").toString())
312
- } catch (_error: any) {
313
- Console.error(
314
- "Invalid JSON syntax in learn.json file:",
315
- _error.message
316
- )
317
- process.exit(1)
318
- }
319
-
320
- if (!learnjson) {
321
- Console.error(
322
- "There is no learn.json file located in the root of the project."
323
- )
324
- process.exit(1)
325
- }
326
-
327
- // Checking the README.md files and possible translations.
328
- let readmeFiles: any[] = []
329
- const translations: string[] = []
330
- const translationRegex = /README\.([a-z]{2,3})\.md/
331
-
332
- try {
333
- const data = await fs.promises.readdir("./")
334
- readmeFiles = data.filter(file => file.includes("README"))
335
- if (readmeFiles.length === 0)
336
- errors.push({
337
- exercise: undefined!,
338
- msg: `There is no README file in the repository.`,
339
- })
340
- } catch (error) {
341
- if (error)
342
- Console.error(
343
- "There was an error getting the directory files",
344
- error
345
- )
346
- }
347
-
348
- for (const readmeFile of readmeFiles) {
349
- // Checking the language of each README file.
350
- if (readmeFile === "README.md")
351
- translations.push("us")
352
- else {
353
- const regexGroups = translationRegex.exec(readmeFile)
354
- if (regexGroups)
355
- translations.push(regexGroups[1])
356
- }
357
-
358
- const readme = fs.readFileSync(path.resolve(readmeFile)).toString()
359
-
360
- const isEmpty = Audit.checkForEmptySpaces(readme)
361
- if (isEmpty || !readme) {
362
- errors.push({
363
- exercise: undefined!,
364
- msg: `This file "${readmeFile}" doesn't have any content inside.`,
365
- })
366
- continue
367
- }
368
-
369
- if (readme.length < 800)
370
- errors.push({
371
- exercise: undefined,
372
- msg: `The "${readmeFile}" file should have at least 800 characters (It currently have: ${readme.length}).`,
373
- })
374
-
375
- // eslint-disable-next-line
376
- await Audit.checkUrl(
377
- config!,
378
- path.resolve(readmeFile),
379
- readmeFile,
380
- undefined,
381
- errors,
382
- warnings,
383
- // eslint-disable-next-line
384
- undefined
385
- )
386
- }
387
-
388
- // Adding the translations to the learn.json
389
- learnjson.translations = translations
390
-
391
- // Checking if the preview image (from the learn.json) is OK.
392
- try {
393
- const res = await fetch(learnjson.preview, { method: "HEAD" })
394
- if (res.status > 399 && res.status < 500) {
395
- errors.push({
396
- exercise: undefined,
397
- msg: `The link of the "preview" is broken: ${learnjson.preview}`,
398
- })
399
- }
400
- } catch {
401
- errors.push({
402
- exercise: undefined,
403
- msg: `The link of the "preview" is broken: ${learnjson.preview}`,
404
- })
405
- }
406
-
407
- const date = new Date()
408
- learnjson.validationAt = date.getTime()
409
-
410
- if (errors.length > 0)
411
- learnjson.validationStatus = "error"
412
- else if (warnings.length > 0)
413
- learnjson.validationStatus = "warning"
414
- else
415
- learnjson.validationStatus = "success"
416
-
417
- // Writes the "learn.json" file with all the new properties
418
- await fs.promises.writeFile("./learn.json", JSON.stringify(learnjson))
419
- }
420
-
421
- await Audit.showWarnings(warnings)
422
- // eslint-disable-next-line
423
- await Audit.showErrors(errors, undefined);
424
- }
425
- }
426
- }
427
-
428
- AuditCommand.description = `learnpack audit is the command in charge of creating an auditory of the repository
429
- ...
430
- learnpack audit checks for the following information in a repository:
431
- 1. The configuration object has slug, repository and description. (Error)
432
- 2. The command learnpack clean has been run. (Error)
433
- 3. If a markdown or test file doesn't have any content. (Error)
434
- 4. The links are accessing to valid servers. (Error)
435
- 5. The relative images are working (If they have the shortest path to the image or if the images exists in the assets). (Error)
436
- 6. The external images are working (If they are pointing to a valid server). (Error)
437
- 7. The exercises directory names are valid. (Error)
438
- 8. If an exercise doesn't have a README file. (Error)
439
- 9. The exercises array (Of the config file) has content. (Error)
440
- 10. The exercses have the same translations. (Warning)
441
- 11. The .gitignore file exists. (Warning)
442
- 12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)
443
- `
444
-
445
- AuditCommand.flags = {
446
- // name: flags.string({char: 'n', description: 'name to print'}),
447
- }
448
-
449
- export default AuditCommand
1
+ import * as fs from "fs"
2
+ import { validateExerciseDirectoryName } from "../managers/config/exercise"
3
+ import Console from "../utils/console"
4
+ import Audit from "../utils/audit"
5
+ import SessionCommand from "../utils/SessionCommand"
6
+ import * as path from "path"
7
+ import { IFile } from "../models/file"
8
+ import { IExercise } from "../models/exercise-obj"
9
+ import { IFrontmatter } from "../models/front-matter"
10
+ import { IAuditErrors } from "../models/audit"
11
+ import { ICounter } from "../models/counter"
12
+ import { IFindings } from "../models/findings"
13
+
14
+ // eslint-disable-next-line
15
+ const fetch = require("node-fetch");
16
+
17
+ class AuditCommand extends SessionCommand {
18
+ async init() {
19
+ const { flags } = this.parse(AuditCommand)
20
+ await this.initSession(flags)
21
+ }
22
+
23
+ async run() {
24
+ Console.log("Running command audit...")
25
+
26
+ // Get configuration object.
27
+ let config = this.configManager?.get()
28
+
29
+ if (config) {
30
+ const errors: IAuditErrors[] = []
31
+ const warnings: IAuditErrors[] = []
32
+ if (config?.config?.projectType === "tutorial") {
33
+ const counter: ICounter = {
34
+ images: {
35
+ error: 0,
36
+ total: 0,
37
+ },
38
+ links: {
39
+ error: 0,
40
+ total: 0,
41
+ },
42
+ exercises: 0,
43
+ readmeFiles: 0,
44
+ }
45
+
46
+ // Checks if learnpack clean has been run
47
+ Audit.checkLearnpackClean(config, errors)
48
+
49
+ // Build exercises if they are not built yet.
50
+ this.configManager?.buildIndex()
51
+ config = this.configManager?.get()
52
+
53
+ // Check if the exercises folder has some files within any ./exercise
54
+ const exercisesPath: string = config!.config!.exercisesPath
55
+
56
+ fs.readdir(exercisesPath, (err, files) => {
57
+ if (err) {
58
+ return console.log("Unable to scan directory: " + err)
59
+ }
60
+
61
+ // listing all files using forEach
62
+ for (const file of files) {
63
+ // Do whatever you want to do with the file
64
+ const filePath: string = path.join(exercisesPath, file)
65
+ if (fs.statSync(filePath).isFile())
66
+ warnings.push({
67
+ exercise: file!,
68
+ msg: "This file is not inside any exercise folder.",
69
+ })
70
+ }
71
+ })
72
+
73
+ // This function is being created because the find method doesn't work with promises.
74
+ const find = async (file: IFile, lang: string, exercise: IExercise) => {
75
+ if (file.name === lang) {
76
+ await Audit.checkUrl(
77
+ config!,
78
+ file.path,
79
+ file.name,
80
+ exercise,
81
+ errors,
82
+ warnings,
83
+ counter
84
+ )
85
+ return true
86
+ }
87
+
88
+ return false
89
+ }
90
+
91
+ Console.debug("config", config)
92
+
93
+ Console.info(" Checking if the config file is fine...")
94
+ // These two lines check if the 'slug' property is inside the configuration object.
95
+ Console.debug(
96
+ "Checking if the slug property is inside the configuration object..."
97
+ )
98
+ // check if the slug property is in the configuration object
99
+ if (!config!.config?.slug)
100
+ errors.push({
101
+ exercise: undefined,
102
+ msg: "The slug property is not in the configuration object",
103
+ })
104
+ // check if the duration property is in the configuration object
105
+ if (!config!.config?.duration)
106
+ warnings.push({
107
+ exercise: undefined,
108
+ msg: "The duration property is not in the configuration object",
109
+ })
110
+ // check if the difficulty property is in the configuration object
111
+ if (!config!.config?.difficulty)
112
+ warnings.push({
113
+ exercise: undefined,
114
+ msg: "The difficulty property is not in the configuration object",
115
+ })
116
+ // check if the bugs_link property is in the configuration object
117
+ if (!config!.config?.bugsLink)
118
+ errors.push({
119
+ exercise: undefined,
120
+ msg: "The bugsLink property is not in the configuration object",
121
+ })
122
+ // check if the video_solutions property is in the configuration object
123
+ if (config!.config?.videoSolutions === undefined)
124
+ warnings.push({
125
+ exercise: undefined,
126
+ msg: "The videoSolutions property is not in the configuration object",
127
+ })
128
+
129
+ // These two lines check if the 'repository' property is inside the configuration object.
130
+ Console.debug(
131
+ "Checking if the repository property is inside the configuration object..."
132
+ )
133
+ if (!config!.config?.repository)
134
+ errors.push({
135
+ exercise: undefined,
136
+ msg: "The repository property is not in the configuration object",
137
+ })
138
+ else
139
+ Audit.isUrl(config!.config?.repository, errors, counter)
140
+
141
+ // These two lines check if the 'description' property is inside the configuration object.
142
+ Console.debug(
143
+ "Checking if the description property is inside the configuration object..."
144
+ )
145
+ if (!config!.config?.description)
146
+ errors.push({
147
+ exercise: undefined,
148
+ msg: "The description property is not in the configuration object",
149
+ })
150
+ if (!config!.config?.projectType)
151
+ errors.push({
152
+ exercise: undefined,
153
+ msg: "The projectType property mandatory in the configuration object",
154
+ })
155
+
156
+ if (errors.length === 0)
157
+ Console.log("The config file is ok")
158
+
159
+ // Validates if images and links are working at every README file.
160
+ const exercises = config!.exercises
161
+ const readmeFiles = []
162
+
163
+ if (exercises && exercises.length > 0) {
164
+ Console.info(" Checking if the images are working...")
165
+ for (const index in exercises) {
166
+ if (Object.prototype.hasOwnProperty.call(exercises, index)) {
167
+ const exercise = exercises[index]
168
+ if (!validateExerciseDirectoryName(exercise.title))
169
+ errors.push({
170
+ exercise: exercise.title,
171
+ msg: `The exercise ${exercise.title} has an invalid name.`,
172
+ })
173
+ let readmeFilesCount = { exercise: exercise.title, count: 0 }
174
+ if (Object.keys(exercise.translations!).length === 0)
175
+ errors.push({
176
+ exercise: exercise.title,
177
+ msg: `The exercise ${exercise.title} doesn't have a README.md file.`,
178
+ })
179
+
180
+ if (
181
+ exercise.language === "python3" ||
182
+ exercise.language === "python"
183
+ ) {
184
+ for (const f of exercise.files.map(f => f)) {
185
+ if (
186
+ f.path.includes("test.py") ||
187
+ f.path.includes("tests.py")
188
+ ) {
189
+ const content = fs.readFileSync(f.path).toString()
190
+ const isEmpty = Audit.checkForEmptySpaces(content)
191
+ if (isEmpty || !content)
192
+ errors.push({
193
+ exercise: exercise.title,
194
+ msg: `This file (${f.name}) doesn't have any content inside.`,
195
+ })
196
+ }
197
+ }
198
+ } else {
199
+ for (const f of exercise.files.map(f => f)) {
200
+ if (
201
+ f.path.includes("test.js") ||
202
+ f.path.includes("tests.js")
203
+ ) {
204
+ const content = fs.readFileSync(f.path).toString()
205
+ const isEmpty: boolean = Audit.checkForEmptySpaces(content)
206
+ if (isEmpty || !content)
207
+ errors.push({
208
+ exercise: exercise.title,
209
+ msg: `This file (${f.name}) doesn't have any content inside.`,
210
+ })
211
+ }
212
+ }
213
+ }
214
+
215
+ for (const lang in exercise.translations) {
216
+ if (
217
+ Object.prototype.hasOwnProperty.call(
218
+ exercise.translations,
219
+ lang
220
+ )
221
+ ) {
222
+ const files: any[] = []
223
+ const findResultPromises = []
224
+ for (const file of exercise.files) {
225
+ const found = find(
226
+ file,
227
+ exercise.translations[lang],
228
+ exercise
229
+ )
230
+ findResultPromises.push(found)
231
+ }
232
+ // eslint-disable-next-line
233
+ let findResults = await Promise.all(findResultPromises);
234
+ for (const found of findResults) {
235
+ if (found) {
236
+ readmeFilesCount = {
237
+ ...readmeFilesCount,
238
+ count: readmeFilesCount.count + 1,
239
+ }
240
+ files.push(found)
241
+ }
242
+ }
243
+
244
+ if (!files.includes(true))
245
+ errors.push({
246
+ exercise: exercise.title,
247
+ msg: "This exercise doesn't have a README.md file.",
248
+ })
249
+ }
250
+ }
251
+
252
+ readmeFiles.push(readmeFilesCount)
253
+ }
254
+ }
255
+ } else
256
+ errors.push({
257
+ exercise: undefined,
258
+ msg: "The exercises array is empty.",
259
+ })
260
+
261
+ Console.log(
262
+ `${counter.images.total - counter.images.error} images ok from ${
263
+ counter.images.total
264
+ }`
265
+ )
266
+
267
+ Console.info(
268
+ " Checking if important files are missing... (README's, translations, gitignore...)"
269
+ )
270
+ // Check if all the exercises has the same ammount of README's, this way we can check if they have the same ammount of translations.
271
+ const files: string[] = []
272
+ let count = 0
273
+ for (const item of readmeFiles) {
274
+ if (count < item.count)
275
+ count = item.count
276
+ }
277
+
278
+ for (const item of readmeFiles) {
279
+ if (item.count !== count)
280
+ files.push(` ${item.exercise}`)
281
+ }
282
+
283
+ if (files.length > 0) {
284
+ const filesString: string = files.join(",")
285
+ warnings.push({
286
+ exercise: undefined,
287
+ msg:
288
+ files.length === 1 ?
289
+ `This exercise is missing translations:${filesString}` :
290
+ `These exercises are missing translations:${filesString}`,
291
+ })
292
+ }
293
+
294
+ // Checks if the .gitignore file exists.
295
+ if (!fs.existsSync(".gitignore"))
296
+ warnings.push({
297
+ exercise: undefined,
298
+ msg: ".gitignore file doesn't exist",
299
+ })
300
+
301
+ counter.exercises = exercises!.length
302
+ for (const readme of readmeFiles) {
303
+ counter.readmeFiles += readme.count
304
+ }
305
+ } else {
306
+ // This is the audit code for Projects
307
+
308
+ // Checking the "learn.json" file:
309
+ let learnjson
310
+ try {
311
+ learnjson = JSON.parse(fs.readFileSync("./learn.json").toString())
312
+ } catch (_error: any) {
313
+ Console.error(
314
+ "Invalid JSON syntax in learn.json file:",
315
+ _error.message
316
+ )
317
+ process.exit(1)
318
+ }
319
+
320
+ if (!learnjson) {
321
+ Console.error(
322
+ "There is no learn.json file located in the root of the project."
323
+ )
324
+ process.exit(1)
325
+ }
326
+
327
+ // Checking the README.md files and possible translations.
328
+ let readmeFiles: any[] = []
329
+ const translations: string[] = []
330
+ const translationRegex = /README\.([a-z]{2,3})\.md/
331
+
332
+ try {
333
+ const data = await fs.promises.readdir("./")
334
+ readmeFiles = data.filter(file => file.includes("README"))
335
+ if (readmeFiles.length === 0)
336
+ errors.push({
337
+ exercise: undefined!,
338
+ msg: `There is no README file in the repository.`,
339
+ })
340
+ } catch (error) {
341
+ if (error)
342
+ Console.error(
343
+ "There was an error getting the directory files",
344
+ error
345
+ )
346
+ }
347
+
348
+ for (const readmeFile of readmeFiles) {
349
+ // Checking the language of each README file.
350
+ if (readmeFile === "README.md")
351
+ translations.push("us")
352
+ else {
353
+ const regexGroups = translationRegex.exec(readmeFile)
354
+ if (regexGroups)
355
+ translations.push(regexGroups[1])
356
+ }
357
+
358
+ const readme = fs.readFileSync(path.resolve(readmeFile)).toString()
359
+
360
+ const isEmpty = Audit.checkForEmptySpaces(readme)
361
+ if (isEmpty || !readme) {
362
+ errors.push({
363
+ exercise: undefined!,
364
+ msg: `This file "${readmeFile}" doesn't have any content inside.`,
365
+ })
366
+ continue
367
+ }
368
+
369
+ if (readme.length < 800)
370
+ errors.push({
371
+ exercise: undefined,
372
+ msg: `The "${readmeFile}" file should have at least 800 characters (It currently have: ${readme.length}).`,
373
+ })
374
+
375
+ // eslint-disable-next-line
376
+ await Audit.checkUrl(
377
+ config!,
378
+ path.resolve(readmeFile),
379
+ readmeFile,
380
+ undefined,
381
+ errors,
382
+ warnings,
383
+ // eslint-disable-next-line
384
+ undefined
385
+ )
386
+ }
387
+
388
+ // Adding the translations to the learn.json
389
+ learnjson.translations = translations
390
+
391
+ // Checking if the preview image (from the learn.json) is OK.
392
+ try {
393
+ const res = await fetch(learnjson.preview, { method: "HEAD" })
394
+ if (res.status > 399 && res.status < 500) {
395
+ errors.push({
396
+ exercise: undefined,
397
+ msg: `The link of the "preview" is broken: ${learnjson.preview}`,
398
+ })
399
+ }
400
+ } catch {
401
+ errors.push({
402
+ exercise: undefined,
403
+ msg: `The link of the "preview" is broken: ${learnjson.preview}`,
404
+ })
405
+ }
406
+
407
+ const date = new Date()
408
+ learnjson.validationAt = date.getTime()
409
+
410
+ if (errors.length > 0)
411
+ learnjson.validationStatus = "error"
412
+ else if (warnings.length > 0)
413
+ learnjson.validationStatus = "warning"
414
+ else
415
+ learnjson.validationStatus = "success"
416
+
417
+ // Writes the "learn.json" file with all the new properties
418
+ await fs.promises.writeFile("./learn.json", JSON.stringify(learnjson))
419
+ }
420
+
421
+ await Audit.showWarnings(warnings)
422
+ // eslint-disable-next-line
423
+ await Audit.showErrors(errors, undefined);
424
+ }
425
+ }
426
+ }
427
+
428
+ AuditCommand.description = `learnpack audit is the command in charge of creating an auditory of the repository
429
+ ...
430
+ learnpack audit checks for the following information in a repository:
431
+ 1. The configuration object has slug, repository and description. (Error)
432
+ 2. The command learnpack clean has been run. (Error)
433
+ 3. If a markdown or test file doesn't have any content. (Error)
434
+ 4. The links are accessing to valid servers. (Error)
435
+ 5. The relative images are working (If they have the shortest path to the image or if the images exists in the assets). (Error)
436
+ 6. The external images are working (If they are pointing to a valid server). (Error)
437
+ 7. The exercises directory names are valid. (Error)
438
+ 8. If an exercise doesn't have a README file. (Error)
439
+ 9. The exercises array (Of the config file) has content. (Error)
440
+ 10. The exercses have the same translations. (Warning)
441
+ 11. The .gitignore file exists. (Warning)
442
+ 12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)
443
+ `
444
+
445
+ AuditCommand.flags = {
446
+ // name: flags.string({char: 'n', description: 'name to print'}),
447
+ }
448
+
449
+ export default AuditCommand