@learnpack/learnpack 2.1.18 → 2.1.23

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,341 +8,290 @@ const SessionCommand_1 = require("../utils/SessionCommand");
8
8
  const path = require("path");
9
9
  // eslint-disable-next-line
10
10
  const fetch = require("node-fetch");
11
- // eslint-disable-next-line
12
- const fm = require("front-matter");
13
11
  class AuditCommand extends SessionCommand_1.default {
14
12
  async init() {
15
13
  const { flags } = this.parse(AuditCommand);
16
14
  await this.initSession(flags);
17
15
  }
18
16
  async run() {
19
- var _a, _b, _c, _d, _e, _f, _g;
17
+ var _a, _b, _c, _d, _e, _f, _g, _h;
20
18
  console_1.default.log("Running command audit...");
21
19
  // Get configuration object.
22
20
  let config = (_a = this.configManager) === null || _a === void 0 ? void 0 : _a.get();
23
21
  if (config) {
24
22
  const errors = [];
25
23
  const warnings = [];
26
- const counter = {
27
- images: {
28
- error: 0,
29
- total: 0,
30
- },
31
- links: {
32
- error: 0,
33
- total: 0,
34
- },
35
- exercises: 0,
36
- readmeFiles: 0,
37
- };
38
- // Checks if learnpack clean has been run
39
- audit_1.default.checkLearnpackClean(config, errors);
40
- // Build exercises if they are not built yet.
41
- (_b = this.configManager) === null || _b === void 0 ? void 0 : _b.buildIndex();
42
- config = (_c = this.configManager) === null || _c === void 0 ? void 0 : _c.get();
43
- // Check if the exercises folder has some files within any ./exercise
44
- const exercisesPath = config.config.exercisesPath;
45
- fs.readdir(exercisesPath, (err, files) => {
46
- if (err) {
47
- return console.log("Unable to scan directory: " + err);
48
- }
49
- // listing all files using forEach
50
- for (const file of files) {
51
- // Do whatever you want to do with the file
52
- const filePath = path.join(exercisesPath, file);
53
- if (fs.statSync(filePath).isFile())
54
- warnings.push({
55
- exercise: file,
56
- msg: "This file is not inside any exercise folder.",
57
- });
58
- }
59
- });
60
- // This function checks that each of the url's are working.
61
- const checkUrl = async (file, exercise) => {
62
- var _a, _b, _c, _d;
63
- if (!fs.existsSync(file.path))
24
+ if (((_b = config === null || config === void 0 ? void 0 : config.config) === null || _b === void 0 ? void 0 : _b.projectType) === "tutorial") {
25
+ const counter = {
26
+ images: {
27
+ error: 0,
28
+ total: 0,
29
+ },
30
+ links: {
31
+ error: 0,
32
+ total: 0,
33
+ },
34
+ exercises: 0,
35
+ readmeFiles: 0,
36
+ };
37
+ // Checks if learnpack clean has been run
38
+ audit_1.default.checkLearnpackClean(config, errors);
39
+ // Build exercises if they are not built yet.
40
+ (_c = this.configManager) === null || _c === void 0 ? void 0 : _c.buildIndex();
41
+ config = (_d = this.configManager) === null || _d === void 0 ? void 0 : _d.get();
42
+ // Check if the exercises folder has some files within any ./exercise
43
+ const exercisesPath = config.config.exercisesPath;
44
+ fs.readdir(exercisesPath, (err, files) => {
45
+ if (err) {
46
+ return console.log("Unable to scan directory: " + err);
47
+ }
48
+ // listing all files using forEach
49
+ for (const file of files) {
50
+ // Do whatever you want to do with the file
51
+ const filePath = path.join(exercisesPath, file);
52
+ if (fs.statSync(filePath).isFile())
53
+ warnings.push({
54
+ exercise: file,
55
+ msg: "This file is not inside any exercise folder.",
56
+ });
57
+ }
58
+ });
59
+ // This function is being created because the find method doesn't work with promises.
60
+ const find = async (file, lang, exercise) => {
61
+ if (file.name === lang) {
62
+ await audit_1.default.checkUrl(config, file.path, file.name, exercise, errors, warnings, counter);
63
+ return true;
64
+ }
64
65
  return false;
65
- const content = fs.readFileSync(file.path).toString();
66
- const isEmpty = audit_1.default.checkForEmptySpaces(content);
67
- if (isEmpty || !content)
66
+ };
67
+ console_1.default.debug("config", config);
68
+ console_1.default.info(" Checking if the config file is fine...");
69
+ // These two lines check if the 'slug' property is inside the configuration object.
70
+ console_1.default.debug("Checking if the slug property is inside the configuration object...");
71
+ if (!((_e = config.config) === null || _e === void 0 ? void 0 : _e.slug))
68
72
  errors.push({
69
- exercise: exercise.title,
70
- msg: `This file (${file.name}) doesn't have any content inside.`,
73
+ exercise: undefined,
74
+ msg: "The slug property is not in the configuration object",
71
75
  });
72
- const frontmatter = fm(content);
73
- for (const attribute in frontmatter.attributes) {
74
- if (Object.prototype.hasOwnProperty.call(frontmatter.attributes, attribute) &&
75
- (attribute === "intro" || attribute === "tutorial")) {
76
- counter.links.total++;
77
- try {
78
- // eslint-disable-next-line
79
- let res = await fetch(frontmatter.attributes[attribute], {
80
- method: "HEAD",
81
- });
82
- if (!res.ok) {
83
- counter.links.error++;
76
+ // These two lines check if the 'repository' property is inside the configuration object.
77
+ console_1.default.debug("Checking if the repository property is inside the configuration object...");
78
+ if (!((_f = config.config) === null || _f === void 0 ? void 0 : _f.repository))
79
+ errors.push({
80
+ exercise: undefined,
81
+ msg: "The repository property is not in the configuration object",
82
+ });
83
+ else
84
+ audit_1.default.isUrl((_g = config.config) === null || _g === void 0 ? void 0 : _g.repository, errors, counter);
85
+ // These two lines check if the 'description' property is inside the configuration object.
86
+ console_1.default.debug("Checking if the description property is inside the configuration object...");
87
+ if (!((_h = config.config) === null || _h === void 0 ? void 0 : _h.description))
88
+ errors.push({
89
+ exercise: undefined,
90
+ msg: "The description property is not in the configuration object",
91
+ });
92
+ if (errors.length === 0)
93
+ console_1.default.log("The config file is ok");
94
+ // Validates if images and links are working at every README file.
95
+ const exercises = config.exercises;
96
+ const readmeFiles = [];
97
+ if (exercises && exercises.length > 0) {
98
+ console_1.default.info(" Checking if the images are working...");
99
+ for (const index in exercises) {
100
+ if (Object.prototype.hasOwnProperty.call(exercises, index)) {
101
+ const exercise = exercises[index];
102
+ if (!exercise_1.validateExerciseDirectoryName(exercise.title))
84
103
  errors.push({
85
104
  exercise: exercise.title,
86
- msg: `This link is broken (${res.ok}): ${frontmatter.attributes[attribute]}`,
105
+ msg: `The exercise ${exercise.title} has an invalid name.`,
87
106
  });
88
- }
89
- }
90
- catch (_e) {
91
- counter.links.error++;
92
- errors.push({
93
- exercise: exercise.title,
94
- msg: `This link is broken: ${frontmatter.attributes[attribute]}`,
95
- });
96
- }
97
- }
98
- }
99
- // Check url's of each README file.
100
- const findings = audit_1.default.findInFile(["relativeImages", "externalImages", "markdownLinks"], content);
101
- for (const finding in findings) {
102
- if (Object.prototype.hasOwnProperty.call(findings, finding)) {
103
- const obj = findings[finding];
104
- // Valdites all the relative path images.
105
- if (finding === "relativeImages" && Object.keys(obj).length > 0) {
106
- for (const img in obj) {
107
- if (Object.prototype.hasOwnProperty.call(obj, img)) {
108
- // Validates if the image is in the assets folder.
109
- counter.images.total++;
110
- const relativePath = path
111
- .relative(exercise.path.replace(/\\/gm, "/"), `${(_a = config.config) === null || _a === void 0 ? void 0 : _a.dirPath}/assets/${obj[img].relUrl}`)
112
- .replace(/\\/gm, "/");
113
- if (relativePath !== obj[img].absUrl.split("?").shift()) {
114
- counter.images.error++;
115
- errors.push({
116
- exercise: exercise.title,
117
- msg: `This relative path (${obj[img].relUrl}) is not pointing to the assets folder.`,
118
- });
119
- }
120
- if (!fs.existsSync(`${(_b = config.config) === null || _b === void 0 ? void 0 : _b.dirPath}/assets/${obj[img].relUrl}`)) {
121
- counter.images.error++;
122
- errors.push({
123
- exercise: exercise.title,
124
- msg: `The file ${obj[img].relUrl} doesn't exist in the assets folder.`,
125
- });
126
- }
127
- }
128
- }
129
- }
130
- else if (finding === "externalImages" &&
131
- Object.keys(obj).length > 0) {
132
- // Valdites all the aboslute path images.
133
- for (const img in obj) {
134
- if (Object.prototype.hasOwnProperty.call(obj, img)) {
135
- counter.images.total++;
136
- if (fs.existsSync(`${(_c = config.config) === null || _c === void 0 ? void 0 : _c.dirPath}/assets${obj[img].mdUrl
137
- .split("?")
138
- .shift()}`)) {
139
- const relativePath = path
140
- .relative(exercise.path.replace(/\\/gm, "/"), `${(_d = config.config) === null || _d === void 0 ? void 0 : _d.dirPath}/assets/${obj[img].mdUrl}`)
141
- .replace(/\\/gm, "/");
142
- warnings.push({
143
- exercise: exercise.title,
144
- 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}".`,
145
- });
146
- }
147
- try {
148
- // eslint-disable-next-line
149
- let res = await fetch(obj[img].absUrl, { method: "HEAD" });
150
- if (!res.ok) {
151
- counter.images.error++;
107
+ let readmeFilesCount = { exercise: exercise.title, count: 0 };
108
+ if (Object.keys(exercise.translations).length === 0)
109
+ errors.push({
110
+ exercise: exercise.title,
111
+ msg: `The exercise ${exercise.title} doesn't have a README.md file.`,
112
+ });
113
+ if (exercise.language === "python3" ||
114
+ exercise.language === "python") {
115
+ for (const f of exercise.files.map(f => f)) {
116
+ if (f.path.includes("test.py") ||
117
+ f.path.includes("tests.py")) {
118
+ const content = fs.readFileSync(f.path).toString();
119
+ const isEmpty = audit_1.default.checkForEmptySpaces(content);
120
+ if (isEmpty || !content)
152
121
  errors.push({
153
122
  exercise: exercise.title,
154
- msg: `This link is broken: ${obj[img].absUrl}`,
123
+ msg: `This file (${f.name}) doesn't have any content inside.`,
155
124
  });
156
- }
157
- }
158
- catch (_f) {
159
- counter.images.error++;
160
- errors.push({
161
- exercise: exercise.title,
162
- msg: `This link is broken: ${obj[img].absUrl}`,
163
- });
164
125
  }
165
126
  }
166
127
  }
167
- }
168
- else if (finding === "markdownLinks" &&
169
- Object.keys(obj).length > 0) {
170
- for (const link in obj) {
171
- if (Object.prototype.hasOwnProperty.call(obj, link)) {
172
- counter.links.total++;
173
- try {
174
- // eslint-disable-next-line
175
- let res = await fetch(obj[link].mdUrl, { method: "HEAD" });
176
- if (res.status > 399 && res.status < 500) {
177
- console_1.default.log("Response links:", res.status, obj[link].mdUrl, res);
178
- counter.links.error++;
128
+ else {
129
+ for (const f of exercise.files.map(f => f)) {
130
+ if (f.path.includes("test.js") ||
131
+ f.path.includes("tests.js")) {
132
+ const content = fs.readFileSync(f.path).toString();
133
+ const isEmpty = audit_1.default.checkForEmptySpaces(content);
134
+ if (isEmpty || !content)
179
135
  errors.push({
180
136
  exercise: exercise.title,
181
- msg: `This link is broken: ${obj[link].mdUrl}`,
137
+ msg: `This file (${f.name}) doesn't have any content inside.`,
182
138
  });
139
+ }
140
+ }
141
+ }
142
+ for (const lang in exercise.translations) {
143
+ if (Object.prototype.hasOwnProperty.call(exercise.translations, lang)) {
144
+ const files = [];
145
+ const findResultPromises = [];
146
+ for (const file of exercise.files) {
147
+ const found = find(file, exercise.translations[lang], exercise);
148
+ findResultPromises.push(found);
149
+ }
150
+ // eslint-disable-next-line
151
+ let findResults = await Promise.all(findResultPromises);
152
+ for (const found of findResults) {
153
+ if (found) {
154
+ readmeFilesCount = Object.assign(Object.assign({}, readmeFilesCount), { count: readmeFilesCount.count + 1 });
155
+ files.push(found);
183
156
  }
184
157
  }
185
- catch (_g) {
186
- counter.links.error++;
158
+ if (!files.includes(true))
187
159
  errors.push({
188
160
  exercise: exercise.title,
189
- msg: `This link is broken: ${obj[link].mdUrl}`,
161
+ msg: "This exercise doesn't have a README.md file.",
190
162
  });
191
- }
192
163
  }
193
164
  }
165
+ readmeFiles.push(readmeFilesCount);
194
166
  }
195
167
  }
196
168
  }
197
- return true;
198
- };
199
- // This function is being created because the find method doesn't work with promises.
200
- const find = async (file, lang, exercise) => {
201
- if (file.name === lang) {
202
- await checkUrl(file, exercise);
203
- return true;
169
+ else
170
+ errors.push({
171
+ exercise: undefined,
172
+ msg: "The exercises array is empty.",
173
+ });
174
+ console_1.default.log(`${counter.images.total - counter.images.error} images ok from ${counter.images.total}`);
175
+ console_1.default.info(" Checking if important files are missing... (README's, translations, gitignore...)");
176
+ // 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.
177
+ const files = [];
178
+ let count = 0;
179
+ for (const item of readmeFiles) {
180
+ if (count < item.count)
181
+ count = item.count;
182
+ }
183
+ for (const item of readmeFiles) {
184
+ if (item.count !== count)
185
+ files.push(` ${item.exercise}`);
204
186
  }
205
- return false;
206
- };
207
- console_1.default.debug("config", config);
208
- console_1.default.info(" Checking if the config file is fine...");
209
- // These two lines check if the 'slug' property is inside the configuration object.
210
- console_1.default.debug("Checking if the slug property is inside the configuration object...");
211
- if (!((_d = config.config) === null || _d === void 0 ? void 0 : _d.slug))
212
- errors.push({
213
- exercise: undefined,
214
- msg: "The slug property is not in the configuration object",
215
- });
216
- // These two lines check if the 'repository' property is inside the configuration object.
217
- console_1.default.debug("Checking if the repository property is inside the configuration object...");
218
- if (!((_e = config.config) === null || _e === void 0 ? void 0 : _e.repository))
219
- errors.push({
220
- exercise: undefined,
221
- msg: "The repository property is not in the configuration object",
222
- });
223
- else
224
- audit_1.default.isUrl((_f = config.config) === null || _f === void 0 ? void 0 : _f.repository, errors, counter);
225
- // These two lines check if the 'description' property is inside the configuration object.
226
- console_1.default.debug("Checking if the description property is inside the configuration object...");
227
- if (!((_g = config.config) === null || _g === void 0 ? void 0 : _g.description))
228
- errors.push({
229
- exercise: undefined,
230
- msg: "The description property is not in the configuration object",
231
- });
232
- if (errors.length === 0)
233
- console_1.default.log("The config file is ok");
234
- // Validates if images and links are working at every README file.
235
- const exercises = config.exercises;
236
- const readmeFiles = [];
237
- if (exercises && exercises.length > 0) {
238
- console_1.default.info(" Checking if the images are working...");
239
- for (const index in exercises) {
240
- if (Object.prototype.hasOwnProperty.call(exercises, index)) {
241
- const exercise = exercises[index];
242
- if (!exercise_1.validateExerciseDirectoryName(exercise.title))
243
- errors.push({
244
- exercise: exercise.title,
245
- msg: `The exercise ${exercise.title} has an invalid name.`,
246
- });
247
- let readmeFilesCount = { exercise: exercise.title, count: 0 };
248
- if (Object.keys(exercise.translations).length === 0)
249
- errors.push({
250
- exercise: exercise.title,
251
- msg: `The exercise ${exercise.title} doesn't have a README.md file.`,
252
- });
253
- if (exercise.language === "python3" ||
254
- exercise.language === "python") {
255
- for (const f of exercise.files.map(f => f)) {
256
- if (f.path.includes("test.py") || f.path.includes("tests.py")) {
257
- const content = fs.readFileSync(f.path).toString();
258
- const isEmpty = audit_1.default.checkForEmptySpaces(content);
259
- if (isEmpty || !content)
260
- errors.push({
261
- exercise: exercise.title,
262
- msg: `This file (${f.name}) doesn't have any content inside.`,
263
- });
264
- }
265
- }
266
- }
267
- else {
268
- for (const f of exercise.files.map(f => f)) {
269
- if (f.path.includes("test.js") || f.path.includes("tests.js")) {
270
- const content = fs.readFileSync(f.path).toString();
271
- const isEmpty = audit_1.default.checkForEmptySpaces(content);
272
- if (isEmpty || !content)
273
- errors.push({
274
- exercise: exercise.title,
275
- msg: `This file (${f.name}) doesn't have any content inside.`,
276
- });
277
- }
278
- }
279
- }
280
- for (const lang in exercise.translations) {
281
- if (Object.prototype.hasOwnProperty.call(exercise.translations, lang)) {
282
- const files = [];
283
- const findResultPromises = [];
284
- for (const file of exercise.files) {
285
- const found = find(file, exercise.translations[lang], exercise);
286
- findResultPromises.push(found);
287
- }
288
- // eslint-disable-next-line
289
- let findResults = await Promise.all(findResultPromises);
290
- for (const found of findResults) {
291
- if (found) {
292
- readmeFilesCount = Object.assign(Object.assign({}, readmeFilesCount), { count: readmeFilesCount.count + 1 });
293
- files.push(found);
294
- }
295
- }
296
- if (!files.includes(true))
297
- errors.push({
298
- exercise: exercise.title,
299
- msg: "This exercise doesn't have a README.md file.",
300
- });
301
- }
302
- }
303
- readmeFiles.push(readmeFilesCount);
304
- }
187
+ if (files.length > 0) {
188
+ const filesString = files.join(",");
189
+ warnings.push({
190
+ exercise: undefined,
191
+ msg: files.length === 1 ?
192
+ `This exercise is missing translations:${filesString}` :
193
+ `These exercises are missing translations:${filesString}`,
194
+ });
195
+ }
196
+ // Checks if the .gitignore file exists.
197
+ if (!fs.existsSync(".gitignore"))
198
+ warnings.push({
199
+ exercise: undefined,
200
+ msg: ".gitignore file doesn't exist",
201
+ });
202
+ counter.exercises = exercises.length;
203
+ for (const readme of readmeFiles) {
204
+ counter.readmeFiles += readme.count;
305
205
  }
306
206
  }
307
- else
308
- errors.push({
309
- exercise: undefined,
310
- msg: "The exercises array is empty.",
311
- });
312
- console_1.default.log(`${counter.images.total - counter.images.error} images ok from ${counter.images.total}`);
313
- console_1.default.info(" Checking if important files are missing... (README's, translations, gitignore...)");
314
- // 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.
315
- const files = [];
316
- let count = 0;
317
- for (const item of readmeFiles) {
318
- if (count < item.count)
319
- count = item.count;
320
- }
321
- for (const item of readmeFiles) {
322
- if (item.count !== count)
323
- files.push(` ${item.exercise}`);
324
- }
325
- if (files.length > 0) {
326
- const filesString = files.join(",");
327
- warnings.push({
328
- exercise: undefined,
329
- msg: files.length === 1 ?
330
- `This exercise is missing translations:${filesString}` :
331
- `These exercises are missing translations:${filesString}`,
332
- });
333
- }
334
- // Checks if the .gitignore file exists.
335
- if (!fs.existsSync(".gitignore"))
336
- warnings.push({
337
- exercise: undefined,
338
- msg: ".gitignore file doesn't exist",
339
- });
340
- counter.exercises = exercises.length;
341
- for (const readme of readmeFiles) {
342
- counter.readmeFiles += readme.count;
207
+ else {
208
+ // This is the audit code for Projects
209
+ // Getting the learn.json schema
210
+ const schemaResponse = await fetch("https://raw.githubusercontent.com/tommygonzaleza/project-template/main/.github/learn-schema.json");
211
+ const schema = await schemaResponse.json();
212
+ // Checking the "learn.json" file:
213
+ const learnjson = JSON.parse(fs.readFileSync("./learn.json").toString());
214
+ if (!learnjson) {
215
+ console_1.default.error("There is no learn.json file located in the root of the project.");
216
+ process.exit(1);
217
+ }
218
+ // Checking the README.md files and possible translations.
219
+ let readmeFiles = [];
220
+ const translations = [];
221
+ const translationRegex = /README\.([a-z]{2,3})\.md/;
222
+ try {
223
+ const data = await fs.promises.readdir("./");
224
+ readmeFiles = data.filter(file => file.includes("README"));
225
+ if (readmeFiles.length === 0)
226
+ errors.push({
227
+ exercise: undefined,
228
+ msg: `There is no README file in the repository.`,
229
+ });
230
+ }
231
+ catch (error) {
232
+ if (error)
233
+ console_1.default.error("There was an error getting the directory files", error);
234
+ }
235
+ for (const readmeFile of readmeFiles) {
236
+ // Checking the language of each README file.
237
+ if (readmeFile === "README.md")
238
+ translations.push("us");
239
+ else {
240
+ const regexGroups = translationRegex.exec(readmeFile);
241
+ if (regexGroups)
242
+ translations.push(regexGroups[1]);
243
+ }
244
+ const readme = fs.readFileSync(path.resolve(readmeFile)).toString();
245
+ const isEmpty = audit_1.default.checkForEmptySpaces(readme);
246
+ if (isEmpty || !readme) {
247
+ errors.push({
248
+ exercise: undefined,
249
+ msg: `This file "${readmeFile}" doesn't have any content inside.`,
250
+ });
251
+ continue;
252
+ }
253
+ if (readme.length < 800)
254
+ errors.push({
255
+ exercise: undefined,
256
+ msg: `The "${readmeFile}" file should have at least 800 characters (It currently have: ${readme.length}).`,
257
+ });
258
+ // eslint-disable-next-line
259
+ await audit_1.default.checkUrl(config, path.resolve(readmeFile), readmeFile, undefined, errors, warnings,
260
+ // eslint-disable-next-line
261
+ undefined);
262
+ }
263
+ // Adding the translations to the learn.json
264
+ learnjson.translations = translations;
265
+ // Checking if the preview image (from the learn.json) is OK.
266
+ try {
267
+ const res = await fetch(learnjson.preview, { method: "HEAD" });
268
+ if (!res.ok) {
269
+ errors.push({
270
+ exercise: undefined,
271
+ msg: `The link of the "preview" is broken: ${learnjson.preview}`,
272
+ });
273
+ }
274
+ }
275
+ catch (_j) {
276
+ errors.push({
277
+ exercise: undefined,
278
+ msg: `The link of the "preview" is broken: ${learnjson.preview}`,
279
+ });
280
+ }
281
+ const date = new Date();
282
+ learnjson.validationAt = date.getTime();
283
+ if (errors.length > 0)
284
+ learnjson.validationStatus = "error";
285
+ else if (warnings.length > 0)
286
+ learnjson.validationStatus = "warning";
287
+ else
288
+ learnjson.validationStatus = "success";
289
+ // Writes the "learn.json" file with all the new properties
290
+ await fs.promises.writeFile("./learn.json", JSON.stringify(learnjson));
343
291
  }
344
292
  await audit_1.default.showWarnings(warnings);
345
- await audit_1.default.showErrors(errors, counter);
293
+ // eslint-disable-next-line
294
+ await audit_1.default.showErrors(errors, undefined);
346
295
  }
347
296
  }
348
297
  }
@@ -14,6 +14,7 @@ declare const _default: {
14
14
  contact: string;
15
15
  language: string;
16
16
  autoPlay: boolean;
17
+ projectType: string;
17
18
  grading: string;
18
19
  exercisesPath: string;
19
20
  webpackTemplate: null;
@@ -16,6 +16,7 @@ exports.default = {
16
16
  contact: "https://github.com/learnpack/learnpack/issues/new",
17
17
  language: "auto",
18
18
  autoPlay: true,
19
+ projectType: "tutorial",
19
20
  grading: "isolated",
20
21
  exercisesPath: "./",
21
22
  webpackTemplate: null,
@@ -0,0 +1,15 @@
1
+ export interface IAuditErrors {
2
+ exercise?: string;
3
+ msg: string;
4
+ }
5
+ declare type TType = "string" | "array" | "number" | "url" | "boolean";
6
+ export interface ISchemaItem {
7
+ key: string;
8
+ mandatory: boolean;
9
+ type: TType;
10
+ max_size?: number;
11
+ allowed_extensions?: string[];
12
+ enum?: string[];
13
+ max_item_size?: number;
14
+ }
15
+ export {};
File without changes
@@ -40,6 +40,7 @@ export interface IConfig {
40
40
  disableGrading: boolean;
41
41
  actions: Array<string>;
42
42
  autoPlay: boolean;
43
+ projectType?: string;
43
44
  contact?: string;
44
45
  disabledActions?: Array<TConfigAction>;
45
46
  compiler: TCompiler;