@learnpack/learnpack 5.0.6 → 5.0.8

Sign up to get free protection for your applications and to get access to all the features.
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,443 +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
-
151
- if (errors.length === 0)
152
- Console.log("The config file is ok")
153
-
154
- // Validates if images and links are working at every README file.
155
- const exercises = config!.exercises
156
- const readmeFiles = []
157
-
158
- if (exercises && exercises.length > 0) {
159
- Console.info(" Checking if the images are working...")
160
- for (const index in exercises) {
161
- if (Object.prototype.hasOwnProperty.call(exercises, index)) {
162
- const exercise = exercises[index]
163
- if (!validateExerciseDirectoryName(exercise.title))
164
- errors.push({
165
- exercise: exercise.title,
166
- msg: `The exercise ${exercise.title} has an invalid name.`,
167
- })
168
- let readmeFilesCount = { exercise: exercise.title, count: 0 }
169
- if (Object.keys(exercise.translations!).length === 0)
170
- errors.push({
171
- exercise: exercise.title,
172
- msg: `The exercise ${exercise.title} doesn't have a README.md file.`,
173
- })
174
-
175
- if (
176
- exercise.language === "python3" ||
177
- exercise.language === "python"
178
- ) {
179
- for (const f of exercise.files.map(f => f)) {
180
- if (
181
- f.path.includes("test.py") ||
182
- f.path.includes("tests.py")
183
- ) {
184
- const content = fs.readFileSync(f.path).toString()
185
- const isEmpty = Audit.checkForEmptySpaces(content)
186
- if (isEmpty || !content)
187
- errors.push({
188
- exercise: exercise.title,
189
- msg: `This file (${f.name}) doesn't have any content inside.`,
190
- })
191
- }
192
- }
193
- } else {
194
- for (const f of exercise.files.map(f => f)) {
195
- if (
196
- f.path.includes("test.js") ||
197
- f.path.includes("tests.js")
198
- ) {
199
- const content = fs.readFileSync(f.path).toString()
200
- const isEmpty: boolean = Audit.checkForEmptySpaces(content)
201
- if (isEmpty || !content)
202
- errors.push({
203
- exercise: exercise.title,
204
- msg: `This file (${f.name}) doesn't have any content inside.`,
205
- })
206
- }
207
- }
208
- }
209
-
210
- for (const lang in exercise.translations) {
211
- if (
212
- Object.prototype.hasOwnProperty.call(
213
- exercise.translations,
214
- lang
215
- )
216
- ) {
217
- const files: any[] = []
218
- const findResultPromises = []
219
- for (const file of exercise.files) {
220
- const found = find(
221
- file,
222
- exercise.translations[lang],
223
- exercise
224
- )
225
- findResultPromises.push(found)
226
- }
227
- // eslint-disable-next-line
228
- let findResults = await Promise.all(findResultPromises);
229
- for (const found of findResults) {
230
- if (found) {
231
- readmeFilesCount = {
232
- ...readmeFilesCount,
233
- count: readmeFilesCount.count + 1,
234
- }
235
- files.push(found)
236
- }
237
- }
238
-
239
- if (!files.includes(true))
240
- errors.push({
241
- exercise: exercise.title,
242
- msg: "This exercise doesn't have a README.md file.",
243
- })
244
- }
245
- }
246
-
247
- readmeFiles.push(readmeFilesCount)
248
- }
249
- }
250
- } else
251
- errors.push({
252
- exercise: undefined,
253
- msg: "The exercises array is empty.",
254
- })
255
-
256
- Console.log(
257
- `${counter.images.total - counter.images.error} images ok from ${
258
- counter.images.total
259
- }`
260
- )
261
-
262
- Console.info(
263
- " Checking if important files are missing... (README's, translations, gitignore...)"
264
- )
265
- // 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.
266
- const files: string[] = []
267
- let count = 0
268
- for (const item of readmeFiles) {
269
- if (count < item.count)
270
- count = item.count
271
- }
272
-
273
- for (const item of readmeFiles) {
274
- if (item.count !== count)
275
- files.push(` ${item.exercise}`)
276
- }
277
-
278
- if (files.length > 0) {
279
- const filesString: string = files.join(",")
280
- warnings.push({
281
- exercise: undefined,
282
- msg:
283
- files.length === 1 ?
284
- `This exercise is missing translations:${filesString}` :
285
- `These exercises are missing translations:${filesString}`,
286
- })
287
- }
288
-
289
- // Checks if the .gitignore file exists.
290
- if (!fs.existsSync(".gitignore"))
291
- warnings.push({
292
- exercise: undefined,
293
- msg: ".gitignore file doesn't exist",
294
- })
295
-
296
- counter.exercises = exercises!.length
297
- for (const readme of readmeFiles) {
298
- counter.readmeFiles += readme.count
299
- }
300
- } else {
301
- // This is the audit code for Projects
302
-
303
- // Getting the learn.json schema
304
- const schemaResponse = await fetch(
305
- "https://raw.githubusercontent.com/tommygonzaleza/project-template/main/.github/learn-schema.json"
306
- )
307
- const schema = await schemaResponse.json()
308
-
309
- // Checking the "learn.json" file:
310
- const learnjson = JSON.parse(
311
- fs.readFileSync("./learn.json").toString()
312
- )
313
-
314
- if (!learnjson) {
315
- Console.error(
316
- "There is no learn.json file located in the root of the project."
317
- )
318
- process.exit(1)
319
- }
320
-
321
- // Checking the README.md files and possible translations.
322
- let readmeFiles: any[] = []
323
- const translations: string[] = []
324
- const translationRegex = /README\.([a-z]{2,3})\.md/
325
-
326
- try {
327
- const data = await fs.promises.readdir("./")
328
- readmeFiles = data.filter(file => file.includes("README"))
329
- if (readmeFiles.length === 0)
330
- errors.push({
331
- exercise: undefined!,
332
- msg: `There is no README file in the repository.`,
333
- })
334
- } catch (error) {
335
- if (error)
336
- Console.error(
337
- "There was an error getting the directory files",
338
- error
339
- )
340
- }
341
-
342
- for (const readmeFile of readmeFiles) {
343
- // Checking the language of each README file.
344
- if (readmeFile === "README.md")
345
- translations.push("us")
346
- else {
347
- const regexGroups = translationRegex.exec(readmeFile)
348
- if (regexGroups)
349
- translations.push(regexGroups[1])
350
- }
351
-
352
- const readme = fs.readFileSync(path.resolve(readmeFile)).toString()
353
-
354
- const isEmpty = Audit.checkForEmptySpaces(readme)
355
- if (isEmpty || !readme) {
356
- errors.push({
357
- exercise: undefined!,
358
- msg: `This file "${readmeFile}" doesn't have any content inside.`,
359
- })
360
- continue
361
- }
362
-
363
- if (readme.length < 800)
364
- errors.push({
365
- exercise: undefined,
366
- msg: `The "${readmeFile}" file should have at least 800 characters (It currently have: ${readme.length}).`,
367
- })
368
-
369
- // eslint-disable-next-line
370
- await Audit.checkUrl(
371
- config!,
372
- path.resolve(readmeFile),
373
- readmeFile,
374
- undefined,
375
- errors,
376
- warnings,
377
- // eslint-disable-next-line
378
- undefined
379
- )
380
- }
381
-
382
- // Adding the translations to the learn.json
383
- learnjson.translations = translations
384
-
385
- // Checking if the preview image (from the learn.json) is OK.
386
- try {
387
- const res = await fetch(learnjson.preview, { method: "HEAD" })
388
- if (res.status > 399 && res.status < 500) {
389
- errors.push({
390
- exercise: undefined,
391
- msg: `The link of the "preview" is broken: ${learnjson.preview}`,
392
- })
393
- }
394
- } catch {
395
- errors.push({
396
- exercise: undefined,
397
- msg: `The link of the "preview" is broken: ${learnjson.preview}`,
398
- })
399
- }
400
-
401
- const date = new Date()
402
- learnjson.validationAt = date.getTime()
403
-
404
- if (errors.length > 0)
405
- learnjson.validationStatus = "error"
406
- else if (warnings.length > 0)
407
- learnjson.validationStatus = "warning"
408
- else
409
- learnjson.validationStatus = "success"
410
-
411
- // Writes the "learn.json" file with all the new properties
412
- await fs.promises.writeFile("./learn.json", JSON.stringify(learnjson))
413
- }
414
-
415
- await Audit.showWarnings(warnings)
416
- // eslint-disable-next-line
417
- await Audit.showErrors(errors, undefined);
418
- }
419
- }
420
- }
421
-
422
- AuditCommand.description = `learnpack audit is the command in charge of creating an auditory of the repository
423
- ...
424
- learnpack audit checks for the following information in a repository:
425
- 1. The configuration object has slug, repository and description. (Error)
426
- 2. The command learnpack clean has been run. (Error)
427
- 3. If a markdown or test file doesn't have any content. (Error)
428
- 4. The links are accessing to valid servers. (Error)
429
- 5. The relative images are working (If they have the shortest path to the image or if the images exists in the assets). (Error)
430
- 6. The external images are working (If they are pointing to a valid server). (Error)
431
- 7. The exercises directory names are valid. (Error)
432
- 8. If an exercise doesn't have a README file. (Error)
433
- 9. The exercises array (Of the config file) has content. (Error)
434
- 10. The exercses have the same translations. (Warning)
435
- 11. The .gitignore file exists. (Warning)
436
- 12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)
437
- `
438
-
439
- AuditCommand.flags = {
440
- // name: flags.string({char: 'n', description: 'name to print'}),
441
- }
442
-
443
- 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