@learnpack/learnpack 5.0.275 → 5.0.277

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +409 -409
  2. package/lib/commands/audit.js +15 -15
  3. package/lib/commands/breakToken.js +19 -19
  4. package/lib/commands/clean.js +3 -3
  5. package/lib/commands/init.js +41 -41
  6. package/lib/commands/logout.js +3 -3
  7. package/lib/commands/publish.js +5 -10
  8. package/lib/commands/serve.js +55 -2
  9. package/lib/creatorDist/assets/index-BfLyIQVh.js +10343 -10224
  10. package/lib/managers/config/index.js +77 -77
  11. package/lib/utils/api.d.ts +1 -1
  12. package/lib/utils/api.js +12 -9
  13. package/lib/utils/creatorUtilities.js +14 -14
  14. package/lib/utils/export/epub.d.ts +2 -0
  15. package/lib/utils/export/epub.js +298 -0
  16. package/lib/utils/export/index.d.ts +3 -0
  17. package/lib/utils/export/index.js +7 -0
  18. package/lib/utils/export/scorm.d.ts +2 -0
  19. package/lib/utils/export/scorm.js +84 -0
  20. package/lib/utils/export/shared.d.ts +4 -0
  21. package/lib/utils/export/shared.js +61 -0
  22. package/lib/utils/export/types.d.ts +15 -0
  23. package/lib/utils/export/types.js +2 -0
  24. package/package.json +2 -1
  25. package/src/commands/audit.ts +487 -487
  26. package/src/commands/breakToken.ts +67 -67
  27. package/src/commands/clean.ts +30 -30
  28. package/src/commands/init.ts +650 -650
  29. package/src/commands/logout.ts +38 -38
  30. package/src/commands/publish.ts +20 -25
  31. package/src/commands/serve.ts +69 -4
  32. package/src/commands/start.ts +333 -333
  33. package/src/commands/translate.ts +123 -123
  34. package/src/creator/README.md +54 -54
  35. package/src/creator/eslint.config.js +7 -7
  36. package/src/creator/src/components/syllabus/ContentIndex.tsx +312 -312
  37. package/src/creator/src/i18n.ts +28 -28
  38. package/src/creator/src/index.css +217 -217
  39. package/src/creator/src/locales/en.json +126 -126
  40. package/src/creator/src/locales/es.json +126 -126
  41. package/src/creator/src/utils/configTypes.ts +122 -122
  42. package/src/creator/src/utils/constants.ts +13 -13
  43. package/src/creator/src/utils/creatorUtils.ts +46 -46
  44. package/src/creator/src/utils/eventBus.ts +2 -2
  45. package/src/creator/src/utils/lib.ts +468 -468
  46. package/src/creator/src/utils/socket.ts +61 -61
  47. package/src/creator/src/utils/store.ts +222 -222
  48. package/src/creator/src/vite-env.d.ts +1 -1
  49. package/src/creator/vite.config.ts +13 -13
  50. package/src/creatorDist/assets/index-BfLyIQVh.js +10343 -10224
  51. package/src/managers/config/defaults.ts +49 -49
  52. package/src/managers/config/exercise.ts +364 -364
  53. package/src/managers/config/index.ts +775 -775
  54. package/src/managers/file.ts +236 -236
  55. package/src/managers/server/routes.ts +554 -554
  56. package/src/managers/session.ts +182 -182
  57. package/src/managers/telemetry.ts +188 -188
  58. package/src/models/action.ts +13 -13
  59. package/src/models/config-manager.ts +28 -28
  60. package/src/models/config.ts +106 -106
  61. package/src/models/creator.ts +47 -47
  62. package/src/models/exercise-obj.ts +30 -30
  63. package/src/models/session.ts +39 -39
  64. package/src/models/socket.ts +61 -61
  65. package/src/models/status.ts +16 -16
  66. package/src/ui/_app/app.css +1 -1
  67. package/src/ui/_app/app.js +400 -397
  68. package/src/ui/app.tar.gz +0 -0
  69. package/src/utils/BaseCommand.ts +56 -56
  70. package/src/utils/api.ts +53 -39
  71. package/src/utils/audit.ts +392 -392
  72. package/src/utils/checkNotInstalled.ts +267 -267
  73. package/src/utils/configBuilder.ts +82 -82
  74. package/src/utils/convertCreds.js +34 -34
  75. package/src/utils/creatorUtilities.ts +504 -504
  76. package/src/utils/export/README.md +178 -0
  77. package/src/utils/export/epub.ts +400 -0
  78. package/src/utils/export/index.ts +3 -0
  79. package/src/utils/export/scorm.ts +121 -0
  80. package/src/utils/export/shared.ts +61 -0
  81. package/src/utils/export/types.ts +17 -0
  82. package/src/utils/incrementVersion.js +74 -74
  83. package/src/utils/misc.ts +58 -58
  84. package/src/utils/rigoActions.ts +500 -500
  85. package/src/utils/sidebarGenerator.ts +195 -195
  86. package/src/utils/templates/epub/epub.css +133 -0
  87. package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  88. package/src/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
  89. package/src/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -0
  90. package/src/utils/templates/scorm/config/api.js +175 -0
  91. package/src/utils/templates/scorm/config/index.html +210 -0
  92. package/src/utils/templates/scorm/ims_xml.xsd +1 -0
  93. package/src/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -0
  94. package/src/utils/templates/scorm/imsmanifest.xml +38 -0
  95. package/src/utils/templates/scorm/imsmd_rootv1p2p1.xsd +573 -0
