@learnpack/learnpack 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. package/README.md +51 -398
  2. package/bin/run +14 -2
  3. package/oclif.manifest.json +1 -1
  4. package/package.json +135 -111
  5. package/src/commands/audit.ts +462 -0
  6. package/src/commands/clean.ts +29 -0
  7. package/src/commands/download.ts +62 -0
  8. package/src/commands/init.ts +169 -0
  9. package/src/commands/login.ts +42 -0
  10. package/src/commands/logout.ts +43 -0
  11. package/src/commands/publish.ts +107 -0
  12. package/src/commands/start.ts +229 -0
  13. package/src/commands/{test.js → test.ts} +19 -21
  14. package/src/index.ts +1 -0
  15. package/src/managers/config/allowed_files.ts +29 -0
  16. package/src/managers/config/defaults.ts +33 -0
  17. package/src/managers/config/exercise.ts +295 -0
  18. package/src/managers/config/index.ts +411 -0
  19. package/src/managers/file.ts +169 -0
  20. package/src/managers/gitpod.ts +84 -0
  21. package/src/managers/server/{index.js → index.ts} +26 -19
  22. package/src/managers/server/routes.ts +250 -0
  23. package/src/managers/session.ts +118 -0
  24. package/src/managers/socket.ts +239 -0
  25. package/src/managers/test.ts +83 -0
  26. package/src/models/action.ts +3 -0
  27. package/src/models/audit-errors.ts +4 -0
  28. package/src/models/config-manager.ts +23 -0
  29. package/src/models/config.ts +74 -0
  30. package/src/models/counter.ts +11 -0
  31. package/src/models/errors.ts +22 -0
  32. package/src/models/exercise-obj.ts +26 -0
  33. package/src/models/file.ts +5 -0
  34. package/src/models/findings.ts +18 -0
  35. package/src/models/flags.ts +10 -0
  36. package/src/models/front-matter.ts +11 -0
  37. package/src/models/gitpod-data.ts +19 -0
  38. package/src/models/language.ts +4 -0
  39. package/src/models/package.ts +7 -0
  40. package/src/models/plugin-config.ts +17 -0
  41. package/src/models/session.ts +26 -0
  42. package/src/models/socket.ts +48 -0
  43. package/src/models/status.ts +15 -0
  44. package/src/models/success-types.ts +1 -0
  45. package/src/plugin/command/compile.ts +17 -0
  46. package/src/plugin/command/test.ts +30 -0
  47. package/src/plugin/index.ts +6 -0
  48. package/src/plugin/plugin.ts +94 -0
  49. package/src/plugin/utils.ts +87 -0
  50. package/src/types/node-fetch.d.ts +1 -0
  51. package/src/ui/download.ts +71 -0
  52. package/src/utils/BaseCommand.ts +48 -0
  53. package/src/utils/SessionCommand.ts +48 -0
  54. package/src/utils/api.ts +194 -0
  55. package/src/utils/audit.ts +162 -0
  56. package/src/utils/console.ts +24 -0
  57. package/src/utils/errors.ts +117 -0
  58. package/src/utils/{exercisesQueue.js → exercisesQueue.ts} +12 -6
  59. package/src/utils/fileQueue.ts +198 -0
  60. package/src/utils/misc.ts +23 -0
  61. package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.es.md +2 -4
  62. package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.md +1 -2
  63. package/src/utils/templates/isolated/01-hello-world/README.es.md +1 -2
  64. package/src/utils/templates/isolated/01-hello-world/README.md +1 -2
  65. package/src/utils/templates/isolated/README.ejs +1 -1
  66. package/src/utils/templates/isolated/README.es.ejs +1 -1
  67. package/src/utils/validators.ts +18 -0
  68. package/src/utils/watcher.ts +27 -0
  69. package/plugin/command/compile.js +0 -17
  70. package/plugin/command/test.js +0 -29
  71. package/plugin/index.js +0 -6
  72. package/plugin/plugin.js +0 -71
  73. package/plugin/utils.js +0 -78
  74. package/src/commands/audit.js +0 -243
  75. package/src/commands/clean.js +0 -27
  76. package/src/commands/download.js +0 -52
  77. package/src/commands/hello.js +0 -20
  78. package/src/commands/init.js +0 -133
  79. package/src/commands/login.js +0 -45
  80. package/src/commands/logout.js +0 -39
  81. package/src/commands/publish.js +0 -78
  82. package/src/commands/start.js +0 -169
  83. package/src/index.js +0 -1
  84. package/src/managers/config/allowed_files.js +0 -12
  85. package/src/managers/config/defaults.js +0 -32
  86. package/src/managers/config/exercise.js +0 -212
  87. package/src/managers/config/index.js +0 -342
  88. package/src/managers/file.js +0 -137
  89. package/src/managers/server/routes.js +0 -151
  90. package/src/managers/session.js +0 -83
  91. package/src/managers/socket.js +0 -185
  92. package/src/managers/test.js +0 -77
  93. package/src/ui/download.js +0 -48
  94. package/src/utils/BaseCommand.js +0 -34
  95. package/src/utils/SessionCommand.js +0 -46
  96. package/src/utils/api.js +0 -164
  97. package/src/utils/audit.js +0 -114
  98. package/src/utils/console.js +0 -16
  99. package/src/utils/errors.js +0 -90
  100. package/src/utils/fileQueue.js +0 -194
  101. package/src/utils/misc.js +0 -26
  102. package/src/utils/validators.js +0 -15
  103. package/src/utils/watcher.js +0 -24
