@learnpack/learnpack 2.0.24 → 2.1.2

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