@learnpack/learnpack 2.1.18 → 2.1.23

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.
@@ -14,6 +14,7 @@ export default {
14
14
  contact: "https://github.com/learnpack/learnpack/issues/new",
15
15
  language: "auto",
16
16
  autoPlay: true,
17
+ projectType: "tutorial", // [tutorial, project]
17
18
  grading: "isolated", // [isolated, incremental]
18
19
  exercisesPath: "./", // path to the folder that contains the exercises
19
20
  webpackTemplate: null, // if you want webpack to use an HTML template
@@ -0,0 +1,16 @@
1
+ export interface IAuditErrors {
2
+ exercise?: string;
3
+ msg: string;
4
+ }
5
+
6
+ type TType = "string" | "array" | "number" | "url" | "boolean";
7
+
8
+ export interface ISchemaItem {
9
+ key: string;
10
+ mandatory: boolean;
11
+ type: TType;
12
+ max_size?: number;
13
+ allowed_extensions?: string[];
14
+ enum?: string[];
15
+ max_item_size?: number;
16
+ }
@@ -55,6 +55,7 @@ export interface IConfig {
55
55
  disableGrading: boolean; // TODO: Deprecate
56
56
  actions: Array<string>; // TODO: Deprecate
57
57
  autoPlay: boolean;
58
+ projectType?: string;
58
59
  // TODO: nameExerciseValidation
59
60
  contact?: string;
60
61
  disabledActions?: Array<TConfigAction>;
@@ -1,162 +1,395 @@
1
- import {IAuditErrors} from '../models/audit-errors'
2
- import {IConfigObj} from '../models/config'
3
- import {ICounter} from '../models/counter'
4
- import {IFindings} from '../models/findings'
5
- import Console from './console'
1
+ import { IAuditErrors } from "../models/audit";
2
+ import { IConfigObj } from "../models/config";
3
+ import { ICounter } from "../models/counter";
4
+ import { IFindings } from "../models/findings";
5
+ import { IExercise } from "../models/exercise-obj";
6
+ import { IFrontmatter } from "../models/front-matter";
7
+ import Console from "./console";
8
+ import * as fs from "fs";
9
+ import * as path from "path";
6
10
 
7
11
  // eslint-disable-next-line
8
12
  const fetch = require("node-fetch");
9
- import * as fs from 'fs'
13
+ // eslint-disable-next-line
14
+ const fm = require("front-matter");
10
15
 
11
- export default {
12
- // This function checks if a url is valid.
13
- isUrl: async (url: string, errors: IAuditErrors[], counter: ICounter) => {
14
- const regexUrl = /(https?:\/\/[\w./-]+)/gm
15
- counter.links.total++
16
- if (!regexUrl.test(url)) {
17
- counter.links.error++
18
- errors.push({
19
- exercise: undefined,
20
- msg: `The repository value of the configuration file is not a link: ${url}`,
21
- })
22
- return false
23
- }
16
+ // This function checks if a url is valid.
17
+ const isUrl = async (
18
+ url: string,
19
+ errors: IAuditErrors[],
20
+ counter: ICounter
21
+ ) => {
22
+ const regexUrl = /(https?:\/\/[\w./-]+)/gm;
23
+ counter.links.total++;
24
+ if (!regexUrl.test(url)) {
25
+ counter.links.error++;
26
+ errors.push({
27
+ exercise: undefined,
28
+ msg: `The repository value of the configuration file is not a link: ${url}`,
29
+ });
30
+ return false;
31
+ }
32
+
33
+ const res = await fetch(url, { method: "HEAD" });
34
+ if (!res.ok) {
35
+ counter.links.error++;
36
+ errors.push({
37
+ exercise: undefined,
38
+ msg: `The link of the repository is broken: ${url}`,
39
+ });
40
+ }
41
+
42
+ return true;
43
+ };
24
44
 
25
- const res = await fetch(url, {method: 'HEAD'})
26
- if (!res.ok) {
27
- counter.links.error++
28
- errors.push({
29
- exercise: undefined,
30
- msg: `The link of the repository is broken: ${url}`,
31
- })
45
+ const checkForEmptySpaces = (str: string) => {
46
+ const isEmpty = true;
47
+ for (const letter of str) {
48
+ if (letter !== " ") {
49
+ return false;
32
50
  }
51
+ }
52
+
53
+ return isEmpty;
54
+ };
55
+
56
+ const checkLearnpackClean = (configObj: IConfigObj, errors: IAuditErrors[]) => {
57
+ if (
58
+ (configObj.config?.outputPath &&
59
+ fs.existsSync(configObj.config?.outputPath)) ||
60
+ fs.existsSync(`${configObj.config?.dirPath}/_app`) ||
61
+ fs.existsSync(`${configObj.config?.dirPath}/reports`) ||
62
+ fs.existsSync(`${configObj.config?.dirPath}/resets`) ||
63
+ fs.existsSync(`${configObj.config?.dirPath}/app.tar.gz`) ||
64
+ fs.existsSync(`${configObj.config?.dirPath}/config.json`) ||
65
+ fs.existsSync(`${configObj.config?.dirPath}/vscode_queue.json`)
66
+ ) {
67
+ errors.push({
68
+ exercise: undefined,
69
+ msg: "You have to run learnpack clean command",
70
+ });
71
+ }
72
+ };
73
+
74
+ const findInFile = (types: string[], content: string) => {
75
+ const regex: any = {
76
+ relativeImages:
77
+ /!\[.*]\s*\((((\.\/)?(\.{2}\/){1,5})(.*\/)*(.[^\s/]*\.[A-Za-z]{2,4})\S*)\)/gm,
78
+ externalImages: /!\[.*]\((https?:\/(\/[^)/]+)+\/?)\)/gm,
79
+ markdownLinks: /(\s)+\[.*]\((https?:\/(\/[^)/]+)+\/?)\)/gm,
80
+ url: /(https?:\/\/[\w./-]+)/gm,
81
+ uploadcare: /https:\/\/ucarecdn.com\/(?:.*\/)*([\w./-]+)/gm,
82
+ };
83
+
84
+ const validTypes = Object.keys(regex);
85
+ if (!Array.isArray(types))
86
+ types = [types];
87
+
88
+ const findings: IFindings = {};
89
+ type findingsType =
90
+ | "relativeImages"
91
+ | "externalImages"
92
+ | "markdownLinks"
93
+ | "url"
94
+ | "uploadcare";
33
95
 
34
- return true
35
- },
36
- checkForEmptySpaces: (str: string) => {
37
- const isEmpty = true
38
- for (const letter of str) {
39
- if (letter !== ' ') {
40
- return false
96
+ for (const type of types) {
97
+ if (!validTypes.includes(type))
98
+ throw new Error("Invalid type: " + type);
99
+ else
100
+ findings[type as findingsType] = {};
101
+ }
102
+
103
+ for (const type of types) {
104
+ let m: RegExpExecArray;
105
+ while ((m = regex[type].exec(content)) !== null) {
106
+ // This is necessary to avoid infinite loops with zero-width matches
107
+ if (m.index === regex.lastIndex) {
108
+ regex.lastIndex++;
41
109
  }
110
+
111
+ // The result can be accessed through the `m`-variable.
112
+ // m.forEach((match, groupIndex) => values.push(match));
113
+
114
+ findings[type as findingsType]![m[0]] = {
115
+ content: m[0],
116
+ absUrl: m[1],
117
+ mdUrl: m[2],
118
+ relUrl: m[6],
119
+ };
42
120
  }
121
+ }
122
+
123
+ return findings;
124
+ };
125
+
126
+ // This function checks that each of the url's are working.
127
+ const checkUrl = async (
128
+ config: IConfigObj,
129
+ filePath: string,
130
+ fileName: string,
131
+ exercise: IExercise | undefined,
132
+ errors: IAuditErrors[],
133
+ warnings: IAuditErrors[],
134
+ counter: ICounter | undefined
135
+ ) => {
136
+ if (!fs.existsSync(filePath))
137
+ return false;
138
+ const content: string = fs.readFileSync(filePath).toString();
139
+ const isEmpty = checkForEmptySpaces(content);
140
+ if (isEmpty || !content)
141
+ errors.push({
142
+ exercise: exercise?.title!,
143
+ msg: `This file (${fileName}) doesn't have any content inside.`,
144
+ });
43
145
 
44
- return isEmpty
45
- },
46
- checkLearnpackClean: (configObj: IConfigObj, errors: IAuditErrors[]) => {
146
+ const frontmatter: IFrontmatter = fm(content);
147
+ for (const attribute in frontmatter.attributes) {
47
148
  if (
48
- (configObj.config?.outputPath &&
49
- fs.existsSync(configObj.config?.outputPath)) ||
50
- fs.existsSync(`${configObj.config?.dirPath}/_app`) ||
51
- fs.existsSync(`${configObj.config?.dirPath}/reports`) ||
52
- fs.existsSync(`${configObj.config?.dirPath}/resets`) ||
53
- fs.existsSync(`${configObj.config?.dirPath}/app.tar.gz`) ||
54
- fs.existsSync(`${configObj.config?.dirPath}/config.json`) ||
55
- fs.existsSync(`${configObj.config?.dirPath}/vscode_queue.json`)
149
+ Object.prototype.hasOwnProperty.call(frontmatter.attributes, attribute) &&
150
+ (attribute === "intro" || attribute === "tutorial")
56
151
  ) {
57
- errors.push({
58
- exercise: undefined,
59
- msg: 'You have to run learnpack clean command',
60
- })
61
- }
62
- },
63
- findInFile: (types: string[], content: string) => {
64
- const regex: any = {
65
- relativeImages:
66
- /!\[.*]\s*\((((\.\/)?(\.{2}\/){1,5})(.*\/)*(.[^\s/]*\.[A-Za-z]{2,4})\S*)\)/gm,
67
- externalImages: /!\[.*]\((https?:\/(\/[^)/]+)+\/?)\)/gm,
68
- markdownLinks: /(\s)+\[.*]\((https?:\/(\/[^)/]+)+\/?)\)/gm,
69
- url: /(https?:\/\/[\w./-]+)/gm,
70
- uploadcare: /https:\/\/ucarecdn.com\/(?:.*\/)*([\w./-]+)/gm,
152
+ counter && counter.links.total++;
153
+ try {
154
+ // eslint-disable-next-line
155
+ let res = await fetch(frontmatter.attributes[attribute], {
156
+ method: "HEAD",
157
+ });
158
+ if (!res.ok) {
159
+ counter && counter.links.error++;
160
+ errors.push({
161
+ exercise: exercise?.title!,
162
+ msg: `This link is broken (${res.ok}): ${frontmatter.attributes[attribute]}`,
163
+ });
164
+ }
165
+ } catch {
166
+ counter && counter.links.error++;
167
+ errors.push({
168
+ exercise: exercise?.title,
169
+ msg: `This link is broken: ${frontmatter.attributes[attribute]}`,
170
+ });
171
+ }
71
172
  }
173
+ }
72
174
 
73
- const validTypes = Object.keys(regex)
74
- if (!Array.isArray(types))
75
- types = [types]
76
-
77
- const findings: IFindings = {}
78
- type findingsType =
79
- | 'relativeImages'
80
- | 'externalImages'
81
- | 'markdownLinks'
82
- | 'url'
83
- | 'uploadcare';
84
-
85
- for (const type of types) {
86
- if (!validTypes.includes(type))
87
- throw new Error('Invalid type: ' + type)
88
- else
89
- findings[type as findingsType] = {}
90
- }
175
+ // Check url's of each README file.
176
+ const findings: IFindings = findInFile(
177
+ ["relativeImages", "externalImages", "markdownLinks"],
178
+ content
179
+ );
180
+ type findingsType =
181
+ | "relativeImages"
182
+ | "externalImages"
183
+ | "markdownLinks"
184
+ | "url"
185
+ | "uploadcare";
186
+ for (const finding in findings) {
187
+ if (Object.prototype.hasOwnProperty.call(findings, finding)) {
188
+ const obj = findings[finding as findingsType];
189
+ // Valdites all the relative path images.
190
+ if (finding === "relativeImages" && Object.keys(obj!).length > 0) {
191
+ for (const img in obj) {
192
+ if (Object.prototype.hasOwnProperty.call(obj, img)) {
193
+ // Validates if the image is in the assets folder.
194
+ counter && counter.images.total++;
195
+ const relativePath = path
196
+ .relative(
197
+ exercise ? exercise.path.replace(/\\/gm, "/") : "./",
198
+ `${config!.config?.dirPath}/assets/${obj[img].relUrl}`
199
+ )
200
+ .replace(/\\/gm, "/");
201
+ if (relativePath !== obj[img].absUrl.split("?").shift()) {
202
+ counter && counter.images.error++;
203
+ errors.push({
204
+ exercise: exercise?.title,
205
+ msg: `This relative path (${obj[img].relUrl}) is not pointing to the assets folder.`,
206
+ });
207
+ }
91
208
 
92
- for (const type of types) {
93
- let m: RegExpExecArray
94
- while ((m = regex[type].exec(content)) !== null) {
95
- // This is necessary to avoid infinite loops with zero-width matches
96
- if (m.index === regex.lastIndex) {
97
- regex.lastIndex++
209
+ if (
210
+ !fs.existsSync(
211
+ `${config!.config?.dirPath}/assets/${obj[img].relUrl}`
212
+ )
213
+ ) {
214
+ counter && counter.images.error++;
215
+ errors.push({
216
+ exercise: exercise?.title,
217
+ msg: `The file ${obj[img].relUrl} doesn't exist in the assets folder.`,
218
+ });
219
+ }
220
+ }
98
221
  }
222
+ } else if (finding === "externalImages" && Object.keys(obj!).length > 0) {
223
+ // Valdites all the aboslute path images.
224
+ for (const img in obj) {
225
+ if (Object.prototype.hasOwnProperty.call(obj, img)) {
226
+ counter && counter.images.total++;
227
+ if (
228
+ fs.existsSync(
229
+ `${config!.config?.dirPath}/assets${obj[img].mdUrl
230
+ .split("?")
231
+ .shift()}`
232
+ )
233
+ ) {
234
+ const relativePath = path
235
+ .relative(
236
+ exercise ? exercise.path.replace(/\\/gm, "/") : "./",
237
+ `${config!.config?.dirPath}/assets/${obj[img].mdUrl}`
238
+ )
239
+ .replace(/\\/gm, "/");
240
+ warnings.push({
241
+ exercise: exercise?.title,
242
+ 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}".`,
243
+ });
244
+ }
99
245
 
100
- // The result can be accessed through the `m`-variable.
101
- // m.forEach((match, groupIndex) => values.push(match));
102
-
103
- findings[type as findingsType]![m[0]] = {
104
- content: m[0],
105
- absUrl: m[1],
106
- mdUrl: m[2],
107
- relUrl: m[6],
246
+ try {
247
+ // eslint-disable-next-line
248
+ let res = await fetch(obj[img].absUrl, {
249
+ method: "HEAD",
250
+ });
251
+ if (!res.ok) {
252
+ counter && counter.images.error++;
253
+ errors.push({
254
+ exercise: exercise?.title,
255
+ msg: `This link is broken: ${obj[img].absUrl}`,
256
+ });
257
+ }
258
+ } catch {
259
+ counter && counter.images.error++;
260
+ errors.push({
261
+ exercise: exercise?.title,
262
+ msg: `This link is broken: ${obj[img].absUrl}`,
263
+ });
264
+ }
265
+ }
266
+ }
267
+ } else if (finding === "markdownLinks" && Object.keys(obj!).length > 0) {
268
+ for (const link in obj) {
269
+ if (Object.prototype.hasOwnProperty.call(obj, link)) {
270
+ counter && counter.links.total++;
271
+ if (!obj[link].mdUrl.includes("twitter")) {
272
+ try {
273
+ // eslint-disable-next-line
274
+ let res = await fetch(obj[link].mdUrl, {
275
+ method: "HEAD",
276
+ });
277
+ if (res.status > 399 && res.status < 500) {
278
+ counter && counter.links.error++;
279
+ errors.push({
280
+ exercise: exercise?.title,
281
+ msg: `This link is broken: ${obj[link].mdUrl}`,
282
+ });
283
+ }
284
+ } catch {
285
+ counter && counter.links.error++;
286
+ errors.push({
287
+ exercise: exercise?.title,
288
+ msg: `This link is broken: ${obj[link].mdUrl}`,
289
+ });
290
+ }
291
+ }
292
+ }
108
293
  }
109
294
  }
110
295
  }
296
+ }
297
+
298
+ return true;
299
+ };
111
300
 
112
- return findings
113
- },
114
- // This function checks if there are errors, and show them in the console at the end.
115
- showErrors: (errors: IAuditErrors[], counter: ICounter) => {
116
- return new Promise((resolve, reject) => {
117
- if (errors) {
118
- if (errors.length > 0) {
119
- Console.log('Checking for errors...')
120
- for (const [i, error] of errors.entries())
121
- Console.error(
122
- `${i + 1}) ${error.msg} ${
123
- error.exercise ? `(Exercise: ${error.exercise})` : ''
124
- }`,
125
- )
301
+ // This function writes a given file with the given content.
302
+ const writeFile = async (content: string, filePath: string) => {
303
+ try {
304
+ await fs.promises.writeFile(filePath, content);
305
+ } catch (error) {
306
+ if (error)
307
+ Console.error(
308
+ `We weren't able to write the file in this path "${filePath}".`,
309
+ error
310
+ );
311
+ }
312
+ };
126
313
 
314
+ // This function checks if there are errors, and show them in the console at the end.
315
+ const showErrors = (
316
+ errors: IAuditErrors[],
317
+ counter: ICounter | undefined
318
+ ) => {
319
+ return new Promise((resolve, reject) => {
320
+ if (errors) {
321
+ if (errors.length > 0) {
322
+ Console.log("Checking for errors...");
323
+ for (const [i, error] of errors.entries())
127
324
  Console.error(
128
- ` We found ${errors.length} errors among ${counter.images.total} images, ${counter.links.total} link, ${counter.readmeFiles} README files and ${counter.exercises} exercises.`,
129
- )
130
- process.exit(1)
325
+ `${i + 1}) ${error.msg} ${
326
+ error.exercise ? `(Exercise: ${error.exercise})` : ""
327
+ }`
328
+ );
329
+ if (counter) {
330
+ Console.error(
331
+ ` We found ${errors.length} error${
332
+ errors.length > 1 ? "s" : ""
333
+ } among ${counter.images.total} images, ${
334
+ counter.links.total
335
+ } link, ${counter.readmeFiles} README files and ${
336
+ counter.exercises
337
+ } exercises.`
338
+ );
131
339
  } else {
132
- Console.success(
133
- `We didn't find any errors in this repository among ${counter.images.total} images, ${counter.links.total} link, ${counter.readmeFiles} README files and ${counter.exercises} exercises.`,
134
- )
135
- process.exit(0)
340
+ Console.error(
341
+ ` We found ${errors.length} error${
342
+ errors.length > 1 ? "s" : ""
343
+ } related with the project integrity.`
344
+ );
136
345
  }
346
+
347
+ process.exit(1);
137
348
  } else {
138
- reject('Failed')
139
- }
140
- })
141
- },
142
- // This function checks if there are warnings, and show them in the console at the end.
143
- showWarnings: (warnings: IAuditErrors[]) => {
144
- return new Promise((resolve, reject) => {
145
- if (warnings) {
146
- if (warnings.length > 0) {
147
- Console.log('Checking for warnings...')
148
- for (const [i, warning] of warnings.entries())
149
- Console.warning(
150
- `${i + 1}) ${warning.msg} ${
151
- warning.exercise ? `File: ${warning.exercise}` : ''
152
- }`,
153
- )
349
+ if (counter) {
350
+ Console.success(
351
+ `We didn't find any errors in this repository among ${counter.images.total} images, ${counter.links.total} link, ${counter.readmeFiles} README files and ${counter.exercises} exercises.`
352
+ );
353
+ } else {
354
+ Console.success(`We didn't find any errors in this repository.`);
154
355
  }
155
356
 
156
- resolve('SUCCESS')
157
- } else {
158
- reject('Failed')
357
+ process.exit(0);
358
+ }
359
+ } else {
360
+ reject("Failed");
361
+ }
362
+ });
363
+ };
364
+
365
+ // This function checks if there are warnings, and show them in the console at the end.
366
+ const showWarnings = (warnings: IAuditErrors[]) => {
367
+ return new Promise((resolve, reject) => {
368
+ if (warnings) {
369
+ if (warnings.length > 0) {
370
+ Console.log("Checking for warnings...");
371
+ for (const [i, warning] of warnings.entries())
372
+ Console.warning(
373
+ `${i + 1}) ${warning.msg} ${
374
+ warning.exercise ? `File: ${warning.exercise}` : ""
375
+ }`
376
+ );
159
377
  }
160
- })
161
- },
162
- }
378
+
379
+ resolve("SUCCESS");
380
+ } else {
381
+ reject("Failed");
382
+ }
383
+ });
384
+ };
385
+
386
+ export default {
387
+ isUrl,
388
+ checkForEmptySpaces,
389
+ checkLearnpackClean,
390
+ findInFile,
391
+ checkUrl,
392
+ writeFile,
393
+ showErrors,
394
+ showWarnings,
395
+ };
@@ -1,4 +0,0 @@
1
- export interface IAuditErrors {
2
- exercise?: string;
3
- msg: string;
4
- }
@@ -1,4 +0,0 @@
1
- export interface IAuditErrors {
2
- exercise?: string;
3
- msg: string;
4
- }