@@ -1,19 +1,25 @@
1
- const express = require("express");
2
- const Console = require("../../utils/console");
3
- const cors = require("cors");
4
- const shell = require("shelljs");
5
- const addRoutes = require("./routes.js");
6
- const cli = require("cli-ux").default;
7
-
8
- module.exports = async function (
9
- configObj,
10
- configManager,
1
+ import * as express from "express";
2
+ // eslint-disable-next-line
3
+ import * as cors from "cors";
4
+ import * as http from "http";
5
+ import Console from "../../utils/console";
6
+ import addRoutes from "./routes";
7
+ import cli from "cli-ux";
8
+ import { IConfigObj } from "../../models/config";
9
+ import { IConfigManager } from "../../models/config-manager";
10
+
11
+ export let TEST_SERVER: http.Server;
12
+
13
+ export default async function (
14
+ configObj: IConfigObj,
15
+ configManager: IConfigManager,
11
16
  isTestingEnvironment = false
12
17
  ) {
13
18
  const { config } = configObj;
14
- var app = express();
15
- var server = require("http").Server(app);
19
+ const app = express();
20
+ const server = require("http").Server(app);
16
21
  app.use(cors());
22
+
17
23
  // app.use(function(req, res, next) {
18
24
  // res.header("Access-Control-Allow-Origin", "*")
19
25
  // res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
@@ -24,20 +30,21 @@ module.exports = async function (
24
30
  // add all needed endpoints
25
31
  await addRoutes(app, configObj, configManager);
26
32
 
27
- server.listen(isTestingEnvironment ? 5000 : config.port, function () {
33
+ server.listen(isTestingEnvironment ? 5000 : config?.port, function () {
28
34
  if (!isTestingEnvironment) {
29
35
  Console.success(
30
36
  `Exercises are running 😃 Open your browser to start practicing!`
31
37
  );
32
38
  Console.success(`\n Open the exercise on this link:`);
33
- Console.log(` ${config.publicUrl}`);
34
- if (config.editor.mode === "standalone") cli.open(`${config.publicUrl}`);
39
+ Console.log(` ${config?.publicUrl}`);
40
+ if (config?.editor.mode === "standalone")
41
+ cli.open(`${config.publicUrl}`);
35
42
  }
36
43
  });
37
44
 
38
- const sockets = new Set();
45
+ const sockets: any = new Set();
39
46
 
40
- server.on("connection", (socket) => {
47
+ server.on("connection", (socket: any) => {
41
48
  sockets.add(socket);
42
49
 
43
50
  server.once("close", () => {
@@ -48,7 +55,7 @@ module.exports = async function (
48
55
  /**
49
56
  * Forcefully terminates HTTP server.
50
57
  */
51
- server.terminate = (callback) => {
58
+ server.terminate = (callback: void) => {
52
59
  for (const socket of sockets) {
53
60
  socket.destroy();
54
61
 
@@ -59,4 +66,4 @@ module.exports = async function (
59
66
  };
60
67
 
61
68
  return server;
62
- };
69
+ }
@@ -0,0 +1,250 @@
1
+ import Console from "../../utils/console";
2
+ import * as express from "express";
3
+ import * as fs from "fs";
4
+ import * as bodyParser from "body-parser";
5
+ import socket from "../socket";
6
+ import queue from "../../utils/fileQueue";
7
+ // import gitpod from '../gitpod'
8
+ import { detect, filterFiles } from "../config/exercise";
9
+ import { IFile } from "../../models/file";
10
+ import { IConfigObj, TEntries } from "../../models/config";
11
+ import { IConfigManager } from "../../models/config-manager";
12
+ import { IExercise } from "../../models/exercise-obj";
13
+
14
+ const withHandler =
15
+ (func: (req: express.Request, res: express.Response) => void) =>
16
+ (req: express.Request, res: express.Response) => {
17
+ try {
18
+ func(req, res);
19
+ } catch (error) {
20
+ Console.debug(error);
21
+ const _err = {
22
+ message: (error as TypeError).message || "There has been an error",
23
+ status: (error as any).status || 500,
24
+ type: (error as any).type || null,
25
+ };
26
+ Console.error(_err.message);
27
+
28
+ // send rep to the server
29
+ res.status(_err.status);
30
+ res.json(_err);
31
+ }
32
+ };
33
+
34
+ export default async function (
35
+ app: express.Application,
36
+ configObject: IConfigObj,
37
+ configManager: IConfigManager
38
+ ) {
39
+ const { config, exercises } = configObject;
40
+
41
+ const dispatcher = queue.dispatcher({
42
+ create: true,
43
+ path: `${config?.dirPath}/vscode_queue.json`,
44
+ });
45
+ app.get(
46
+ "/config",
47
+ withHandler((_: express.Request, res: express.Response) => {
48
+ res.json(configObject);
49
+ })
50
+ );
51
+
52
+ /**
53
+ * TODO: replicate a socket action, the request payload must be passed to the socket as well
54
+
55
+ const jsonBodyParser = bodyParser.json()
56
+
57
+ type ISocketActions = "addAllowed" | "removeAllowed" | "start" | "on" | "clean" | "ask" | "reload" | "openWindow" | "log" | "emit" | "ready" | "error" | "fatal" | "success" | "onTestingFinished";
58
+
59
+ app.post('/socket/:actionName', jsonBodyParser, withHandler((req, res) => {
60
+ if (socket[req.params.actionName as ISocketActions] instanceof Function) {
61
+ socket[req.params.actionName as ISocketActions](req.body ? req.body.data : null)
62
+ res.json({ "details": "Socket call executed sucessfully" })
63
+ } else res.status(400).json({ "details": `Socket action ${req.params.actionName} not found` })
64
+ }))
65
+ */
66
+
67
+ // symbolic link to maintain path compatiblity
68
+ const fetchStaticAsset = withHandler((req, res) => {
69
+ const filePath = `${config?.dirPath}/assets/${req.params.filePath}`;
70
+ if (!fs.existsSync(filePath))
71
+ throw new Error("File not found: " + filePath);
72
+ const content = fs.readFileSync(filePath);
73
+ res.write(content);
74
+ res.end();
75
+ });
76
+
77
+ app.get(
78
+ `${
79
+ config?.dirPath.indexOf("./") === 0 ?
80
+ config.dirPath.slice(1) :
81
+ config?.dirPath
82
+ }/assets/:filePath`,
83
+ fetchStaticAsset
84
+ );
85
+
86
+ app.get("/assets/:filePath", fetchStaticAsset);
87
+
88
+ app.get(
89
+ "/exercise",
90
+ withHandler((_: express.Request, res: express.Response) => {
91
+ res.json(exercises);
92
+ })
93
+ );
94
+
95
+ app.get(
96
+ "/exercise/:slug/readme",
97
+ withHandler(
98
+ (
99
+ { params: { slug }, query: { lang } }: express.Request,
100
+ res: express.Response
101
+ ) => {
102
+ const excercise: IExercise = configManager.getExercise(slug);
103
+
104
+ if (excercise) {
105
+ const readme = excercise.getReadme((lang as string) || null);
106
+ res.json(readme);
107
+ } else {
108
+ res.status(400);
109
+ }
110
+ }
111
+ )
112
+ );
113
+
114
+ app.get(
115
+ "/exercise/:slug/report",
116
+ withHandler(
117
+ ({ params: { slug } }: express.Request, res: express.Response) => {
118
+ const report = configManager.getExercise(slug).getTestReport();
119
+ res.json(JSON.stringify(report));
120
+ }
121
+ )
122
+ );
123
+
124
+ app.get(
125
+ "/exercise/:slug",
126
+ withHandler((req: express.Request, res: express.Response) => {
127
+ // no need to re-start exercise if it's already started
128
+ if (
129
+ configObject.currentExercise &&
130
+ req.params.slug === configObject.currentExercise
131
+ ) {
132
+ const exercise = configManager.getExercise(req.params.slug);
133
+ res.json(exercise);
134
+ return;
135
+ }
136
+
137
+ const exercise = configManager.startExercise(req.params.slug);
138
+ dispatcher.enqueue(dispatcher.events.START_EXERCISE, req.params.slug);
139
+
140
+ type TEntry = "python3" | "html" | "node" | "react" | "java";
141
+
142
+ // eslint-disable-next-line
143
+ const entries = new Set(Object.keys(config?.entries!).map(
144
+ lang => config?.entries[lang as TEntry]
145
+ ));
146
+ // if we are in incremental grading, the entry file can by dinamically detected
147
+ // based on the changes the student is making during the exercise
148
+ if (config?.grading === "incremental") {
149
+ const scanedFiles = fs.readdirSync("./");
150
+
151
+ // update the file hierarchy with updates
152
+ exercise.files = [...exercise.files.filter(f => f.name.includes("test.")), ...filterFiles(scanedFiles)];
153
+ Console.debug(`Exercise updated files: `, exercise.files);
154
+ }
155
+
156
+ const detected = detect(
157
+ configObject,
158
+ exercise.files
159
+ .filter(fileName => entries.has(fileName.name))
160
+ .map(f => f.name || f) as string[]
161
+ );
162
+
163
+ // if a new language for the testing engine is detected, we replace it
164
+ // if not we leave it as it was before
165
+ if (config?.language && !["", "auto"].includes(config?.language)) {
166
+ Console.debug(
167
+ `Exercise language ignored, instead imported from configuration ${config?.language}`
168
+ );
169
+ exercise.language = detected?.language;
170
+ } else if (
171
+ detected?.language &&
172
+ (!config?.language || config?.language === "auto")
173
+ ) {
174
+ Console.debug(
175
+ `Switching to ${detected.language} engine in this exercise`
176
+ );
177
+ exercise.language = detected.language;
178
+ }
179
+
180
+ // WARNING: has to be the FULL PATH to the entry path
181
+ // We need to detect entry in both gradings: Incremental and Isolate
182
+ exercise.entry = detected?.entry;
183
+ Console.debug(
184
+ `Exercise detected entry: ${detected?.entry} and language ${exercise.language}`
185
+ );
186
+
187
+ if (
188
+ !exercise.graded ||
189
+ config?.disableGrading ||
190
+ config?.disabledActions?.includes("test")
191
+ ) {
192
+ socket.removeAllowed("test");
193
+ } else {
194
+ socket.addAllowed("test");
195
+ }
196
+
197
+ if (!exercise.entry || config?.disabledActions?.includes("build")) {
198
+ socket.removeAllowed("build");
199
+ } else {
200
+ socket.addAllowed("build");
201
+ }
202
+
203
+ if (
204
+ exercise.files.filter(
205
+ (f: IFile) =>
206
+ !f.name.toLowerCase().includes("readme.") &&
207
+ !f.name.toLowerCase().includes("test.")
208
+ ).length === 0 ||
209
+ config?.disabledActions?.includes("reset")
210
+ ) {
211
+ socket.removeAllowed("reset");
212
+ } else if (!config?.disabledActions?.includes("reset")) {
213
+ socket.addAllowed("reset");
214
+ }
215
+
216
+ socket.log("ready");
217
+
218
+ res.json(exercise);
219
+ })
220
+ );
221
+
222
+ app.get(
223
+ "/exercise/:slug/file/:fileName",
224
+ withHandler((req: express.Request, res: express.Response) => {
225
+ res.write(
226
+ configManager.getExercise(req.params.slug).getFile(req.params.fileName)
227
+ );
228
+ res.end();
229
+ })
230
+ );
231
+
232
+ const textBodyParser = bodyParser.text();
233
+ app.put(
234
+ "/exercise/:slug/file/:fileName",
235
+ textBodyParser,
236
+ withHandler((req: express.Request, res: express.Response) => {
237
+ // const result =
238
+ configManager
239
+ .getExercise(req.params.slug)
240
+ .saveFile(req.params.fileName, req.body);
241
+ res.end();
242
+ })
243
+ );
244
+
245
+ if (config?.outputPath) {
246
+ app.use("/preview", express.static(config.outputPath));
247
+ }
248
+
249
+ app.use("/", express.static(`${config?.dirPath}/_app`));
250
+ }
@@ -0,0 +1,118 @@
1
+ import Console from '../utils/console'
2
+ import api from '../utils/api'
3
+
4
+ import v from 'validator'
5
+ import {ValidationError, InternalError} from '../utils/errors'
6
+ // import moment from 'moment'
7
+ import * as fs from 'fs'
8
+ import cli from 'cli-ux'
9
+ import * as storage from 'node-persist'
10
+
11
+ import {IPayload, ISession, IStartProps} from '../models/session'
12
+ import {IConfigObj} from '../models/config'
13
+
14
+ const Session: ISession = {
15
+ sessionStarted: false,
16
+ token: null,
17
+ config: null,
18
+ currentCohort: null,
19
+ initialize: async function () {
20
+ if (!this.sessionStarted) {
21
+ if (!this.config) {
22
+ throw InternalError('Configuration not found')
23
+ }
24
+
25
+ if (!fs.existsSync(this.config.dirPath)) {
26
+ fs.mkdirSync(this.config.dirPath)
27
+ }
28
+
29
+ await storage.init({dir: `${this.config.dirPath}/.session`})
30
+ this.sessionStarted = true
31
+ }
32
+
33
+ return true
34
+ },
35
+ setPayload: async function (value: IPayload) {
36
+ await this.initialize()
37
+ await storage.setItem('bc-payload', {token: this.token, ...value})
38
+ Console.debug('Payload successfuly found and set for ' + value.email)
39
+ return true
40
+ },
41
+ getPayload: async function () {
42
+ await this.initialize()
43
+ let payload = null
44
+ try {
45
+ payload = await storage.getItem('bc-payload')
46
+ } catch (error) {
47
+ // TODO: Remove it
48
+ console.log(error)
49
+ Console.debug('Error retriving session payload')
50
+ }
51
+
52
+ return payload
53
+ },
54
+ isActive: function () {
55
+ /* if (this.token) {
56
+ return true
57
+ } else {
58
+ return false
59
+ } */
60
+ return !!this.token
61
+ },
62
+ get: async function (configObj?: IConfigObj) {
63
+ if (configObj && configObj.config) {
64
+ this.config = configObj.config
65
+ }
66
+
67
+ await this.sync()
68
+ if (!this.isActive()) {
69
+ return null
70
+ }
71
+
72
+ const payload = await this.getPayload()
73
+
74
+ return {
75
+ payload,
76
+ token: this.token,
77
+ }
78
+ },
79
+ login: async function () {
80
+ const email = await cli.prompt('What is your email?')
81
+ if (!v.isEmail(email)) {
82
+ throw ValidationError('Invalid email')
83
+ }
84
+
85
+ const password = await cli.prompt('What is your password?', {
86
+ type: 'hide',
87
+ })
88
+
89
+ const data = await api.login(email, password)
90
+ if (data) {
91
+ this.start({token: data.token, payload: data})
92
+ }
93
+ },
94
+ sync: async function () {
95
+ const payload = await this.getPayload()
96
+ if (payload) {
97
+ this.token = payload.token
98
+ }
99
+ },
100
+ start: async function ({token, payload = null}: IStartProps) {
101
+ if (!token) {
102
+ throw new Error('A token and email is needed to start a session')
103
+ }
104
+
105
+ this.token = token
106
+
107
+ if (payload && (await this.setPayload(payload))) {
108
+ Console.success(`Successfully logged in as ${payload.email}`)
109
+ }
110
+ },
111
+ destroy: async function () {
112
+ await storage.clear()
113
+ this.token = null
114
+ Console.success('You have logged out')
115
+ },
116
+ }
117
+
118
+ export default Session
@@ -0,0 +1,239 @@
1
+ import {Socket, Server} from 'socket.io'
2
+ import Console from '../utils/console'
3
+ import queue from '../utils/fileQueue'
4
+
5
+ import {ISocket, TPossibleActions} from '../models/socket'
6
+ import {IConfig} from '../models/config'
7
+ import {ICallback, TAction} from '../models/action'
8
+ import {IExercise, IExerciseData} from '../models/exercise-obj'
9
+ import {TStatus} from '../models/status'
10
+ import {TSuccessType} from '../models/success-types'
11
+ import * as http from 'http'
12
+
13
+ const SocketManager: ISocket = {
14
+ socket: null,
15
+ config: null,
16
+ allowedActions: [],
17
+ possibleActions: ['build', 'reset', 'test', 'tutorial'],
18
+ isTestingEnvironment: false,
19
+ actionCallBacks: {
20
+ clean: (_, s: { logs: Array<string> }) => {
21
+ s.logs = []
22
+ },
23
+ },
24
+ addAllowed: function (actions: Array<TPossibleActions> | TPossibleActions) {
25
+ if (!Array.isArray(actions))
26
+ actions = [actions]
27
+
28
+ // avoid adding the "test" action if grading is disabled
29
+ if (
30
+ actions.includes('test') &&
31
+ this.config?.disabledActions?.includes('test')
32
+ ) {
33
+ actions = actions.filter((a: TPossibleActions) => a !== 'test')
34
+ }
35
+
36
+ this.allowedActions = [
37
+ ...(this.allowedActions || []).filter(
38
+ (a: TPossibleActions) => !actions.includes(a),
39
+ ),
40
+ ...actions,
41
+ ]
42
+ },
43
+ removeAllowed: function (
44
+ actions: Array<TPossibleActions> | TPossibleActions,
45
+ ) {
46
+ if (!Array.isArray(actions)) {
47
+ actions = [actions]
48
+ }
49
+
50
+ this.allowedActions = (this.allowedActions || []).filter(
51
+ (a: TPossibleActions) => !actions.includes(a),
52
+ )
53
+ },
54
+ start: function (
55
+ config: IConfig,
56
+ server: http.Server,
57
+ isTestingEnvironment = false,
58
+ ) {
59
+ this.config = config
60
+ this.isTestingEnvironment = isTestingEnvironment
61
+
62
+ this.socket = new Server(server, {allowEIO3: true})
63
+
64
+ this.allowedActions = this.config?.disabledActions?.includes('test') ||
65
+ this.config?.disableGrading ? this.possibleActions.filter(
66
+ a => !this.config?.disabledActions?.includes(a) && a !== 'test',
67
+ ) : this.possibleActions.filter(
68
+ a => !this.allowedActions?.includes(a),
69
+ )
70
+
71
+ if (this.config?.grading === 'incremental') {
72
+ this.removeAllowed('reset')
73
+ }
74
+
75
+ if (this.socket) {
76
+ this.socket.on('connection', (socket: Socket) => {
77
+ Console.debug(
78
+ 'Connection with client successfully established',
79
+ this.allowedActions,
80
+ )
81
+ if (!this.isTestingEnvironment) {
82
+ this.log('ready', ['Ready to compile or test...'])
83
+ }
84
+
85
+ socket.on(
86
+ 'compiler',
87
+ ({action, data}: { action: string; data: IExerciseData }) => {
88
+ this.emit('clean', 'pending', ['Working...'])
89
+
90
+ if (typeof data.exerciseSlug === 'undefined') {
91
+ this.log('internal-error', ['No exercise slug specified'])
92
+ Console.error('No exercise slug especified')
93
+ return
94
+ }
95
+
96
+ if (
97
+ this.actionCallBacks &&
98
+ typeof this.actionCallBacks[action] === 'function'
99
+ ) {
100
+ this.actionCallBacks[action](data)
101
+ } else {
102
+ this.log('internal-error', ['Uknown action ' + action])
103
+ }
104
+ },
105
+ )
106
+ })
107
+ }
108
+ },
109
+ on: function (action: TAction, callBack: ICallback) {
110
+ if (this.actionCallBacks) {
111
+ this.actionCallBacks[action] = callBack
112
+ }
113
+ },
114
+ clean: function (_ = 'pending', logs = []) {
115
+ this.emit('clean', 'pending', logs)
116
+ },
117
+ ask: function (questions = []) {
118
+ return new Promise((resolve, _) => {
119
+ this.emit('ask', 'pending', ['Waiting for input...'], questions)
120
+ this.on('input', ({inputs}: any) => {
121
+ // Workaround to fix issue because null inputs
122
+ let isNull = false
123
+ // eslint-disable-next-line
124
+ inputs.forEach((input: any) => {
125
+ if (input === null) {
126
+ isNull = true
127
+ }
128
+ })
129
+
130
+ if (!isNull) {
131
+ resolve(inputs)
132
+ }
133
+ })
134
+ })
135
+ },
136
+ reload: function (
137
+ files: Array<string> | null = null,
138
+ exercises: Array<string> | null = null,
139
+ ) {
140
+ this.emit(
141
+ 'reload',
142
+ files?.join('') || '' /* TODO: Check it out this */,
143
+ exercises!,
144
+ )
145
+ },
146
+ openWindow: function (url = '') {
147
+ queue.dispatcher().enqueue(queue.events.OPEN_WINDOW, url)
148
+ this.emit(
149
+ queue.events.OPEN_WINDOW as TAction,
150
+ 'ready',
151
+ [`Opening ${url}`],
152
+ [],
153
+ [],
154
+ url,
155
+ )
156
+ },
157
+ log: function (
158
+ status: TStatus,
159
+ messages: string | Array<string> = [],
160
+ report: Array<string> = [],
161
+ data: any = null,
162
+ ) {
163
+ this.emit('log', status, messages, [], report, data)
164
+ Console.log(messages)
165
+ },
166
+ emit: function (
167
+ action: TAction,
168
+ status: TStatus | string = 'ready',
169
+ logs: string | Array<string> = [],
170
+ inputs: Array<string> = [],
171
+ report: Array<string> = [],
172
+ data: any = null,
173
+ ) {
174
+ if (
175
+ this.config?.compiler &&
176
+ ['webpack', 'vanillajs', 'vue', 'react', 'css', 'html'].includes(
177
+ this.config?.compiler,
178
+ )
179
+ ) {
180
+ if (['compiler-success', 'compiler-warning'].includes(status))
181
+ this.addAllowed('preview')
182
+ if (['compiler-error'].includes(status) || action === 'ready')
183
+ this.removeAllowed('preview')
184
+ }
185
+
186
+ if (this.config?.grading === 'incremental') {
187
+ this.removeAllowed('reset')
188
+ }
189
+
190
+ // eslint-disable-next-line
191
+ this.config?.disabledActions?.forEach((a) => this.removeAllowed(a));
192
+
193
+ this.socket?.emit('compiler', {
194
+ action,
195
+ status,
196
+ logs,
197
+ allowed: this.allowedActions,
198
+ inputs,
199
+ report,
200
+ data,
201
+ })
202
+ },
203
+
204
+ ready: function (message: string) {
205
+ this.log('ready', [message])
206
+ },
207
+ success: function (type: TSuccessType, stdout: string) {
208
+ const types = ['compiler', 'testing']
209
+ if (!types.includes(type))
210
+ this.fatal(`Invalid socket success type "${type}" on socket`)
211
+ else if (stdout === '')
212
+ this.log((type + '-success') as TSuccessType, [
213
+ 'No stdout to display on the console',
214
+ ])
215
+ else
216
+ this.log((type + '-success') as TSuccessType, [stdout])
217
+ },
218
+ error: function (type: TStatus, stdout: string) {
219
+ console.error('Socket error: ' + type, stdout)
220
+ this.log(type, [stdout])
221
+
222
+ if (this.isTestingEnvironment) {
223
+ this.onTestingFinished({
224
+ result: 'failed',
225
+ })
226
+ }
227
+ },
228
+ fatal: function (msg: string) {
229
+ this.log('internal-error', [msg])
230
+ throw msg
231
+ },
232
+ onTestingFinished: function (result: any) {
233
+ if (this.config?.testingFinishedCallback) {
234
+ this.config.testingFinishedCallback(result)
235
+ }
236
+ },
237
+ }
238
+
239
+ export default SocketManager