@learnpack/learnpack 2.0.22 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,412 +1,412 @@
1
- import * as path from "path";
2
- import * as fs from "fs";
3
- import * as shell from "shelljs";
4
- import Console from "../../utils/console";
5
- import watch from "../../utils/watcher";
6
- import {
7
- ValidationError,
8
- NotFoundError,
9
- InternalError,
10
- } from "../../utils/errors";
11
-
12
- import defaults from "./defaults";
13
- import { exercise } from "./exercise";
14
-
15
- import { rmSync } from "../file";
16
- import { IConfigObj, TConfigObjAttributes, TMode } from "../../models/config";
17
- import {
18
- IConfigManagerAttributes,
19
- IConfigManager,
20
- } from "../../models/config-manager";
21
- import { IFile } from "../../models/file";
22
-
23
- /* exercise folder name standard */
24
-
25
- // eslint-disable-next-line
26
- const fetch = require("node-fetch");
27
- // eslint-disable-next-line
28
- const chalk = require("chalk");
29
-
30
- /* exercise folder name standard */
31
-
32
- const getConfigPath = () => {
33
- const possibleFileNames = [
34
- "learn.json",
35
- ".learn/learn.json",
36
- "bc.json",
37
- ".breathecode/bc.json",
38
- ];
39
- const config = possibleFileNames.find(file => fs.existsSync(file)) || null;
40
- if (config && fs.existsSync(".breathecode"))
41
- return { config, base: ".breathecode" };
42
- if (config === null)
43
- throw NotFoundError(
44
- "learn.json file not found on current folder, is this a learnpack package?"
45
- );
46
- return { config, base: ".learn" };
47
- };
48
-
49
- const getExercisesPath = (base: string) => {
50
- const possibleFileNames = ["./exercises", base + "/exercises", "./"];
51
- return possibleFileNames.find(file => fs.existsSync(file)) || null;
52
- };
53
-
54
- const getGitpodAddress = () => {
55
- if (shell.exec("gp -h", { silent: true }).code === 0) {
56
- return shell
57
- .exec("gp url", { silent: true })
58
- .stdout.replace(/(\r\n|\n|\r)/gm, "");
59
- }
60
-
61
- Console.debug("Gitpod command line tool not found");
62
- return "http://localhost";
63
- };
64
-
65
- export default async ({
66
- grading,
67
- mode,
68
- disableGrading,
69
- version,
70
- }: IConfigManagerAttributes): Promise<IConfigManager> => {
71
- const confPath = getConfigPath();
72
- Console.debug("This is the config path: ", confPath);
73
-
74
- let configObj: IConfigObj = {};
75
-
76
- if (confPath) {
77
- const bcContent = fs.readFileSync(confPath.config);
78
-
79
- let hiddenBcContent = {};
80
- if (fs.existsSync(confPath.base + "/config.json")) {
81
- hiddenBcContent = fs.readFileSync(confPath.base + "/config.json");
82
- hiddenBcContent = JSON.parse(hiddenBcContent as string);
83
- if (!hiddenBcContent)
84
- throw new Error(
85
- `Invalid ${confPath.base}/config.json syntax: Unable to parse.`
86
- );
87
- }
88
-
89
- const jsonConfig = JSON.parse(`${bcContent}`);
90
- if (!jsonConfig)
91
- throw new Error(`Invalid ${confPath.config} syntax: Unable to parse.`);
92
-
93
- let session: number;
94
-
95
- // add using id to the installation
96
- if (!jsonConfig.session) {
97
- session = Math.floor(Math.random() * 10_000_000_000_000_000_000);
98
- } else {
99
- session = jsonConfig.session;
100
- delete jsonConfig.session;
101
- }
102
-
103
- configObj = deepMerge(hiddenBcContent, {
104
- config: jsonConfig,
105
- session: session,
106
- });
107
- Console.debug("Content from the configuration .json ", configObj);
108
- } else {
109
- throw ValidationError(
110
- "No learn.json file has been found, make sure you are in the folder"
111
- );
112
- }
113
-
114
- configObj = deepMerge(defaults || {}, configObj, {
115
- config: {
116
- grading: grading || configObj.config?.grading,
117
- configPath: confPath.config,
118
- },
119
- });
120
-
121
- if (configObj.config) {
122
- configObj.config.outputPath = confPath.base + "/dist";
123
- }
124
-
125
- Console.debug("This is your configuration object: ", {
126
- ...configObj,
127
- exercises: configObj.exercises ?
128
- configObj.exercises.map(e => e.slug) :
129
- [],
130
- });
131
-
132
- // auto detect agent (if possible)
133
- if (shell.which("gp") && configObj && configObj.config) {
134
- configObj.config.editor.agent = "gitpod";
135
- configObj.address = getGitpodAddress();
136
- configObj.config.publicUrl = `https://${
137
- configObj.config.port
138
- }-${configObj.address?.slice(8)}`;
139
- } else if (configObj.config && !configObj.config.editor.agent) {
140
- configObj.config.editor.agent = "localhost";
141
- }
142
-
143
- if (configObj.config && !configObj.config.publicUrl)
144
- configObj.config.publicUrl = `${configObj.address}:${configObj.config.port}`;
145
-
146
- // Assign default editor mode if not set already
147
- if (configObj.config && mode !== null) {
148
- configObj.config.editor.mode = (mode as TMode) || ("vscode" as TMode);
149
- }
150
-
151
- if (configObj.config && !configObj.config.editor.mode)
152
- configObj.config.editor.mode =
153
- configObj.config.editor.agent === "localhost" ? "standalone" : "preview";
154
-
155
- if (version && configObj.config)
156
- configObj.config.editor.version = version;
157
- else if (configObj.config && configObj.config.editor.version === null) {
158
- Console.debug("Config version not found, downloading default.");
159
- const resp = await fetch(
160
- "https://raw.githubusercontent.com/learnpack/coding-ide/learnpack/package.json"
161
- );
162
- const packageJSON = await resp.json();
163
- configObj.config.editor.version = packageJSON.version || "1.0.72";
164
- }
165
-
166
- if (configObj.config) {
167
- configObj.config.dirPath = "./" + confPath.base;
168
- configObj.config.exercisesPath = getExercisesPath(confPath.base) || "./";
169
- }
170
-
171
- return {
172
- validLanguages: {},
173
- get: () => configObj,
174
- validateEngine: function (language: string, server: any, socket: any) {
175
- // eslint-disable-next-line
176
- const alias = (_l: string) => {
177
- const map: any = {
178
- python3: "python",
179
- };
180
- if (map[_l])
181
- return map[_l];
182
- return _l;
183
- };
184
-
185
- // decode aliases
186
- language = alias(language);
187
-
188
- if (this.validLanguages[language])
189
- return true;
190
-
191
- Console.debug(`Validating engine for ${language} compilation`);
192
- let result = shell.exec("learnpack plugins", { silent: true });
193
-
194
- if (
195
- result.code === 0 &&
196
- result.stdout.includes(`learnpack-${language}`)
197
- ) {
198
- this.validLanguages[language] = true;
199
- return true;
200
- }
201
-
202
- Console.info(`Language engine for ${language} not found, installing...`);
203
- result = shell.exec(`learnpack plugins:install learnpack-${language}`, {
204
- silent: true,
205
- });
206
- if (result.code === 0) {
207
- socket.log(
208
- "compiling",
209
- "Installing the python compiler, you will have to reset the exercises after installation by writing on your terminal: $ learnpack run"
210
- );
211
- Console.info(
212
- `Successfully installed the ${language} exercise engine, \n please start learnpack again by running the following command: \n ${chalk.white(
213
- "$ learnpack start"
214
- )}\n\n `
215
- );
216
- server.terminate();
217
- return false;
218
- }
219
-
220
- this.validLanguages[language] = false;
221
- socket.error(`Error installing ${language} exercise engine`);
222
- Console.error(`Error installing ${language} exercise engine`);
223
- Console.log(result.stdout);
224
- throw InternalError(`Error installing ${language} exercise engine`);
225
- },
226
- clean: () => {
227
- if (configObj.config) {
228
- if (configObj.config.outputPath) {
229
- rmSync(configObj.config.outputPath);
230
- }
231
-
232
- rmSync(configObj.config.dirPath + "/_app");
233
- rmSync(configObj.config.dirPath + "/reports");
234
- rmSync(configObj.config.dirPath + "/.session");
235
- rmSync(configObj.config.dirPath + "/resets");
236
-
237
- // clean tag gz
238
- if (fs.existsSync(configObj.config.dirPath + "/app.tar.gz"))
239
- fs.unlinkSync(configObj.config.dirPath + "/app.tar.gz");
240
-
241
- if (fs.existsSync(configObj.config.dirPath + "/config.json"))
242
- fs.unlinkSync(configObj.config.dirPath + "/config.json");
243
-
244
- if (fs.existsSync(configObj.config.dirPath + "/vscode_queue.json"))
245
- fs.unlinkSync(configObj.config.dirPath + "/vscode_queue.json");
246
- }
247
- },
248
- getExercise: slug => {
249
- Console.debug("ExercisePath Slug", slug);
250
- const exercise = (configObj.exercises || []).find(
251
- ex => ex.slug === slug
252
- );
253
- if (!exercise)
254
- throw ValidationError(`Exercise ${slug} not found`);
255
-
256
- return exercise;
257
- },
258
- getAllExercises: () => {
259
- return configObj.exercises;
260
- },
261
- startExercise: function (slug: string) {
262
- const exercise = this.getExercise(slug);
263
-
264
- // set config.json with current exercise
265
- configObj.currentExercise = exercise.slug;
266
-
267
- this.save();
268
-
269
- // eslint-disable-next-line
270
- exercise.files.forEach((f: IFile) => {
271
- if (configObj.config) {
272
- const _path = configObj.config.outputPath + "/" + f.name;
273
- if (f.hidden === false && fs.existsSync(_path))
274
- fs.unlinkSync(_path);
275
- }
276
- });
277
-
278
- return exercise;
279
- },
280
- noCurrentExercise: function () {
281
- configObj.currentExercise = null;
282
- this.save();
283
- },
284
- reset: slug => {
285
- if (
286
- configObj.config &&
287
- !fs.existsSync(`${configObj.config.dirPath}/resets/` + slug)
288
- )
289
- throw ValidationError("Could not find the original files for " + slug);
290
-
291
- const exercise = (configObj.exercises || []).find(
292
- ex => ex.slug === slug
293
- );
294
- if (!exercise)
295
- throw ValidationError(
296
- `Exercise ${slug} not found on the configuration`
297
- );
298
-
299
- if (configObj.config) {
300
- for (const fileName of fs.readdirSync(
301
- `${configObj.config.dirPath}/resets/${slug}/`
302
- )) {
303
- const content = fs.readFileSync(
304
- `${configObj.config?.dirPath}/resets/${slug}/${fileName}`
305
- );
306
- fs.writeFileSync(`${exercise.path}/${fileName}`, content);
307
- }
308
- }
309
- },
310
- buildIndex: function () {
311
- Console.info("Building the exercise index...");
312
-
313
- const isDirectory = (source: string) => {
314
- const name = path.basename(source);
315
- if (name === path.basename(configObj?.config?.dirPath || ""))
316
- return false;
317
- // ignore folders that start with a dot
318
- if (name.charAt(0) === "." || name.charAt(0) === "_")
319
- return false;
320
-
321
- return fs.lstatSync(source).isDirectory();
322
- };
323
-
324
- const getDirectories = (source: string) =>
325
- fs
326
- .readdirSync(source)
327
- .map(name => path.join(source, name))
328
- .filter(isDirectory);
329
- // add the .learn folder
330
- if (!fs.existsSync(confPath.base))
331
- fs.mkdirSync(confPath.base);
332
- // add the outout folder where webpack will publish the the html/css/js files
333
- if (
334
- configObj.config &&
335
- configObj.config.outputPath &&
336
- !fs.existsSync(configObj.config.outputPath)
337
- )
338
- fs.mkdirSync(configObj.config.outputPath);
339
-
340
- // TODO: we could use npm library front-mater to read the title of the exercises from the README.md
341
- const grupedByDirectory = getDirectories(
342
- configObj?.config?.exercisesPath || ""
343
- );
344
- configObj.exercises =
345
- grupedByDirectory.length > 0 ?
346
- grupedByDirectory.map((path, position) =>
347
- exercise(path, position, configObj)
348
- ) :
349
- [exercise(configObj?.config?.exercisesPath || "", 0, configObj)];
350
- this.save();
351
- },
352
- watchIndex: function (onChange: () => void) {
353
- if (configObj.config && !configObj.config.exercisesPath)
354
- throw ValidationError(
355
- "No exercises directory to watch: " + configObj.config.exercisesPath
356
- );
357
-
358
- this.buildIndex();
359
- watch(configObj?.config?.exercisesPath || "")
360
- .then((/* eventname, filename */) => {
361
- Console.debug("Changes detected on your exercises");
362
- this.buildIndex();
363
- if (onChange)
364
- onChange();
365
- })
366
- .catch(error => {
367
- throw error;
368
- });
369
- },
370
- save: () => {
371
- Console.debug("Saving configuration with: ", configObj);
372
-
373
- // remove the duplicates form the actions array
374
- // configObj.config.actions = [...new Set(configObj.config.actions)];
375
- if (configObj.config) {
376
- configObj.config.translations = [
377
- ...new Set(configObj.config.translations),
378
- ];
379
-
380
- fs.writeFileSync(
381
- configObj.config.dirPath + "/config.json",
382
- JSON.stringify(configObj, null, 4)
383
- );
384
- }
385
- },
386
- } as IConfigManager;
387
- };
388
-
389
- function deepMerge(...sources: any): any {
390
- let acc: any = {};
391
- for (const source of sources) {
392
- if (Array.isArray(source)) {
393
- if (!Array.isArray(acc)) {
394
- acc = [];
395
- }
396
-
397
- acc = [...source];
398
- } else if (source instanceof Object) {
399
- // eslint-disable-next-line
400
- for (let [key, value] of Object.entries(source)) {
401
- if (value instanceof Object && key in acc) {
402
- value = deepMerge(acc[key], value);
403
- }
404
-
405
- if (value !== undefined)
406
- acc = { ...acc, [key]: value };
407
- }
408
- }
409
- }
410
-
411
- return acc;
412
- }
1
+ import * as path from "path";
2
+ import * as fs from "fs";
3
+ import * as shell from "shelljs";
4
+ import Console from "../../utils/console";
5
+ import watch from "../../utils/watcher";
6
+ import {
7
+ ValidationError,
8
+ NotFoundError,
9
+ InternalError,
10
+ } from "../../utils/errors";
11
+
12
+ import defaults from "./defaults";
13
+ import { exercise } from "./exercise";
14
+
15
+ import { rmSync } from "../file";
16
+ import { IConfigObj, TConfigObjAttributes, TMode } from "../../models/config";
17
+ import {
18
+ IConfigManagerAttributes,
19
+ IConfigManager,
20
+ } from "../../models/config-manager";
21
+ import { IFile } from "../../models/file";
22
+
23
+ /* exercise folder name standard */
24
+
25
+ // eslint-disable-next-line
26
+ const fetch = require("node-fetch");
27
+ // eslint-disable-next-line
28
+ const chalk = require("chalk");
29
+
30
+ /* exercise folder name standard */
31
+
32
+ const getConfigPath = () => {
33
+ const possibleFileNames = [
34
+ "learn.json",
35
+ ".learn/learn.json",
36
+ "bc.json",
37
+ ".breathecode/bc.json",
38
+ ];
39
+ const config = possibleFileNames.find(file => fs.existsSync(file)) || null;
40
+ if (config && fs.existsSync(".breathecode"))
41
+ return { config, base: ".breathecode" };
42
+ if (config === null)
43
+ throw NotFoundError(
44
+ "learn.json file not found on current folder, is this a learnpack package?"
45
+ );
46
+ return { config, base: ".learn" };
47
+ };
48
+
49
+ const getExercisesPath = (base: string) => {
50
+ const possibleFileNames = ["./exercises", base + "/exercises", "./"];
51
+ return possibleFileNames.find(file => fs.existsSync(file)) || null;
52
+ };
53
+
54
+ const getGitpodAddress = () => {
55
+ if (shell.exec("gp -h", { silent: true }).code === 0) {
56
+ return shell
57
+ .exec("gp url", { silent: true })
58
+ .stdout.replace(/(\r\n|\n|\r)/gm, "");
59
+ }
60
+
61
+ Console.debug("Gitpod command line tool not found");
62
+ return "http://localhost";
63
+ };
64
+
65
+ export default async ({
66
+ grading,
67
+ mode,
68
+ disableGrading,
69
+ version,
70
+ }: IConfigManagerAttributes): Promise<IConfigManager> => {
71
+ const confPath = getConfigPath();
72
+ Console.debug("This is the config path: ", confPath);
73
+
74
+ let configObj: IConfigObj = {};
75
+
76
+ if (confPath) {
77
+ const bcContent = fs.readFileSync(confPath.config);
78
+
79
+ let hiddenBcContent = {};
80
+ if (fs.existsSync(confPath.base + "/config.json")) {
81
+ hiddenBcContent = fs.readFileSync(confPath.base + "/config.json");
82
+ hiddenBcContent = JSON.parse(hiddenBcContent as string);
83
+ if (!hiddenBcContent)
84
+ throw new Error(
85
+ `Invalid ${confPath.base}/config.json syntax: Unable to parse.`
86
+ );
87
+ }
88
+
89
+ const jsonConfig = JSON.parse(`${bcContent}`);
90
+ if (!jsonConfig)
91
+ throw new Error(`Invalid ${confPath.config} syntax: Unable to parse.`);
92
+
93
+ let session: number;
94
+
95
+ // add using id to the installation
96
+ if (!jsonConfig.session) {
97
+ session = Math.floor(Math.random() * 10_000_000_000_000_000_000);
98
+ } else {
99
+ session = jsonConfig.session;
100
+ delete jsonConfig.session;
101
+ }
102
+
103
+ configObj = deepMerge(hiddenBcContent, {
104
+ config: jsonConfig,
105
+ session: session,
106
+ });
107
+ Console.debug("Content from the configuration .json ", configObj);
108
+ } else {
109
+ throw ValidationError(
110
+ "No learn.json file has been found, make sure you are in the folder"
111
+ );
112
+ }
113
+
114
+ configObj = deepMerge(defaults || {}, configObj, {
115
+ config: {
116
+ grading: grading || configObj.config?.grading,
117
+ configPath: confPath.config,
118
+ },
119
+ });
120
+
121
+ if (configObj.config) {
122
+ configObj.config.outputPath = confPath.base + "/dist";
123
+ }
124
+
125
+ Console.debug("This is your configuration object: ", {
126
+ ...configObj,
127
+ exercises: configObj.exercises ?
128
+ configObj.exercises.map(e => e.slug) :
129
+ [],
130
+ });
131
+
132
+ // auto detect agent (if possible)
133
+ if (shell.which("gp") && configObj && configObj.config) {
134
+ configObj.config.editor.agent = "gitpod";
135
+ configObj.address = getGitpodAddress();
136
+ configObj.config.publicUrl = `https://${
137
+ configObj.config.port
138
+ }-${configObj.address?.slice(8)}`;
139
+ } else if (configObj.config && !configObj.config.editor.agent) {
140
+ configObj.config.editor.agent = "localhost";
141
+ }
142
+
143
+ if (configObj.config && !configObj.config.publicUrl)
144
+ configObj.config.publicUrl = `${configObj.address}:${configObj.config.port}`;
145
+
146
+ // Assign default editor mode if not set already
147
+ if (configObj.config && mode !== null) {
148
+ configObj.config.editor.mode = (mode as TMode) || ("vscode" as TMode);
149
+ }
150
+
151
+ if (configObj.config && !configObj.config.editor.mode)
152
+ configObj.config.editor.mode =
153
+ configObj.config.editor.agent === "localhost" ? "standalone" : "preview";
154
+
155
+ if (version && configObj.config)
156
+ configObj.config.editor.version = version;
157
+ else if (configObj.config && configObj.config.editor.version === null) {
158
+ Console.debug("Config version not found, downloading default.");
159
+ const resp = await fetch(
160
+ "https://raw.githubusercontent.com/learnpack/coding-ide/learnpack/package.json"
161
+ );
162
+ const packageJSON = await resp.json();
163
+ configObj.config.editor.version = packageJSON.version || "1.0.72";
164
+ }
165
+
166
+ if (configObj.config) {
167
+ configObj.config.dirPath = "./" + confPath.base;
168
+ configObj.config.exercisesPath = getExercisesPath(confPath.base) || "./";
169
+ }
170
+
171
+ return {
172
+ validLanguages: {},
173
+ get: () => configObj,
174
+ validateEngine: function (language: string, server: any, socket: any) {
175
+ // eslint-disable-next-line
176
+ const alias = (_l: string) => {
177
+ const map: any = {
178
+ python3: "python",
179
+ };
180
+ if (map[_l])
181
+ return map[_l];
182
+ return _l;
183
+ };
184
+
185
+ // decode aliases
186
+ language = alias(language);
187
+
188
+ if (this.validLanguages[language])
189
+ return true;
190
+
191
+ Console.debug(`Validating engine for ${language} compilation`);
192
+ let result = shell.exec("learnpack plugins", { silent: true });
193
+
194
+ if (
195
+ result.code === 0 &&
196
+ result.stdout.includes(`learnpack-${language}`)
197
+ ) {
198
+ this.validLanguages[language] = true;
199
+ return true;
200
+ }
201
+
202
+ Console.info(`Language engine for ${language} not found, installing...`);
203
+ result = shell.exec(`learnpack plugins:install learnpack-${language}`, {
204
+ silent: true,
205
+ });
206
+ if (result.code === 0) {
207
+ socket.log(
208
+ "compiling",
209
+ "Installing the python compiler, you will have to reset the exercises after installation by writing on your terminal: $ learnpack run"
210
+ );
211
+ Console.info(
212
+ `Successfully installed the ${language} exercise engine, \n please start learnpack again by running the following command: \n ${chalk.white(
213
+ "$ learnpack start"
214
+ )}\n\n `
215
+ );
216
+ server.terminate();
217
+ return false;
218
+ }
219
+
220
+ this.validLanguages[language] = false;
221
+ socket.error(`Error installing ${language} exercise engine`);
222
+ Console.error(`Error installing ${language} exercise engine`);
223
+ Console.log(result.stdout);
224
+ throw InternalError(`Error installing ${language} exercise engine`);
225
+ },
226
+ clean: () => {
227
+ if (configObj.config) {
228
+ if (configObj.config.outputPath) {
229
+ rmSync(configObj.config.outputPath);
230
+ }
231
+
232
+ rmSync(configObj.config.dirPath + "/_app");
233
+ rmSync(configObj.config.dirPath + "/reports");
234
+ rmSync(configObj.config.dirPath + "/.session");
235
+ rmSync(configObj.config.dirPath + "/resets");
236
+
237
+ // clean tag gz
238
+ if (fs.existsSync(configObj.config.dirPath + "/app.tar.gz"))
239
+ fs.unlinkSync(configObj.config.dirPath + "/app.tar.gz");
240
+
241
+ if (fs.existsSync(configObj.config.dirPath + "/config.json"))
242
+ fs.unlinkSync(configObj.config.dirPath + "/config.json");
243
+
244
+ if (fs.existsSync(configObj.config.dirPath + "/vscode_queue.json"))
245
+ fs.unlinkSync(configObj.config.dirPath + "/vscode_queue.json");
246
+ }
247
+ },
248
+ getExercise: slug => {
249
+ Console.debug("ExercisePath Slug", slug);
250
+ const exercise = (configObj.exercises || []).find(
251
+ ex => ex.slug === slug
252
+ );
253
+ if (!exercise)
254
+ throw ValidationError(`Exercise ${slug} not found`);
255
+
256
+ return exercise;
257
+ },
258
+ getAllExercises: () => {
259
+ return configObj.exercises;
260
+ },
261
+ startExercise: function (slug: string) {
262
+ const exercise = this.getExercise(slug);
263
+
264
+ // set config.json with current exercise
265
+ configObj.currentExercise = exercise.slug;
266
+
267
+ this.save();
268
+
269
+ // eslint-disable-next-line
270
+ exercise.files.forEach((f: IFile) => {
271
+ if (configObj.config) {
272
+ const _path = configObj.config.outputPath + "/" + f.name;
273
+ if (f.hidden === false && fs.existsSync(_path))
274
+ fs.unlinkSync(_path);
275
+ }
276
+ });
277
+
278
+ return exercise;
279
+ },
280
+ noCurrentExercise: function () {
281
+ configObj.currentExercise = null;
282
+ this.save();
283
+ },
284
+ reset: slug => {
285
+ if (
286
+ configObj.config &&
287
+ !fs.existsSync(`${configObj.config.dirPath}/resets/` + slug)
288
+ )
289
+ throw ValidationError("Could not find the original files for " + slug);
290
+
291
+ const exercise = (configObj.exercises || []).find(
292
+ ex => ex.slug === slug
293
+ );
294
+ if (!exercise)
295
+ throw ValidationError(
296
+ `Exercise ${slug} not found on the configuration`
297
+ );
298
+
299
+ if (configObj.config) {
300
+ for (const fileName of fs.readdirSync(
301
+ `${configObj.config.dirPath}/resets/${slug}/`
302
+ )) {
303
+ const content = fs.readFileSync(
304
+ `${configObj.config?.dirPath}/resets/${slug}/${fileName}`
305
+ );
306
+ fs.writeFileSync(`${exercise.path}/${fileName}`, content);
307
+ }
308
+ }
309
+ },
310
+ buildIndex: function () {
311
+ Console.info("Building the exercise index...");
312
+
313
+ const isDirectory = (source: string) => {
314
+ const name = path.basename(source);
315
+ if (name === path.basename(configObj?.config?.dirPath || ""))
316
+ return false;
317
+ // ignore folders that start with a dot
318
+ if (name.charAt(0) === "." || name.charAt(0) === "_")
319
+ return false;
320
+
321
+ return fs.lstatSync(source).isDirectory();
322
+ };
323
+
324
+ const getDirectories = (source: string) =>
325
+ fs
326
+ .readdirSync(source)
327
+ .map(name => path.join(source, name))
328
+ .filter(isDirectory);
329
+ // add the .learn folder
330
+ if (!fs.existsSync(confPath.base))
331
+ fs.mkdirSync(confPath.base);
332
+ // add the outout folder where webpack will publish the the html/css/js files
333
+ if (
334
+ configObj.config &&
335
+ configObj.config.outputPath &&
336
+ !fs.existsSync(configObj.config.outputPath)
337
+ )
338
+ fs.mkdirSync(configObj.config.outputPath);
339
+
340
+ // TODO: we could use npm library front-mater to read the title of the exercises from the README.md
341
+ const grupedByDirectory = getDirectories(
342
+ configObj?.config?.exercisesPath || ""
343
+ );
344
+ configObj.exercises =
345
+ grupedByDirectory.length > 0 ?
346
+ grupedByDirectory.map((path, position) =>
347
+ exercise(path, position, configObj)
348
+ ) :
349
+ [exercise(configObj?.config?.exercisesPath || "", 0, configObj)];
350
+ this.save();
351
+ },
352
+ watchIndex: function (onChange: () => void) {
353
+ if (configObj.config && !configObj.config.exercisesPath)
354
+ throw ValidationError(
355
+ "No exercises directory to watch: " + configObj.config.exercisesPath
356
+ );
357
+
358
+ this.buildIndex();
359
+ watch(configObj?.config?.exercisesPath || "", onChange)
360
+ .then((/* eventname, filename */) => {
361
+ Console.debug("Changes detected on your exercises");
362
+ this.buildIndex();
363
+ if (onChange)
364
+ onChange();
365
+ })
366
+ .catch(error => {
367
+ throw error;
368
+ });
369
+ },
370
+ save: () => {
371
+ Console.debug("Saving configuration with: ", configObj);
372
+
373
+ // remove the duplicates form the actions array
374
+ // configObj.config.actions = [...new Set(configObj.config.actions)];
375
+ if (configObj.config) {
376
+ configObj.config.translations = [
377
+ ...new Set(configObj.config.translations),
378
+ ];
379
+
380
+ fs.writeFileSync(
381
+ configObj.config.dirPath + "/config.json",
382
+ JSON.stringify(configObj, null, 4)
383
+ );
384
+ }
385
+ },
386
+ } as IConfigManager;
387
+ };
388
+
389
+ function deepMerge(...sources: any): any {
390
+ let acc: any = {};
391
+ for (const source of sources) {
392
+ if (Array.isArray(source)) {
393
+ if (!Array.isArray(acc)) {
394
+ acc = [];
395
+ }
396
+
397
+ acc = [...source];
398
+ } else if (source instanceof Object) {
399
+ // eslint-disable-next-line
400
+ for (let [key, value] of Object.entries(source)) {
401
+ if (value instanceof Object && key in acc) {
402
+ value = deepMerge(acc[key], value);
403
+ }
404
+
405
+ if (value !== undefined)
406
+ acc = { ...acc, [key]: value };
407
+ }
408
+ }
409
+ }
410
+
411
+ return acc;
412
+ }