@learnpack/learnpack 2.1.19 → 2.1.24

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,612 +1,418 @@
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, ISchemaItem } 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
- // 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
- if (config?.config?.projectType === "tutorial") {
35
- const counter: ICounter = {
36
- images: {
37
- error: 0,
38
- total: 0,
39
- },
40
- links: {
41
- error: 0,
42
- total: 0,
43
- },
44
- exercises: 0,
45
- readmeFiles: 0,
46
- };
47
-
48
- // Checks if learnpack clean has been run
49
- Audit.checkLearnpackClean(config, errors);
50
-
51
- // Build exercises if they are not built yet.
52
- this.configManager?.buildIndex();
53
- config = this.configManager?.get();
54
-
55
- // Check if the exercises folder has some files within any ./exercise
56
- const exercisesPath: string = config!.config!.exercisesPath;
57
-
58
- fs.readdir(exercisesPath, (err, files) => {
59
- if (err) {
60
- return console.log("Unable to scan directory: " + err);
61
- }
62
-
63
- // listing all files using forEach
64
- for (const file of files) {
65
- // Do whatever you want to do with the file
66
- const filePath: string = path.join(exercisesPath, file);
67
- if (fs.statSync(filePath).isFile())
68
- warnings.push({
69
- exercise: file!,
70
- msg: "This file is not inside any exercise folder.",
71
- });
72
- }
73
- });
74
-
75
- // This function checks that each of the url's are working.
76
- const checkUrl = async (file: IFile, exercise: IExercise) => {
77
- if (!fs.existsSync(file.path))
78
- return false;
79
- const content: string = fs.readFileSync(file.path).toString();
80
- const isEmpty = Audit.checkForEmptySpaces(content);
81
- if (isEmpty || !content)
82
- errors.push({
83
- exercise: exercise.title!,
84
- msg: `This file (${file.name}) doesn't have any content inside.`,
85
- });
86
-
87
- const frontmatter: IFrontmatter = fm(content);
88
- for (const attribute in frontmatter.attributes) {
89
- if (
90
- Object.prototype.hasOwnProperty.call(
91
- frontmatter.attributes,
92
- attribute
93
- ) &&
94
- (attribute === "intro" || attribute === "tutorial")
95
- ) {
96
- counter.links.total++;
97
- try {
98
- // eslint-disable-next-line
99
- let res = await fetch(frontmatter.attributes[attribute], {
100
- method: "HEAD",
101
- });
102
- if (!res.ok) {
103
- counter.links.error++;
104
- errors.push({
105
- exercise: exercise.title!,
106
- msg: `This link is broken (${res.ok}): ${frontmatter.attributes[attribute]}`,
107
- });
108
- }
109
- } catch {
110
- counter.links.error++;
111
- errors.push({
112
- exercise: exercise.title,
113
- msg: `This link is broken: ${frontmatter.attributes[attribute]}`,
114
- });
115
- }
116
- }
117
- }
118
-
119
- // Check url's of each README file.
120
- const findings: IFindings = Audit.findInFile(
121
- ["relativeImages", "externalImages", "markdownLinks"],
122
- content
123
- );
124
- type findingsType =
125
- | "relativeImages"
126
- | "externalImages"
127
- | "markdownLinks"
128
- | "url"
129
- | "uploadcare";
130
- for (const finding in findings) {
131
- if (Object.prototype.hasOwnProperty.call(findings, finding)) {
132
- const obj = findings[finding as findingsType];
133
- // Valdites all the relative path images.
134
- if (
135
- finding === "relativeImages" &&
136
- Object.keys(obj!).length > 0
137
- ) {
138
- for (const img in obj) {
139
- if (Object.prototype.hasOwnProperty.call(obj, img)) {
140
- // Validates if the image is in the assets folder.
141
- counter.images.total++;
142
- const relativePath = path
143
- .relative(
144
- exercise.path.replace(/\\/gm, "/"),
145
- `${config!.config?.dirPath}/assets/${obj[img].relUrl}`
146
- )
147
- .replace(/\\/gm, "/");
148
- if (relativePath !== obj[img].absUrl.split("?").shift()) {
149
- counter.images.error++;
150
- errors.push({
151
- exercise: exercise.title,
152
- msg: `This relative path (${obj[img].relUrl}) is not pointing to the assets folder.`,
153
- });
154
- }
155
-
156
- if (
157
- !fs.existsSync(
158
- `${config!.config?.dirPath}/assets/${obj[img].relUrl}`
159
- )
160
- ) {
161
- counter.images.error++;
162
- errors.push({
163
- exercise: exercise.title,
164
- msg: `The file ${obj[img].relUrl} doesn't exist in the assets folder.`,
165
- });
166
- }
167
- }
168
- }
169
- } else if (
170
- finding === "externalImages" &&
171
- Object.keys(obj!).length > 0
172
- ) {
173
- // Valdites all the aboslute path images.
174
- for (const img in obj) {
175
- if (Object.prototype.hasOwnProperty.call(obj, img)) {
176
- counter.images.total++;
177
- if (
178
- fs.existsSync(
179
- `${config!.config?.dirPath}/assets${obj[img].mdUrl
180
- .split("?")
181
- .shift()}`
182
- )
183
- ) {
184
- const relativePath = path
185
- .relative(
186
- exercise.path.replace(/\\/gm, "/"),
187
- `${config!.config?.dirPath}/assets/${obj[img].mdUrl}`
188
- )
189
- .replace(/\\/gm, "/");
190
- warnings.push({
191
- exercise: exercise.title,
192
- 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}".`,
193
- });
194
- }
195
-
196
- try {
197
- // eslint-disable-next-line
198
- let res = await fetch(obj[img].absUrl, {
199
- method: "HEAD",
200
- });
201
- if (!res.ok) {
202
- counter.images.error++;
203
- errors.push({
204
- exercise: exercise.title,
205
- msg: `This link is broken: ${obj[img].absUrl}`,
206
- });
207
- }
208
- } catch {
209
- counter.images.error++;
210
- errors.push({
211
- exercise: exercise.title,
212
- msg: `This link is broken: ${obj[img].absUrl}`,
213
- });
214
- }
215
- }
216
- }
217
- } else if (
218
- finding === "markdownLinks" &&
219
- Object.keys(obj!).length > 0
220
- ) {
221
- for (const link in obj) {
222
- if (Object.prototype.hasOwnProperty.call(obj, link)) {
223
- counter.links.total++;
224
- try {
225
- // eslint-disable-next-line
226
- let res = await fetch(obj[link].mdUrl, {
227
- method: "HEAD",
228
- });
229
- if (res.status > 399 && res.status < 500) {
230
- Console.log(
231
- "Response links:",
232
- res.status,
233
- obj[link].mdUrl,
234
- res
235
- );
236
- counter.links.error++;
237
- errors.push({
238
- exercise: exercise.title,
239
- msg: `This link is broken: ${obj[link].mdUrl}`,
240
- });
241
- }
242
- } catch {
243
- counter.links.error++;
244
- errors.push({
245
- exercise: exercise.title,
246
- msg: `This link is broken: ${obj[link].mdUrl}`,
247
- });
248
- }
249
- }
250
- }
251
- }
252
- }
253
- }
254
-
255
- return true;
256
- };
257
-
258
- // This function is being created because the find method doesn't work with promises.
259
- const find = async (file: IFile, lang: string, exercise: IExercise) => {
260
- if (file.name === lang) {
261
- await checkUrl(file, exercise);
262
- return true;
263
- }
264
-
265
- return false;
266
- };
267
-
268
- Console.debug("config", config);
269
-
270
- Console.info(" Checking if the config file is fine...");
271
- // These two lines check if the 'slug' property is inside the configuration object.
272
- Console.debug(
273
- "Checking if the slug property is inside the configuration object..."
274
- );
275
- if (!config!.config?.slug)
276
- errors.push({
277
- exercise: undefined,
278
- msg: "The slug property is not in the configuration object",
279
- });
280
-
281
- // These two lines check if the 'repository' property is inside the configuration object.
282
- Console.debug(
283
- "Checking if the repository property is inside the configuration object..."
284
- );
285
- if (!config!.config?.repository)
286
- errors.push({
287
- exercise: undefined,
288
- msg: "The repository property is not in the configuration object",
289
- });
290
- else
291
- Audit.isUrl(config!.config?.repository, errors, counter);
292
-
293
- // These two lines check if the 'description' property is inside the configuration object.
294
- Console.debug(
295
- "Checking if the description property is inside the configuration object..."
296
- );
297
- if (!config!.config?.description)
298
- errors.push({
299
- exercise: undefined,
300
- msg: "The description property is not in the configuration object",
301
- });
302
-
303
- if (errors.length === 0)
304
- Console.log("The config file is ok");
305
-
306
- // Validates if images and links are working at every README file.
307
- const exercises = config!.exercises;
308
- const readmeFiles = [];
309
-
310
- if (exercises && exercises.length > 0) {
311
- Console.info(" Checking if the images are working...");
312
- for (const index in exercises) {
313
- if (Object.prototype.hasOwnProperty.call(exercises, index)) {
314
- const exercise = exercises[index];
315
- if (!validateExerciseDirectoryName(exercise.title))
316
- errors.push({
317
- exercise: exercise.title,
318
- msg: `The exercise ${exercise.title} has an invalid name.`,
319
- });
320
- let readmeFilesCount = { exercise: exercise.title, count: 0 };
321
- if (Object.keys(exercise.translations!).length === 0)
322
- errors.push({
323
- exercise: exercise.title,
324
- msg: `The exercise ${exercise.title} doesn't have a README.md file.`,
325
- });
326
-
327
- if (
328
- exercise.language === "python3" ||
329
- exercise.language === "python"
330
- ) {
331
- for (const f of exercise.files.map(f => f)) {
332
- if (
333
- f.path.includes("test.py") ||
334
- f.path.includes("tests.py")
335
- ) {
336
- const content = fs.readFileSync(f.path).toString();
337
- const isEmpty = Audit.checkForEmptySpaces(content);
338
- if (isEmpty || !content)
339
- errors.push({
340
- exercise: exercise.title,
341
- msg: `This file (${f.name}) doesn't have any content inside.`,
342
- });
343
- }
344
- }
345
- } else {
346
- for (const f of exercise.files.map(f => f)) {
347
- if (
348
- f.path.includes("test.js") ||
349
- f.path.includes("tests.js")
350
- ) {
351
- const content = fs.readFileSync(f.path).toString();
352
- const isEmpty: boolean = Audit.checkForEmptySpaces(content);
353
- if (isEmpty || !content)
354
- errors.push({
355
- exercise: exercise.title,
356
- msg: `This file (${f.name}) doesn't have any content inside.`,
357
- });
358
- }
359
- }
360
- }
361
-
362
- for (const lang in exercise.translations) {
363
- if (
364
- Object.prototype.hasOwnProperty.call(
365
- exercise.translations,
366
- lang
367
- )
368
- ) {
369
- const files: any[] = [];
370
- const findResultPromises = [];
371
- for (const file of exercise.files) {
372
- const found = find(
373
- file,
374
- exercise.translations[lang],
375
- exercise
376
- );
377
- findResultPromises.push(found);
378
- }
379
- // eslint-disable-next-line
380
- let findResults = await Promise.all(findResultPromises);
381
- for (const found of findResults) {
382
- if (found) {
383
- readmeFilesCount = {
384
- ...readmeFilesCount,
385
- count: readmeFilesCount.count + 1,
386
- };
387
- files.push(found);
388
- }
389
- }
390
-
391
- if (!files.includes(true))
392
- errors.push({
393
- exercise: exercise.title,
394
- msg: "This exercise doesn't have a README.md file.",
395
- });
396
- }
397
- }
398
-
399
- readmeFiles.push(readmeFilesCount);
400
- }
401
- }
402
- } else
403
- errors.push({
404
- exercise: undefined,
405
- msg: "The exercises array is empty.",
406
- });
407
-
408
- Console.log(
409
- `${counter.images.total - counter.images.error} images ok from ${
410
- counter.images.total
411
- }`
412
- );
413
-
414
- Console.info(
415
- " Checking if important files are missing... (README's, translations, gitignore...)"
416
- );
417
- // 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.
418
- const files: string[] = [];
419
- let count = 0;
420
- for (const item of readmeFiles) {
421
- if (count < item.count)
422
- count = item.count;
423
- }
424
-
425
- for (const item of readmeFiles) {
426
- if (item.count !== count)
427
- files.push(` ${item.exercise}`);
428
- }
429
-
430
- if (files.length > 0) {
431
- const filesString: string = files.join(",");
432
- warnings.push({
433
- exercise: undefined,
434
- msg:
435
- files.length === 1 ?
436
- `This exercise is missing translations:${filesString}` :
437
- `These exercises are missing translations:${filesString}`,
438
- });
439
- }
440
-
441
- // Checks if the .gitignore file exists.
442
- if (!fs.existsSync(".gitignore"))
443
- warnings.push({
444
- exercise: undefined,
445
- msg: ".gitignore file doesn't exist",
446
- });
447
-
448
- counter.exercises = exercises!.length;
449
- for (const readme of readmeFiles) {
450
- counter.readmeFiles += readme.count;
451
- }
452
-
453
- await Audit.showWarnings(warnings);
454
- await Audit.showErrors(errors, counter);
455
- } else {
456
- // This is the audit for Projects
457
-
458
- // Getting the learn.json schema
459
- const schemaResponse = await fetch(
460
- "https://raw.githubusercontent.com/tommygonzaleza/project-template/main/.github/learn-schema.json"
461
- );
462
- const schema = await schemaResponse.json();
463
-
464
- // Checking the "learn.json" file:
465
- const learnjson = JSON.parse(
466
- fs.readFileSync("./learn.json").toString()
467
- );
468
-
469
- if (!learnjson) {
470
- Console.error(
471
- "There is no learn.json file located in the root of the project."
472
- );
473
- process.exit(1);
474
- }
475
-
476
- // Checkimg the README.md file
477
- const readme = fs.readFileSync("./README.md").toString();
478
- if (!readme)
479
- errors.push({
480
- exercise: undefined,
481
- msg: 'There is no "README.md" located in the root of the project.',
482
- });
483
-
484
- if (readme.length < 800)
485
- errors.push({
486
- exercise: undefined,
487
- msg: `The "README.md" file should have at least 800 characters (It currently have: ${readme.length}).`,
488
- });
489
-
490
- // Checking if the preview image (from the learn.json) is OK.
491
- try {
492
- const res = await fetch(learnjson.preview, { method: "HEAD" });
493
- if (!res.ok) {
494
- errors.push({
495
- exercise: undefined,
496
- msg: `The link of the "preview" is broken: ${learnjson.preview}`,
497
- });
498
- }
499
- } catch {
500
- errors.push({
501
- exercise: undefined,
502
- msg: `The link of the "preview" is broken: ${learnjson.preview}`,
503
- });
504
- }
505
-
506
- // Checking each of the schema rules that are mandatory.
507
- for (const schemaItem of schema) {
508
- const learnItem = learnjson[schemaItem.key];
509
-
510
- if (schemaItem.mandatory) {
511
- Console.info(`Checking for the "${schemaItem.key}" property...`);
512
-
513
- if (!learnItem) {
514
- errors.push({
515
- exercise: undefined,
516
- msg: `learn.json missing "${schemaItem.key}" mandatory property.`,
517
- });
518
- return;
519
- }
520
-
521
- if (schemaItem.max_size && learnItem.length > schemaItem.max_size)
522
- errors.push({
523
- exercise: undefined,
524
- msg: `The "${schemaItem.key}" property should have a maximum size of ${schemaItem.max_size}`,
525
- });
526
-
527
- if (schemaItem.enum) {
528
- if (typeof learnItem === "object") {
529
- let valid = true;
530
- for (const ele of learnItem) {
531
- if (!schemaItem.enum!.includes(ele))
532
- valid = false;
533
- }
534
-
535
- if (!valid)
536
- errors.push({
537
- exercise: undefined,
538
- msg: `The "${
539
- schemaItem.key
540
- }" property (current: ${learnItem}) should be one of the following values: ${schemaItem.enum.join(
541
- ", "
542
- )}.`,
543
- });
544
- } else if (!schemaItem.enum.includes(learnItem.toLowerCase()))
545
- errors.push({
546
- exercise: undefined,
547
- msg: `The "${
548
- schemaItem.key
549
- }" property (current: ${learnItem}) should be one of the following values: ${schemaItem.enum.join(
550
- ", "
551
- )}.`,
552
- });
553
- }
554
-
555
- if (schemaItem.type === "url" && schemaItem.allowed_extensions) {
556
- let valid = false;
557
- for (const ele of schemaItem.allowed_extensions) {
558
- if (learnItem.split(".").includes(ele))
559
- valid = true;
560
- }
561
-
562
- if (!valid)
563
- errors.push({
564
- exercise: undefined,
565
- msg: `The "${
566
- schemaItem.key
567
- }" property should have one of the allowed extensions: ${schemaItem.allowed_extensions.join(
568
- ", "
569
- )}.`,
570
- });
571
- }
572
-
573
- if (
574
- schemaItem.max_item_size &&
575
- learnItem.length > schemaItem.max_item_size
576
- )
577
- errors.push({
578
- exercise: undefined,
579
- msg: `The "${schemaItem.key}" property has more items than allowed (${schemaItem.max_item_size}).`,
580
- });
581
- }
582
- }
583
-
584
- /* eslint-disable-next-line */
585
- await Audit.showErrors(errors, undefined);
586
- }
587
- }
588
- }
589
- }
590
-
591
- AuditCommand.description = `learnpack audit is the command in charge of creating an auditory of the repository
592
- ...
593
- learnpack audit checks for the following information in a repository:
594
- 1. The configuration object has slug, repository and description. (Error)
595
- 2. The command learnpack clean has been run. (Error)
596
- 3. If a markdown or test file doesn't have any content. (Error)
597
- 4. The links are accessing to valid servers. (Error)
598
- 5. The relative images are working (If they have the shortest path to the image or if the images exists in the assets). (Error)
599
- 6. The external images are working (If they are pointing to a valid server). (Error)
600
- 7. The exercises directory names are valid. (Error)
601
- 8. If an exercise doesn't have a README file. (Error)
602
- 9. The exercises array (Of the config file) has content. (Error)
603
- 10. The exercses have the same translations. (Warning)
604
- 11. The .gitignore file exists. (Warning)
605
- 12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)
606
- `;
607
-
608
- AuditCommand.flags = {
609
- // name: flags.string({char: 'n', description: 'name to print'}),
610
- };
611
-
612
- 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
+ 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;