@learnpack/learnpack 2.1.20 → 2.1.23
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +10 -10
- package/lib/commands/audit.js +62 -220
- package/lib/utils/audit.d.ts +3 -0
- package/lib/utils/audit.js +279 -116
- package/oclif.manifest.json +1 -1
- package/package.json +2 -2
- package/src/commands/audit.ts +87 -281
- package/src/utils/audit.ts +395 -181
package/src/commands/audit.ts
CHANGED
@@ -7,14 +7,12 @@ 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
|
10
|
+
import { IAuditErrors } from "../models/audit";
|
11
11
|
import { ICounter } from "../models/counter";
|
12
12
|
import { IFindings } from "../models/findings";
|
13
13
|
|
14
14
|
// eslint-disable-next-line
|
15
15
|
const fetch = require("node-fetch");
|
16
|
-
// eslint-disable-next-line
|
17
|
-
const fm = require("front-matter");
|
18
16
|
|
19
17
|
class AuditCommand extends SessionCommand {
|
20
18
|
async init() {
|
@@ -72,193 +70,18 @@ class AuditCommand extends SessionCommand {
|
|
72
70
|
}
|
73
71
|
});
|
74
72
|
|
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
73
|
// This function is being created because the find method doesn't work with promises.
|
259
74
|
const find = async (file: IFile, lang: string, exercise: IExercise) => {
|
260
75
|
if (file.name === lang) {
|
261
|
-
await checkUrl(
|
76
|
+
await Audit.checkUrl(
|
77
|
+
config!,
|
78
|
+
file.path,
|
79
|
+
file.name,
|
80
|
+
exercise,
|
81
|
+
errors,
|
82
|
+
warnings,
|
83
|
+
counter
|
84
|
+
);
|
262
85
|
return true;
|
263
86
|
}
|
264
87
|
|
@@ -449,11 +272,8 @@ files.push(` ${item.exercise}`);
|
|
449
272
|
for (const readme of readmeFiles) {
|
450
273
|
counter.readmeFiles += readme.count;
|
451
274
|
}
|
452
|
-
|
453
|
-
await Audit.showWarnings(warnings);
|
454
|
-
await Audit.showErrors(errors, counter);
|
455
275
|
} else {
|
456
|
-
// This is the audit for Projects
|
276
|
+
// This is the audit code for Projects
|
457
277
|
|
458
278
|
// Getting the learn.json schema
|
459
279
|
const schemaResponse = await fetch(
|
@@ -473,19 +293,69 @@ files.push(` ${item.exercise}`);
|
|
473
293
|
process.exit(1);
|
474
294
|
}
|
475
295
|
|
476
|
-
//
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
exercise: undefined,
|
481
|
-
msg: 'There is no "README.md" located in the root of the project.',
|
482
|
-
});
|
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/;
|
483
300
|
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
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;
|
489
359
|
|
490
360
|
// Checking if the preview image (from the learn.json) is OK.
|
491
361
|
try {
|
@@ -503,87 +373,23 @@ files.push(` ${item.exercise}`);
|
|
503
373
|
});
|
504
374
|
}
|
505
375
|
|
506
|
-
|
507
|
-
|
508
|
-
const learnItem = learnjson[schemaItem.key];
|
376
|
+
const date = new Date();
|
377
|
+
learnjson.validationAt = date.getTime();
|
509
378
|
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
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
|
-
}
|
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";
|
583
385
|
|
584
|
-
|
585
|
-
await
|
386
|
+
// Writes the "learn.json" file with all the new properties
|
387
|
+
await fs.promises.writeFile("./learn.json", JSON.stringify(learnjson));
|
586
388
|
}
|
389
|
+
|
390
|
+
await Audit.showWarnings(warnings);
|
391
|
+
// eslint-disable-next-line
|
392
|
+
await Audit.showErrors(errors, undefined);
|
587
393
|
}
|
588
394
|
}
|
589
395
|
}
|