@learnpack/learnpack 2.1.18 → 2.1.19

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
  }