@learnpack/learnpack 2.1.14 → 2.1.19

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