@learnpack/learnpack 2.1.1 → 2.1.2

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