@@ -1,364 +1,364 @@
1
- import * as p from "path"
2
- // import frontMatter from 'front-matter'
3
- import * as fs from "fs"
4
- import Console from "../../utils/console"
5
- import allowed from "./allowed_files"
6
-
7
- import { IConfigObj } from "../../models/config"
8
- import { IFile } from "../../models/file"
9
- import { IExercise } from "../../models/exercise-obj"
10
-
11
- // function processQuestions(markdown: string): {
12
- // updatedMarkdown: string
13
- // questions: { id: string; examples: string[] }[]
14
- // } {
15
- // // Regular expression to find code blocks with language "question"
16
- // const questionRegex = /```question([^`]+?)```/g
17
- // const questions: { id: string; examples: string[] }[] = []
18
-
19
- // // Function to generate a random ID
20
- // const generateId = () => `id-${Math.random().toString(36).substr(2, 9)}`
21
-
22
- // // Replace function to modify the matched question block
23
- // const updatedMarkdown = markdown.replace(
24
- // questionRegex,
25
- // (match: string, properties: string) => {
26
- // // Check for existing id in the properties
27
- // const idMatch = properties.match(/id="([^"]+)"/)
28
- // const newId = idMatch ? idMatch[1] : generateId() // Use existing id or generate new one
29
-
30
- // // Update the properties by adding or updating the id
31
- // let updatedProperties = `id="${newId}"${properties.replace(
32
- // /id="[^"]+"/,
33
- // ""
34
- // )}`
35
-
36
- // // Make sure the properties are separated by a single space
37
- // updatedProperties = updatedProperties.replace(/\s+/g, " ").trim()
38
-
39
- // questions.push({ id: newId, examples: [] })
40
-
41
- // return `\`\`\`question ${updatedProperties}
42
- // \`\`\``
43
- // }
44
- // )
45
-
46
- // return { updatedMarkdown, questions }
47
- // }
48
- // eslint-disable-next-line
49
- const frontMatter = require("front-matter")
50
-
51
- export const exercise = (
52
- path: string,
53
- position: number,
54
- configObject: IConfigObj
55
- ): IExercise => {
56
- const { config, exercises } = configObject
57
- let slug = p.basename(path)
58
-
59
- if (!validateExerciseDirectoryName(slug)) {
60
- Console.error(
61
- `Exercise directory ${slug} has an invalid name, it has to start with two or three digits followed by words separated by underscors or hyphen (no white spaces). e.g: 01.12-hello-world`
62
- )
63
- }
64
-
65
- // get all the files
66
- const files = fs.readdirSync(path)
67
-
68
- /**
69
- * build the translation array like:
70
- {
71
- "us": "path/to/Readme.md",
72
- "es": "path/to/Readme.es.md"
73
- }
74
- */
75
- const translations: { [key: string]: string } = {}
76
- for (const file of files.filter(file =>
77
- file.toLowerCase().includes("readme")
78
- )) {
79
- const parts = file.split(".")
80
-
81
- // const content = fs.readFileSync(path + "/" + file, "utf8")
82
- // const { updatedMarkdown, questions } = processQuestions(content)
83
-
84
- // if (questions.length > 0) {
85
- // fs.writeFileSync(path + "/" + file, updatedMarkdown, "utf8")
86
- // }
87
-
88
- if (parts.length === 3) translations[parts[1]] = file
89
- else translations.us = file
90
- }
91
-
92
- // if the slug is a dot, it means there is not "exercises" folder, and its just a single README.md
93
- if (slug === ".") slug = "default-index"
94
-
95
- const detected = detect(configObject, files)
96
-
97
- const exerciseObj: IExercise = {
98
- position,
99
- path,
100
- slug,
101
- translations,
102
- language: detected?.language,
103
- entry: detected?.entry ? path + "/" + detected.entry : null, // full path to the exercise entry
104
- title: slug || "Exercise",
105
- graded: files.some(
106
- (file: any) =>
107
- file.toLowerCase().startsWith("test.") ||
108
- file.toLowerCase().startsWith("tests.")
109
- ),
110
- files: filterFiles(files, path),
111
- // if the exercises was on the config before I may keep the status done
112
- done:
113
- Array.isArray(exercises) &&
114
- typeof exercises[position] !== "undefined" &&
115
- path.slice(Math.max(0, path.indexOf("exercises/") + 10)) ===
116
- exercises[position].slug ?
117
- exercises[position].done :
118
- false,
119
- getReadme: function (lang = null) {
120
- if (lang === "en" || lang === "us") lang = null // <-- english is default, no need to append it to the file name
121
-
122
- if (!fs.existsSync(`${this.path}/README${lang ? "." + lang : ""}.md`)) {
123
- Console.error(
124
- `Language ${lang} not found for exercise ${slug}, switching to default language`
125
- )
126
-
127
- if (lang) lang = null
128
-
129
- if (!fs.existsSync(`${this.path}/README${lang ? "." + lang : ""}.md`))
130
- throw new Error(
131
- "Readme file not found for exercise: " + this.path + "/README.md"
132
- )
133
- }
134
-
135
- const content = fs.readFileSync(
136
- `${this.path}/README${lang ? "." + lang : ""}.md`,
137
- "utf8"
138
- )
139
- const attr = frontMatter(content)
140
- return attr
141
- },
142
- getFile: function (name: string) {
143
- const file: IFile | undefined = this.files.find(
144
- (f: IFile) => f.name === name
145
- )
146
-
147
- if (!file || !fs.existsSync(file.path)) {
148
- throw new Error(`File not found: + ${file?.path}`)
149
- } else if (fs.lstatSync(file.path).isDirectory()) {
150
- return (
151
- "Error: This is not a file to be read, but a directory: " + file.path
152
- )
153
- }
154
-
155
- // get file content
156
- const content = fs.readFileSync(file.path)
157
-
158
- // create reset folder
159
- if (!fs.existsSync(`${config?.dirPath}/resets`))
160
- fs.mkdirSync(`${config?.dirPath}/resets`)
161
- if (!fs.existsSync(`${config?.dirPath}/resets/` + this.slug)) {
162
- fs.mkdirSync(`${config?.dirPath}/resets/` + this.slug)
163
- }
164
-
165
- for (const _file of this.files) {
166
- const stats = fs.statSync(_file.path)
167
- if (stats.isDirectory() || _file.hidden) continue
168
- const fileContent = fs.readFileSync(_file.path)
169
- if (
170
- !fs.existsSync(`${config?.dirPath}/resets/${this.slug}/${_file.name}`)
171
- ) {
172
- fs.writeFileSync(
173
- `${config?.dirPath}/resets/${this.slug}/${_file.name}`,
174
- fileContent
175
- )
176
- }
177
- }
178
-
179
- return content
180
- },
181
-
182
- renameFolder: function (newName: string) {
183
- if (!config?.dirPath) {
184
- throw new Error("No config directory found")
185
- }
186
-
187
- try {
188
- const newPath = p.join(config?.exercisesPath, newName)
189
- fs.renameSync(this.path, newPath)
190
- this.path = newPath
191
- this.slug = newName
192
- this.title = newName
193
- } catch (error) {
194
- console.log(error)
195
- throw new Error("Failed to rename exercise: " + error)
196
- }
197
- },
198
- saveFile: function (name: string, content: string) {
199
- const file: IFile | undefined = this.files.find(
200
- (f: IFile) => f.name === name
201
- )
202
-
203
- if (file) {
204
- if (!fs.existsSync(file.path)) {
205
- throw new Error("File not found: " + file.path)
206
- }
207
-
208
- return fs.writeFileSync(file.path, content, "utf8")
209
- }
210
- },
211
- getTestReport: function () {
212
- const _path = `${configObject?.confPath?.base}/reports/${this.slug}.json`
213
-
214
- if (!fs.existsSync(_path)) return {}
215
-
216
- const content = fs.readFileSync(_path)
217
- const data = JSON.parse(`${content}`)
218
- return data
219
- },
220
- }
221
-
222
- return exerciseObj
223
- }
224
-
225
- export const validateExerciseDirectoryName = (str: string) => {
226
- if (str === "./") return true
227
- // TODO: Add nameValidationREgex from the config
228
- const regex = /^(\d{2,3}(\.\d{1,2})?-([\dA-Za-z]+(-|_)?)+)$/
229
- return regex.test(str)
230
- }
231
-
232
- export const isCodable = (str: string) => {
233
- const extension = p.extname(str)
234
- return allowed.extensions.includes(extension.slice(1).toLowerCase())
235
- }
236
-
237
- const isNotConfiguration = (str: string) => {
238
- return !allowed.names.includes(str)
239
- }
240
-
241
- export const shouldBeVisible = function (file: IFile) {
242
- return (
243
- // doest not have "test." on their name
244
- !file.name.toLocaleLowerCase().includes("test.") &&
245
- !file.name.toLocaleLowerCase().includes("tests.") &&
246
- !file.name.toLocaleLowerCase().includes(".hide.") &&
247
- // ignore hidden files
248
- file.name.charAt(0) !== "." &&
249
- // ignore learn.json and bc.json
250
- !file.name.toLocaleLowerCase().includes("learn.json") &&
251
- !file.name.toLocaleLowerCase().includes("bc.json") &&
252
- // ignore images, videos, vectors, etc.
253
- isCodable(file.name) &&
254
- isNotConfiguration(file.name) &&
255
- // readme's and directories
256
- !file.name.toLowerCase().includes("readme.") &&
257
- !isDirectory(file.path) &&
258
- file.name.charAt(0) !== "_"
259
- )
260
- }
261
-
262
- export const isDirectory = (source: string) => {
263
- // if(path.basename(source) === path.basename(config.dirPath)) return false
264
- return fs.lstatSync(source).isDirectory()
265
- }
266
-
267
- export const detect = (
268
- configObject: IConfigObj | undefined,
269
- files: Array<string>
270
- ) => {
271
- if (!configObject) {
272
- return
273
- }
274
-
275
- const { config } = configObject
276
-
277
- if (!config)
278
- throw new Error("No configuration found during the engine detection")
279
-
280
- if (!config.entries)
281
- throw new Error(
282
- "No configuration found for entries, please add a 'entries' object with the default file name for your exercise entry file that is going to be used while compiling, for example: index.html for html, app.py for python3, etc."
283
- )
284
- // A language was found on the config object, but this language will only be used as last resort, learnpack will try to guess each exercise language independently based on file extension (js, jsx, html, etc.)
285
-
286
- let hasFiles = files.filter(f => f.includes(".py"))
287
- if (hasFiles.length > 0)
288
- return {
289
- language: "python3",
290
- entry: hasFiles.find(f => config.entries.python3 === f),
291
- }
292
-
293
- hasFiles = files.filter(f => f.includes(".java"))
294
- if (hasFiles.length > 0)
295
- return {
296
- language: "java",
297
- entry: hasFiles.find(f => config.entries.java === f),
298
- }
299
-
300
- hasFiles = files.filter(f => f.includes(".jsx"))
301
- if (hasFiles.length > 0)
302
- return {
303
- language: "react",
304
- entry: hasFiles.find(f => config.entries.react === f),
305
- }
306
- const hasHTML = files.filter(f => f.includes("index.html"))
307
- const hasIndexJS = files.find(f => f.includes("index.js"))
308
- const hasJS = files.filter(f => f.includes(".js"))
309
- // angular, vue, vanillajs needs to have at least 2 files (html,css,js),
310
- // the test.js and the entry file in js
311
- // if not its just another HTML
312
-
313
- if (hasIndexJS && hasHTML.length > 0)
314
- return {
315
- language: "vanillajs",
316
- entry: hasIndexJS,
317
- }
318
- if (hasHTML.length > 0)
319
- return {
320
- language: "html",
321
- entry: hasHTML.find(f => config.entries.html === f),
322
- }
323
- if (hasJS.length > 0)
324
- return {
325
- language: "node",
326
- entry: hasJS.find(f => config.entries.node === f),
327
- }
328
-
329
- return {
330
- language: null,
331
- entry: null,
332
- }
333
- }
334
-
335
- export const filterFiles = (files: Array<string>, basePath = ".") =>
336
- files
337
- .map((ex: string) => ({
338
- path: basePath + "/" + ex,
339
- name: ex,
340
- hidden: !shouldBeVisible({
341
- name: ex,
342
- path: basePath + "/" + ex,
343
- } as IFile),
344
- }))
345
- .sort((f1, f2) => {
346
- const score: { [key: string]: number } = {
347
- // sorting priority
348
- "index.html": 1,
349
- "styles.css": 2,
350
- "styles.scss": 2,
351
- "style.css": 2,
352
- "style.scss": 2,
353
- "index.css": 2,
354
- "index.scss": 2,
355
- "index.js": 3,
356
- }
357
- return score[f1.name] < score[f2.name] ? -1 : 1
358
- })
359
-
360
- export default {
361
- exercise,
362
- detect,
363
- filterFiles,
364
- }
1
+ import * as p from "path"
2
+ // import frontMatter from 'front-matter'
3
+ import * as fs from "fs"
4
+ import Console from "../../utils/console"
5
+ import allowed from "./allowed_files"
6
+
7
+ import { IConfigObj } from "../../models/config"
8
+ import { IFile } from "../../models/file"
9
+ import { IExercise } from "../../models/exercise-obj"
10
+
11
+ // function processQuestions(markdown: string): {
12
+ // updatedMarkdown: string
13
+ // questions: { id: string; examples: string[] }[]
14
+ // } {
15
+ // // Regular expression to find code blocks with language "question"
16
+ // const questionRegex = /```question([^`]+?)```/g
17
+ // const questions: { id: string; examples: string[] }[] = []
18
+
19
+ // // Function to generate a random ID
20
+ // const generateId = () => `id-${Math.random().toString(36).substr(2, 9)}`
21
+
22
+ // // Replace function to modify the matched question block
23
+ // const updatedMarkdown = markdown.replace(
24
+ // questionRegex,
25
+ // (match: string, properties: string) => {
26
+ // // Check for existing id in the properties
27
+ // const idMatch = properties.match(/id="([^"]+)"/)
28
+ // const newId = idMatch ? idMatch[1] : generateId() // Use existing id or generate new one
29
+
30
+ // // Update the properties by adding or updating the id
31
+ // let updatedProperties = `id="${newId}"${properties.replace(
32
+ // /id="[^"]+"/,
33
+ // ""
34
+ // )}`
35
+
36
+ // // Make sure the properties are separated by a single space
37
+ // updatedProperties = updatedProperties.replace(/\s+/g, " ").trim()
38
+
39
+ // questions.push({ id: newId, examples: [] })
40
+
41
+ // return `\`\`\`question ${updatedProperties}
42
+ // \`\`\``
43
+ // }
44
+ // )
45
+
46
+ // return { updatedMarkdown, questions }
47
+ // }
48
+ // eslint-disable-next-line
49
+ const frontMatter = require("front-matter")
50
+
51
+ export const exercise = (
52
+ path: string,
53
+ position: number,
54
+ configObject: IConfigObj
55
+ ): IExercise => {
56
+ const { config, exercises } = configObject
57
+ let slug = p.basename(path)
58
+
59
+ if (!validateExerciseDirectoryName(slug)) {
60
+ Console.error(
61
+ `Exercise directory ${slug} has an invalid name, it has to start with two or three digits followed by words separated by underscors or hyphen (no white spaces). e.g: 01.12-hello-world`
62
+ )
63
+ }
64
+
65
+ // get all the files
66
+ const files = fs.readdirSync(path)
67
+
68
+ /**
69
+ * build the translation array like:
70
+ {
71
+ "us": "path/to/Readme.md",
72
+ "es": "path/to/Readme.es.md"
73
+ }
74
+ */
75
+ const translations: { [key: string]: string } = {}
76
+ for (const file of files.filter(file =>
77
+ file.toLowerCase().includes("readme")
78
+ )) {
79
+ const parts = file.split(".")
80
+
81
+ // const content = fs.readFileSync(path + "/" + file, "utf8")
82
+ // const { updatedMarkdown, questions } = processQuestions(content)
83
+
84
+ // if (questions.length > 0) {
85
+ // fs.writeFileSync(path + "/" + file, updatedMarkdown, "utf8")
86
+ // }
87
+
88
+ if (parts.length === 3) translations[parts[1]] = file
89
+ else translations.us = file
90
+ }
91
+
92
+ // if the slug is a dot, it means there is not "exercises" folder, and its just a single README.md
93
+ if (slug === ".") slug = "default-index"
94
+
95
+ const detected = detect(configObject, files)
96
+
97
+ const exerciseObj: IExercise = {
98
+ position,
99
+ path,
100
+ slug,
101
+ translations,
102
+ language: detected?.language,
103
+ entry: detected?.entry ? path + "/" + detected.entry : null, // full path to the exercise entry
104
+ title: slug || "Exercise",
105
+ graded: files.some(
106
+ (file: any) =>
107
+ file.toLowerCase().startsWith("test.") ||
108
+ file.toLowerCase().startsWith("tests.")
109
+ ),
110
+ files: filterFiles(files, path),
111
+ // if the exercises was on the config before I may keep the status done
112
+ done:
113
+ Array.isArray(exercises) &&
114
+ typeof exercises[position] !== "undefined" &&
115
+ path.slice(Math.max(0, path.indexOf("exercises/") + 10)) ===
116
+ exercises[position].slug ?
117
+ exercises[position].done :
118
+ false,
119
+ getReadme: function (lang = null) {
120
+ if (lang === "en" || lang === "us") lang = null // <-- english is default, no need to append it to the file name
121
+
122
+ if (!fs.existsSync(`${this.path}/README${lang ? "." + lang : ""}.md`)) {
123
+ Console.error(
124
+ `Language ${lang} not found for exercise ${slug}, switching to default language`
125
+ )
126
+
127
+ if (lang) lang = null
128
+
129
+ if (!fs.existsSync(`${this.path}/README${lang ? "." + lang : ""}.md`))
130
+ throw new Error(
131
+ "Readme file not found for exercise: " + this.path + "/README.md"
132
+ )
133
+ }
134
+
135
+ const content = fs.readFileSync(
136
+ `${this.path}/README${lang ? "." + lang : ""}.md`,
137
+ "utf8"
138
+ )
139
+ const attr = frontMatter(content)
140
+ return attr
141
+ },
142
+ getFile: function (name: string) {
143
+ const file: IFile | undefined = this.files.find(
144
+ (f: IFile) => f.name === name
145
+ )
146
+
147
+ if (!file || !fs.existsSync(file.path)) {
148
+ throw new Error(`File not found: + ${file?.path}`)
149
+ } else if (fs.lstatSync(file.path).isDirectory()) {
150
+ return (
151
+ "Error: This is not a file to be read, but a directory: " + file.path
152
+ )
153
+ }
154
+
155
+ // get file content
156
+ const content = fs.readFileSync(file.path)
157
+
158
+ // create reset folder
159
+ if (!fs.existsSync(`${config?.dirPath}/resets`))
160
+ fs.mkdirSync(`${config?.dirPath}/resets`)
161
+ if (!fs.existsSync(`${config?.dirPath}/resets/` + this.slug)) {
162
+ fs.mkdirSync(`${config?.dirPath}/resets/` + this.slug)
163
+ }
164
+
165
+ for (const _file of this.files) {
166
+ const stats = fs.statSync(_file.path)
167
+ if (stats.isDirectory() || _file.hidden) continue
168
+ const fileContent = fs.readFileSync(_file.path)
169
+ if (
170
+ !fs.existsSync(`${config?.dirPath}/resets/${this.slug}/${_file.name}`)
171
+ ) {
172
+ fs.writeFileSync(
173
+ `${config?.dirPath}/resets/${this.slug}/${_file.name}`,
174
+ fileContent
175
+ )
176
+ }
177
+ }
178
+
179
+ return content
180
+ },
181
+
182
+ renameFolder: function (newName: string) {
183
+ if (!config?.dirPath) {
184
+ throw new Error("No config directory found")
185
+ }
186
+
187
+ try {
188
+ const newPath = p.join(config?.exercisesPath, newName)
189
+ fs.renameSync(this.path, newPath)
190
+ this.path = newPath
191
+ this.slug = newName
192
+ this.title = newName
193
+ } catch (error) {
194
+ console.log(error)
195
+ throw new Error("Failed to rename exercise: " + error)
196
+ }
197
+ },
198
+ saveFile: function (name: string, content: string) {
199
+ const file: IFile | undefined = this.files.find(
200
+ (f: IFile) => f.name === name
201
+ )
202
+
203
+ if (file) {
204
+ if (!fs.existsSync(file.path)) {
205
+ throw new Error("File not found: " + file.path)
206
+ }
207
+
208
+ return fs.writeFileSync(file.path, content, "utf8")
209
+ }
210
+ },
211
+ getTestReport: function () {
212
+ const _path = `${configObject?.confPath?.base}/reports/${this.slug}.json`
213
+
214
+ if (!fs.existsSync(_path)) return {}
215
+
216
+ const content = fs.readFileSync(_path)
217
+ const data = JSON.parse(`${content}`)
218
+ return data
219
+ },
220
+ }
221
+
222
+ return exerciseObj
223
+ }
224
+
225
+ export const validateExerciseDirectoryName = (str: string) => {
226
+ if (str === "./") return true
227
+ // TODO: Add nameValidationREgex from the config
228
+ const regex = /^(\d{2,3}(\.\d{1,2})?-([\dA-Za-z]+(-|_)?)+)$/
229
+ return regex.test(str)
230
+ }
231
+
232
+ export const isCodable = (str: string) => {
233
+ const extension = p.extname(str)
234
+ return allowed.extensions.includes(extension.slice(1).toLowerCase())
235
+ }
236
+
237
+ const isNotConfiguration = (str: string) => {
238
+ return !allowed.names.includes(str)
239
+ }
240
+
241
+ export const shouldBeVisible = function (file: IFile) {
242
+ return (
243
+ // doest not have "test." on their name
244
+ !file.name.toLocaleLowerCase().includes("test.") &&
245
+ !file.name.toLocaleLowerCase().includes("tests.") &&
246
+ !file.name.toLocaleLowerCase().includes(".hide.") &&
247
+ // ignore hidden files
248
+ file.name.charAt(0) !== "." &&
249
+ // ignore learn.json and bc.json
250
+ !file.name.toLocaleLowerCase().includes("learn.json") &&
251
+ !file.name.toLocaleLowerCase().includes("bc.json") &&
252
+ // ignore images, videos, vectors, etc.
253
+ isCodable(file.name) &&
254
+ isNotConfiguration(file.name) &&
255
+ // readme's and directories
256
+ !file.name.toLowerCase().includes("readme.") &&
257
+ !isDirectory(file.path) &&
258
+ file.name.charAt(0) !== "_"
259
+ )
260
+ }
261
+
262
+ export const isDirectory = (source: string) => {
263
+ // if(path.basename(source) === path.basename(config.dirPath)) return false
264
+ return fs.lstatSync(source).isDirectory()
265
+ }
266
+
267
+ export const detect = (
268
+ configObject: IConfigObj | undefined,
269
+ files: Array<string>
270
+ ) => {
271
+ if (!configObject) {
272
+ return
273
+ }
274
+
275
+ const { config } = configObject
276
+
277
+ if (!config)
278
+ throw new Error("No configuration found during the engine detection")
279
+
280
+ if (!config.entries)
281
+ throw new Error(
282
+ "No configuration found for entries, please add a 'entries' object with the default file name for your exercise entry file that is going to be used while compiling, for example: index.html for html, app.py for python3, etc."
283
+ )
284
+ // A language was found on the config object, but this language will only be used as last resort, learnpack will try to guess each exercise language independently based on file extension (js, jsx, html, etc.)
285
+
286
+ let hasFiles = files.filter(f => f.includes(".py"))
287
+ if (hasFiles.length > 0)
288
+ return {
289
+ language: "python3",
290
+ entry: hasFiles.find(f => config.entries.python3 === f),
291
+ }
292
+
293
+ hasFiles = files.filter(f => f.includes(".java"))
294
+ if (hasFiles.length > 0)
295
+ return {
296
+ language: "java",
297
+ entry: hasFiles.find(f => config.entries.java === f),
298
+ }
299
+
300
+ hasFiles = files.filter(f => f.includes(".jsx"))
301
+ if (hasFiles.length > 0)
302
+ return {
303
+ language: "react",
304
+ entry: hasFiles.find(f => config.entries.react === f),
305
+ }
306
+ const hasHTML = files.filter(f => f.includes("index.html"))
307
+ const hasIndexJS = files.find(f => f.includes("index.js"))
308
+ const hasJS = files.filter(f => f.includes(".js"))
309
+ // angular, vue, vanillajs needs to have at least 2 files (html,css,js),
310
+ // the test.js and the entry file in js
311
+ // if not its just another HTML
312
+
313
+ if (hasIndexJS && hasHTML.length > 0)
314
+ return {
315
+ language: "vanillajs",
316
+ entry: hasIndexJS,
317
+ }
318
+ if (hasHTML.length > 0)
319
+ return {
320
+ language: "html",
321
+ entry: hasHTML.find(f => config.entries.html === f),
322
+ }
323
+ if (hasJS.length > 0)
324
+ return {
325
+ language: "node",
326
+ entry: hasJS.find(f => config.entries.node === f),
327
+ }
328
+
329
+ return {
330
+ language: null,
331
+ entry: null,
332
+ }
333
+ }
334
+
335
+ export const filterFiles = (files: Array<string>, basePath = ".") =>
336
+ files
337
+ .map((ex: string) => ({
338
+ path: basePath + "/" + ex,
339
+ name: ex,
340
+ hidden: !shouldBeVisible({
341
+ name: ex,
342
+ path: basePath + "/" + ex,
343
+ } as IFile),
344
+ }))
345
+ .sort((f1, f2) => {
346
+ const score: { [key: string]: number } = {
347
+ // sorting priority
348
+ "index.html": 1,
349
+ "styles.css": 2,
350
+ "styles.scss": 2,
351
+ "style.css": 2,
352
+ "style.scss": 2,
353
+ "index.css": 2,
354
+ "index.scss": 2,
355
+ "index.js": 3,
356
+ }
357
+ return score[f1.name] < score[f2.name] ? -1 : 1
358
+ })
359
+
360
+ export default {
361
+ exercise,
362
+ detect,
363
+ filterFiles,
364
+ }