@learnpack/learnpack 2.1.39 → 2.1.40

Sign up to get free protection for your applications and to get access to all the features.
Files changed (196) hide show
  1. package/README.md +370 -35
  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 -342
  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 +239 -208
  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 +41 -41
  27. package/lib/managers/config/defaults.js +44 -44
  28. package/lib/managers/config/exercise.d.ts +36 -36
  29. package/lib/managers/config/exercise.js +236 -236
  30. package/lib/managers/config/index.d.ts +3 -3
  31. package/lib/managers/config/index.js +337 -337
  32. package/lib/managers/file.d.ts +14 -14
  33. package/lib/managers/file.js +153 -153
  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 -58
  38. package/lib/managers/server/routes.d.ts +4 -4
  39. package/lib/managers/server/routes.js +219 -219
  40. package/lib/managers/session.d.ts +3 -3
  41. package/lib/managers/session.js +125 -125
  42. package/lib/managers/socket.d.ts +3 -3
  43. package/lib/managers/socket.js +176 -176
  44. package/lib/managers/telemetry.d.ts +74 -74
  45. package/lib/managers/telemetry.js +206 -206
  46. package/lib/managers/test.js +84 -84
  47. package/lib/models/action.d.ts +2 -2
  48. package/lib/models/action.js +2 -2
  49. package/lib/models/audit.d.ts +15 -15
  50. package/lib/models/audit.js +2 -2
  51. package/lib/models/config-manager.d.ts +21 -21
  52. package/lib/models/config-manager.js +2 -2
  53. package/lib/models/config.d.ts +68 -67
  54. package/lib/models/config.js +2 -2
  55. package/lib/models/counter.d.ts +11 -11
  56. package/lib/models/counter.js +2 -2
  57. package/lib/models/errors.d.ts +15 -15
  58. package/lib/models/errors.js +2 -2
  59. package/lib/models/exercise-obj.d.ts +30 -30
  60. package/lib/models/exercise-obj.js +2 -2
  61. package/lib/models/file.d.ts +5 -5
  62. package/lib/models/file.js +2 -2
  63. package/lib/models/findings.d.ts +17 -17
  64. package/lib/models/findings.js +2 -2
  65. package/lib/models/flags.d.ts +10 -10
  66. package/lib/models/flags.js +2 -2
  67. package/lib/models/front-matter.d.ts +11 -11
  68. package/lib/models/front-matter.js +2 -2
  69. package/lib/models/gitpod-data.d.ts +16 -16
  70. package/lib/models/gitpod-data.js +2 -2
  71. package/lib/models/language.d.ts +4 -4
  72. package/lib/models/language.js +2 -2
  73. package/lib/models/package.d.ts +7 -7
  74. package/lib/models/package.js +2 -2
  75. package/lib/models/plugin-config.d.ts +16 -16
  76. package/lib/models/plugin-config.js +2 -2
  77. package/lib/models/session.d.ts +31 -31
  78. package/lib/models/session.js +2 -2
  79. package/lib/models/socket.d.ts +36 -32
  80. package/lib/models/socket.js +2 -2
  81. package/lib/models/status.d.ts +1 -1
  82. package/lib/models/status.js +2 -2
  83. package/lib/models/success-types.d.ts +1 -1
  84. package/lib/models/success-types.js +2 -2
  85. package/lib/plugin/command/compile.d.ts +6 -6
  86. package/lib/plugin/command/compile.js +18 -18
  87. package/lib/plugin/command/test.d.ts +6 -6
  88. package/lib/plugin/command/test.js +25 -25
  89. package/lib/plugin/index.d.ts +27 -27
  90. package/lib/plugin/index.js +7 -7
  91. package/lib/plugin/plugin.d.ts +8 -8
  92. package/lib/plugin/plugin.js +68 -68
  93. package/lib/plugin/utils.d.ts +16 -16
  94. package/lib/plugin/utils.js +58 -58
  95. package/lib/ui/download.d.ts +5 -5
  96. package/lib/ui/download.js +61 -61
  97. package/lib/utils/BaseCommand.d.ts +8 -8
  98. package/lib/utils/BaseCommand.js +41 -41
  99. package/lib/utils/SessionCommand.d.ts +10 -10
  100. package/lib/utils/SessionCommand.js +43 -43
  101. package/lib/utils/api.d.ts +14 -14
  102. package/lib/utils/api.js +255 -255
  103. package/lib/utils/audit.d.ts +16 -16
  104. package/lib/utils/audit.js +303 -303
  105. package/lib/utils/checkNotInstalled.d.ts +2 -0
  106. package/lib/utils/checkNotInstalled.js +36 -0
  107. package/lib/utils/console.d.ts +12 -12
  108. package/lib/utils/console.js +19 -19
  109. package/lib/utils/errors.d.ts +17 -17
  110. package/lib/utils/errors.js +100 -100
  111. package/lib/utils/exercisesQueue.d.ts +9 -9
  112. package/lib/utils/exercisesQueue.js +38 -38
  113. package/lib/utils/fileQueue.d.ts +40 -40
  114. package/lib/utils/fileQueue.js +168 -168
  115. package/lib/utils/misc.d.ts +1 -1
  116. package/lib/utils/misc.js +23 -23
  117. package/lib/utils/osOperations.d.ts +5 -5
  118. package/lib/utils/osOperations.js +72 -72
  119. package/lib/utils/validators.d.ts +5 -5
  120. package/lib/utils/validators.js +17 -17
  121. package/lib/utils/watcher.d.ts +2 -2
  122. package/lib/utils/watcher.js +25 -25
  123. package/oclif.manifest.json +1 -1
  124. package/package.json +139 -139
  125. package/src/commands/audit.ts +443 -443
  126. package/src/commands/clean.ts +29 -29
  127. package/src/commands/download.ts +61 -61
  128. package/src/commands/init.ts +170 -170
  129. package/src/commands/login.ts +42 -42
  130. package/src/commands/logout.ts +43 -43
  131. package/src/commands/publish.ts +107 -107
  132. package/src/commands/start.ts +53 -23
  133. package/src/commands/test.ts +85 -85
  134. package/src/index.ts +1 -1
  135. package/src/managers/config/allowed_files.ts +29 -29
  136. package/src/managers/config/defaults.ts +42 -42
  137. package/src/managers/config/exercise.ts +311 -311
  138. package/src/managers/config/index.ts +455 -455
  139. package/src/managers/file.ts +196 -196
  140. package/src/managers/gitpod.ts +84 -84
  141. package/src/managers/server/index.ts +78 -78
  142. package/src/managers/server/routes.ts +330 -330
  143. package/src/managers/session.ts +145 -145
  144. package/src/managers/socket.ts +250 -250
  145. package/src/managers/telemetry.ts +346 -346
  146. package/src/managers/test.ts +83 -83
  147. package/src/models/action.ts +10 -10
  148. package/src/models/audit.ts +16 -16
  149. package/src/models/config-manager.ts +23 -23
  150. package/src/models/config.ts +5 -3
  151. package/src/models/counter.ts +11 -11
  152. package/src/models/errors.ts +22 -22
  153. package/src/models/exercise-obj.ts +29 -29
  154. package/src/models/file.ts +5 -5
  155. package/src/models/findings.ts +18 -18
  156. package/src/models/flags.ts +10 -10
  157. package/src/models/front-matter.ts +11 -11
  158. package/src/models/gitpod-data.ts +19 -19
  159. package/src/models/language.ts +4 -4
  160. package/src/models/package.ts +7 -7
  161. package/src/models/plugin-config.ts +17 -17
  162. package/src/models/session.ts +34 -34
  163. package/src/models/socket.ts +5 -0
  164. package/src/models/status.ts +16 -16
  165. package/src/models/success-types.ts +1 -1
  166. package/src/plugin/command/compile.ts +17 -17
  167. package/src/plugin/command/test.ts +30 -30
  168. package/src/plugin/index.ts +6 -6
  169. package/src/plugin/plugin.ts +94 -94
  170. package/src/plugin/utils.ts +87 -87
  171. package/src/types/node-fetch.d.ts +1 -1
  172. package/src/ui/download.ts +71 -71
  173. package/src/utils/BaseCommand.ts +48 -48
  174. package/src/utils/SessionCommand.ts +43 -43
  175. package/src/utils/api.ts +303 -303
  176. package/src/utils/audit.ts +393 -393
  177. package/src/utils/checkNotInstalled.ts +46 -0
  178. package/src/utils/console.ts +24 -24
  179. package/src/utils/errors.ts +117 -117
  180. package/src/utils/exercisesQueue.ts +51 -51
  181. package/src/utils/fileQueue.ts +198 -198
  182. package/src/utils/misc.ts +23 -23
  183. package/src/utils/osOperations.ts +79 -79
  184. package/src/utils/templates/gitignore.txt +19 -19
  185. package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.es.md +24 -24
  186. package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.md +24 -24
  187. package/src/utils/templates/incremental/README.ejs +4 -4
  188. package/src/utils/templates/incremental/README.es.ejs +4 -4
  189. package/src/utils/templates/isolated/01-hello-world/README.es.md +26 -26
  190. package/src/utils/templates/isolated/01-hello-world/README.md +26 -26
  191. package/src/utils/templates/isolated/README.ejs +4 -4
  192. package/src/utils/templates/isolated/README.es.ejs +4 -4
  193. package/src/utils/templates/no-grading/README.ejs +4 -4
  194. package/src/utils/templates/no-grading/README.es.ejs +4 -4
  195. package/src/utils/validators.ts +18 -18
  196. package/src/utils/watcher.ts +27 -27
@@ -1,443 +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
- // 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
+
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