@learnpack/learnpack 5.0.7 → 5.0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) 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 +34 -22
  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/managers/config/index.d.ts +10 -0
  10. package/lib/managers/config/index.js +3 -1
  11. package/oclif.manifest.json +1 -1
  12. package/package.json +152 -152
  13. package/src/commands/audit.ts +449 -443
  14. package/src/commands/clean.ts +29 -29
  15. package/src/commands/download.ts +61 -61
  16. package/src/commands/login.ts +42 -42
  17. package/src/commands/logout.ts +43 -43
  18. package/src/commands/publish.ts +249 -249
  19. package/src/commands/test.ts +85 -85
  20. package/src/index.ts +1 -1
  21. package/src/managers/config/allowed_files.ts +29 -29
  22. package/src/managers/config/index.ts +6 -4
  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/templates/gitignore.txt +19 -19
  57. package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.es.md +24 -24
  58. package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.md +24 -24
  59. package/src/utils/templates/incremental/.vscode/schema.json +121 -121
  60. package/src/utils/templates/incremental/.vscode/settings.json +13 -13
  61. package/src/utils/templates/incremental/README.ejs +4 -4
  62. package/src/utils/templates/incremental/README.es.ejs +4 -4
  63. package/src/utils/templates/isolated/.vscode/schema.json +121 -121
  64. package/src/utils/templates/isolated/.vscode/settings.json +13 -13
  65. package/src/utils/templates/isolated/README.ejs +4 -4
  66. package/src/utils/templates/isolated/README.es.ejs +4 -4
  67. package/src/utils/templates/no-grading/README.ejs +4 -4
  68. package/src/utils/templates/no-grading/README.es.ejs +4 -4
  69. package/src/utils/validators.ts +18 -18
  70. 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
