@learnpack/learnpack 2.1.24 → 2.1.26

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