+ import { getConfigPath } from "../managers/config/index"
14
+
15
+ // eslint-disable-next-line
16
+ const fetch = require("node-fetch");
17
+
18
+ class AuditCommand extends SessionCommand {
19
+ async init() {
20
+ const { flags } = this.parse(AuditCommand)
21
+ await this.initSession(flags)
22
+ }
23
+
24
+ async run() {
25
+ Console.log("Running command audit...")
26
+
27
+ // Checking the "learn.json" file:
28
+ let learnjson
29
+ try {
30
+ const configPath: string = getConfigPath().config
31
+ const learnJsonContent: Buffer = fs.readFileSync(configPath)
32
+ learnjson = JSON.parse(learnJsonContent.toString())
33
+ } catch (_error: any) {
34
+ Console.error("Invalid JSON syntax in learn.json file:", _error.message)
35
+ process.exit(1)
36
+ }
37
+
38
+ // Get configuration object.
39
+ let config = this.configManager?.get()
40
+
41
+ if (config) {
42
+ const errors: IAuditErrors[] = []
43
+ const warnings: IAuditErrors[] = []
44
+ if (config?.config?.projectType === "tutorial") {
45
+ const counter: ICounter = {
46
+ images: {
47
+ error: 0,
48
+ total: 0,
49
+ },
50
+ links: {
51
+ error: 0,
52
+ total: 0,
53
+ },
54
+ exercises: 0,
55
+ readmeFiles: 0,
56
+ }
57
+
58
+ // Checks if learnpack clean has been run
59
+ Audit.checkLearnpackClean(config, errors)
60
+
61
+ // Build exercises if they are not built yet.
62
+ this.configManager?.buildIndex()
63
+ config = this.configManager?.get()
64
+
65
+ // Check if the exercises folder has some files within any ./exercise
66
+ const exercisesPath: string = config!.config!.exercisesPath
67
+
68
+ fs.readdir(exercisesPath, (err, files) => {
69
+ if (err) {
70
+ return console.log("Unable to scan directory: " + err)
71
+ }
72
+
73
+ // listing all files using forEach
74
+ for (const file of files) {
75
+ // Do whatever you want to do with the file
76
+ const filePath: string = path.join(exercisesPath, file)
77
+ if (fs.statSync(filePath).isFile())
78
+ warnings.push({
79
+ exercise: file!,
80
+ msg: "This file is not inside any exercise folder.",
81
+ })
82
+ }
83
+ })
84
+
85
+ // This function is being created because the find method doesn't work with promises.
86
+ const find = async (file: IFile, lang: string, exercise: IExercise) => {
87
+ if (file.name === lang) {
88
+ await Audit.checkUrl(
89
+ config!,
90
+ file.path,
91
+ file.name,
92
+ exercise,
93
+ errors,
94
+ warnings,
95
+ counter
96
+ )
97
+ return true
98
+ }
99
+
100
+ return false
101
+ }
102
+
103
+ Console.debug("config", config)
104
+
105
+ Console.info(" Checking if the config file is fine...")
106
+ // These two lines check if the 'slug' property is inside the configuration object.
107
+ Console.debug(
108
+ "Checking if the slug property is inside the configuration object..."
109
+ )
110
+ // check if the slug property is in the configuration object
111
+ if (!config!.config?.slug)
112
+ errors.push({
113
+ exercise: undefined,
114
+ msg: "The slug property is not in the configuration object",
115
+ })
116
+ // check if the duration property is in the configuration object
117
+ if (!config!.config?.duration)
118
+ warnings.push({
119
+ exercise: undefined,
120
+ msg: "The duration property is not in the configuration object",
121
+ })
122
+ // check if the difficulty property is in the configuration object
123
+ if (!config!.config?.difficulty)
124
+ warnings.push({
125
+ exercise: undefined,
126
+ msg: "The difficulty property is not in the configuration object",
127
+ })
128
+ // check if the bugs_link property is in the configuration object
129
+ if (!config!.config?.bugsLink)
130
+ errors.push({
131
+ exercise: undefined,
132
+ msg: "The bugsLink property is not in the configuration object",
133
+ })
134
+ // check if the video_solutions property is in the configuration object
135
+ if (config!.config?.videoSolutions === undefined)
136
+ warnings.push({
137
+ exercise: undefined,
138
+ msg: "The videoSolutions property is not in the configuration object",
139
+ })
140
+
141
+ // These two lines check if the 'repository' property is inside the configuration object.
142
+ Console.debug(
143
+ "Checking if the repository property is inside the configuration object..."
144
+ )
145
+ if (!config!.config?.repository)
146
+ errors.push({
147
+ exercise: undefined,
148
+ msg: "The repository property is not in the configuration object",
149
+ })
150
+ else
151
+ Audit.isUrl(config!.config?.repository, errors, counter)
152
+
153
+ // These two lines check if the 'description' property is inside the configuration object.
154
+ Console.debug(
155
+ "Checking if the description property is inside the configuration object..."
156
+ )
157
+ if (!config!.config?.description)
158
+ errors.push({
159
+ exercise: undefined,
160
+ msg: "The description property is not in the configuration object",
161
+ })
162
+ if (!config!.config?.projectType)
163
+ errors.push({
164
+ exercise: undefined,
165
+ msg: "The projectType property mandatory in the configuration object",
166
+ })
167
+
168
+ if (errors.length === 0)
169
+ Console.log("The config file is ok")
170
+
171
+ // Validates if images and links are working at every README file.
172
+ const exercises = config!.exercises
173
+ const readmeFiles = []
174
+
175
+ if (exercises && exercises.length > 0) {
176
+ Console.info(" Checking if the images are working...")
177
+ for (const index in exercises) {
178
+ if (Object.prototype.hasOwnProperty.call(exercises, index)) {
179
+ const exercise = exercises[index]
180
+ if (!validateExerciseDirectoryName(exercise.title))
181
+ errors.push({
182
+ exercise: exercise.title,
183
+ msg: `The exercise ${exercise.title} has an invalid name.`,
184
+ })
185
+ let readmeFilesCount = { exercise: exercise.title, count: 0 }
186
+ if (Object.keys(exercise.translations!).length === 0)
187
+ errors.push({
188
+ exercise: exercise.title,
189
+ msg: `The exercise ${exercise.title} doesn't have a README.md file.`,
190
+ })
191
+
192
+ if (
193
+ exercise.language === "python3" ||
194
+ exercise.language === "python"
195
+ ) {
196
+ for (const f of exercise.files.map(f => f)) {
197
+ if (
198
+ f.path.includes("test.py") ||
199
+ f.path.includes("tests.py")
200
+ ) {
201
+ const content = fs.readFileSync(f.path).toString()
202
+ const isEmpty = Audit.checkForEmptySpaces(content)
203
+ if (isEmpty || !content)
204
+ errors.push({
205
+ exercise: exercise.title,
206
+ msg: `This file (${f.name}) doesn't have any content inside.`,
207
+ })
208
+ }
209
+ }
210
+ } else {
211
+ for (const f of exercise.files.map(f => f)) {
212
+ if (
213
+ f.path.includes("test.js") ||
214
+ f.path.includes("tests.js")
215
+ ) {
216
+ const content = fs.readFileSync(f.path).toString()
217
+ const isEmpty: boolean = Audit.checkForEmptySpaces(content)
218
+ if (isEmpty || !content)
219
+ errors.push({
220
+ exercise: exercise.title,
221
+ msg: `This file (${f.name}) doesn't have any content inside.`,
222
+ })
223
+ }
224
+ }
225
+ }
226
+
227
+ for (const lang in exercise.translations) {
228
+ if (
229
+ Object.prototype.hasOwnProperty.call(
230
+ exercise.translations,
231
+ lang
232
+ )
233
+ ) {
234
+ const files: any[] = []
235
+ const findResultPromises = []
236
+ for (const file of exercise.files) {
237
+ const found = find(
238
+ file,
239
+ exercise.translations[lang],
240
+ exercise
241
+ )
242
+ findResultPromises.push(found)
243
+ }
244
+ // eslint-disable-next-line
245
+ let findResults = await Promise.all(findResultPromises);
246
+ for (const found of findResults) {
247
+ if (found) {
248
+ readmeFilesCount = {
249
+ ...readmeFilesCount,
250
+ count: readmeFilesCount.count + 1,
251
+ }
252
+ files.push(found)
253
+ }
254
+ }
255
+
256
+ if (!files.includes(true))
257
+ errors.push({
258
+ exercise: exercise.title,
259
+ msg: "This exercise doesn't have a README.md file.",
260
+ })
261
+ }
262
+ }
263
+
264
+ readmeFiles.push(readmeFilesCount)
265
+ }
266
+ }
267
+ } else
268
+ errors.push({
269
+ exercise: undefined,
270
+ msg: "The exercises array is empty.",
271
+ })
272
+
273
+ Console.log(
274
+ `${counter.images.total - counter.images.error} images ok from ${
275
+ counter.images.total
276
+ }`
277
+ )
278
+
279
+ Console.info(
280
+ " Checking if important files are missing... (README's, translations, gitignore...)"
281
+ )
282
+ // 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.
283
+ const files: string[] = []
284
+ let count = 0
285
+ for (const item of readmeFiles) {
286
+ if (count < item.count)
287
+ count = item.count
288
+ }
289
+
290
+ for (const item of readmeFiles) {
291
+ if (item.count !== count)
292
+ files.push(` ${item.exercise}`)
293
+ }
294
+
295
+ if (files.length > 0) {
296
+ const filesString: string = files.join(",")
297
+ warnings.push({
298
+ exercise: undefined,
299
+ msg:
300
+ files.length === 1 ?
301
+ `This exercise is missing translations:${filesString}` :
302
+ `These exercises are missing translations:${filesString}`,
303
+ })
304
+ }
305
+
306
+ // Checks if the .gitignore file exists.
307
+ if (!fs.existsSync(".gitignore"))
308
+ warnings.push({
309
+ exercise: undefined,
310
+ msg: ".gitignore file doesn't exist",
311
+ })
312
+
313
+ counter.exercises = exercises!.length
314
+ for (const readme of readmeFiles) {
315
+ counter.readmeFiles += readme.count
316
+ }
317
+ } else {
318
+ // This is the audit code for Projects
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