@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
@@ -0,0 +1,29 @@
1
+ const extensions = {
2
+ extensions: [
3
+ "py",
4
+ "java",
5
+ "py",
6
+ "ruby",
7
+ "html",
8
+ "css",
9
+ "htm",
10
+ "php", // images
11
+ "js",
12
+ "jsx",
13
+ "ts", // images
14
+ "sh",
15
+ "bash", // images
16
+ "json",
17
+ "yml",
18
+ "yaml",
19
+ "csv",
20
+ "xml", // file storage extensions
21
+ "txt",
22
+ "text",
23
+ "markdown",
24
+ "readme", // compressed files
25
+ ],
26
+ names: ["package.json", "package-lock.json"],
27
+ };
28
+
29
+ export default extensions;
@@ -0,0 +1,33 @@
1
+ export default {
2
+ config: {
3
+ port: 3000,
4
+ editor: {
5
+ mode: null, // [standalone, preview]
6
+ agent: null, // [vscode, theia]
7
+ version: null,
8
+ },
9
+ dirPath: './.learn',
10
+ configPath: './learn.json',
11
+ outputPath: './.learn/dist',
12
+ publicPath: '/preview',
13
+ publicUrl: null,
14
+ language: 'auto',
15
+ grading: 'isolated', // [isolated, incremental]
16
+ exercisesPath: './', // path to the folder that contains the exercises
17
+ webpackTemplate: null, // if you want webpack to use an HTML template
18
+ disableGrading: false,
19
+ disabledActions: [], // Possible: 'build', 'test' or 'reset'
20
+ actions: [], // ⚠️ deprecated, leave empty )
21
+ entries: {
22
+ html: 'index.html',
23
+ vanillajs: 'index.js',
24
+ react: 'app.jsx',
25
+ node: 'app.js',
26
+ python3: 'app.py',
27
+ java: 'app.java',
28
+ },
29
+ },
30
+ address: 'http://localhost',
31
+ currentExercise: null,
32
+ exercises: [],
33
+ }
@@ -0,0 +1,295 @@
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
+ // eslint-disable-next-line
12
+ const frontMatter = require("front-matter");
13
+
14
+ export const exercise = (
15
+ path: string,
16
+ position: number,
17
+ configObject: IConfigObj
18
+ ): IExercise => {
19
+ const { config, exercises } = configObject;
20
+ let slug = p.basename(path);
21
+
22
+ if (!validateExerciseDirectoryName(slug)) {
23
+ Console.error(
24
+ `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`
25
+ );
26
+ }
27
+
28
+ // get all the files
29
+ const files = fs.readdirSync(path);
30
+
31
+ /**
32
+ * build the translation array like:
33
+ {
34
+ "us": "path/to/Readme.md",
35
+ "es": "path/to/Readme.es.md"
36
+ }
37
+ */
38
+ const translations: { [key: string]: string } = {};
39
+ for (const file of files.filter(file =>
40
+ file.toLowerCase().includes("readme")
41
+ )) {
42
+ const parts = file.split(".");
43
+
44
+ if (parts.length === 3)
45
+ translations[parts[1]] = file;
46
+ else
47
+ translations.us = file;
48
+ }
49
+
50
+ // if the slug is a dot, it means there is not "exercises" folder, and its just a single README.md
51
+ if (slug === ".")
52
+ slug = "default-index";
53
+
54
+ const detected = detect(configObject, files);
55
+
56
+ const exerciseObj: IExercise = {
57
+ position,
58
+ path,
59
+ slug,
60
+ translations,
61
+ language: detected?.language,
62
+ entry: detected?.entry ? path + "/" + detected.entry : null, // full path to the exercise entry
63
+ title: slug || "Exercise",
64
+ graded: files.some(file => file.toLowerCase().startsWith("test.") || file.toLowerCase().startsWith("tests.")),
65
+ files: filterFiles(files, path),
66
+ // if the exercises was on the config before I may keep the status done
67
+ done:
68
+ Array.isArray(exercises) &&
69
+ typeof exercises[position] !== "undefined" &&
70
+ path.slice(Math.max(0, path.indexOf("exercises/") + 10)) === exercises[position].slug ? exercises[position].done : false,
71
+ getReadme: function (lang = null) {
72
+ if (lang === "us")
73
+ lang = null; // <-- english is default, no need to append it to the file name
74
+
75
+ if (!fs.existsSync(`${this.path}/README${lang ? "." + lang : ""}.md`)) {
76
+ Console.error(
77
+ `Language ${lang} not found for exercise ${slug}, switching to default language`
78
+ );
79
+
80
+ if (lang)
81
+ lang = null;
82
+
83
+ if (!fs.existsSync(`${this.path}/README${lang ? "." + lang : ""}.md`))
84
+ throw new Error(
85
+ "Readme file not found for exercise: " + this.path + "/README.md"
86
+ );
87
+ }
88
+
89
+ const content = fs.readFileSync(
90
+ `${this.path}/README${lang ? "." + lang : ""}.md`,
91
+ "utf8"
92
+ );
93
+ const attr = frontMatter(content);
94
+ return attr;
95
+ },
96
+ getFile: function (name: string) {
97
+ const file: IFile | undefined = this.files.find(
98
+ (f: IFile) => f.name === name
99
+ );
100
+
101
+ if (!file || !fs.existsSync(file.path)) {
102
+ throw new Error(`File not found: + ${file?.path}`);
103
+ } else if (fs.lstatSync(file.path).isDirectory()) {
104
+ return (
105
+ "Error: This is not a file to be read, but a directory: " + file.path
106
+ );
107
+ }
108
+
109
+ // get file content
110
+ const content = fs.readFileSync(file.path);
111
+
112
+ // create reset folder
113
+ if (!fs.existsSync(`${config?.dirPath}/resets`))
114
+ fs.mkdirSync(`${config?.dirPath}/resets`);
115
+ if (!fs.existsSync(`${config?.dirPath}/resets/` + this.slug)) {
116
+ fs.mkdirSync(`${config?.dirPath}/resets/` + this.slug);
117
+ if (!fs.existsSync(`${config?.dirPath}/resets/${this.slug}/${name}`)) {
118
+ fs.writeFileSync(
119
+ `${config?.dirPath}/resets/${this.slug}/${name}`,
120
+ content
121
+ );
122
+ }
123
+ }
124
+
125
+ return content;
126
+ },
127
+ saveFile: function (name: string, content: string) {
128
+ const file: IFile | undefined = this.files.find(
129
+ (f: IFile) => f.name === name
130
+ );
131
+
132
+ if (file) {
133
+ if (!fs.existsSync(file.path)) {
134
+ throw new Error("File not found: " + file.path);
135
+ }
136
+
137
+ return fs.writeFileSync(file.path, content, "utf8");
138
+ }
139
+ },
140
+ getTestReport: function () {
141
+ const _path = `${configObject?.confPath?.base}/reports/${this.slug}.json`;
142
+
143
+ if (!fs.existsSync(_path))
144
+ return {};
145
+
146
+ const content = fs.readFileSync(_path);
147
+ const data = JSON.parse(`${content}`);
148
+ return data;
149
+ },
150
+ };
151
+
152
+ return exerciseObj;
153
+ };
154
+
155
+ export const validateExerciseDirectoryName = (str: string) => {
156
+ if (str === "./")
157
+ return true;
158
+ // TODO: Add nameValidationREgex from the config
159
+ const regex = /^(\d{2,3}(\.\d{1,2})?-([\dA-Za-z]+(-|_)?)+)$/;
160
+ return regex.test(str);
161
+ };
162
+
163
+ export const isCodable = (str: string) => {
164
+ const extension = p.extname(str);
165
+ return allowed.extensions.includes(extension.slice(1).toLowerCase());
166
+ };
167
+
168
+ const isNotConfiguration = (str: string) => {
169
+ return !allowed.names.includes(str);
170
+ };
171
+
172
+ export const shouldBeVisible = function (file: IFile) {
173
+ return (
174
+ // doest not have "test." on their name
175
+ !file.name.toLocaleLowerCase().includes("test.") &&
176
+ !file.name.toLocaleLowerCase().includes("tests.") &&
177
+ !file.name.toLocaleLowerCase().includes(".hide.") &&
178
+ // ignore hidden files
179
+ file.name.charAt(0) !== "." &&
180
+ // ignore learn.json and bc.json
181
+ !file.name.toLocaleLowerCase().includes("learn.json") &&
182
+ !file.name.toLocaleLowerCase().includes("bc.json") &&
183
+ // ignore images, videos, vectors, etc.
184
+ isCodable(file.name) &&
185
+ isNotConfiguration(file.name) &&
186
+ // readme's and directories
187
+ !file.name.toLowerCase().includes("readme.") &&
188
+ !isDirectory(file.path) &&
189
+ file.name.charAt(0) !== "_"
190
+ );
191
+ };
192
+
193
+ export const isDirectory = (source: string) => {
194
+ // if(path.basename(source) === path.basename(config.dirPath)) return false
195
+ return fs.lstatSync(source).isDirectory();
196
+ };
197
+
198
+ export const detect = (
199
+ configObject: IConfigObj | undefined,
200
+ files: Array<string>
201
+ ) => {
202
+ if (!configObject) {
203
+ return;
204
+ }
205
+
206
+ const { config } = configObject;
207
+
208
+ if (!config)
209
+ throw new Error("No configuration found during the engine detection");
210
+
211
+ if (!config.entries)
212
+ throw new Error(
213
+ "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."
214
+ );
215
+ // 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.)
216
+
217
+ let hasFiles = files.filter(f => f.includes(".py"));
218
+ if (hasFiles.length > 0)
219
+ return {
220
+ language: "python3",
221
+ entry: hasFiles.find(f => config.entries.python3 === f),
222
+ };
223
+
224
+ hasFiles = files.filter(f => f.includes(".java"));
225
+ if (hasFiles.length > 0)
226
+ return {
227
+ language: "java",
228
+ entry: hasFiles.find(f => config.entries.java === f),
229
+ };
230
+
231
+ hasFiles = files.filter(f => f.includes(".jsx"));
232
+ if (hasFiles.length > 0)
233
+ return {
234
+ language: "react",
235
+ entry: hasFiles.find(f => config.entries.react === f),
236
+ };
237
+ const hasHTML = files.filter(f => f.includes("index.html"));
238
+ const hasIndexJS = files.find(f => f.includes("index.js"));
239
+ const hasJS = files.filter(f => f.includes(".js"));
240
+ // angular, vue, vanillajs needs to have at least 2 files (html,css,js),
241
+ // the test.js and the entry file in js
242
+ // if not its just another HTML
243
+
244
+ if (hasIndexJS && hasHTML.length > 0)
245
+ return {
246
+ language: "vanillajs",
247
+ entry: hasIndexJS,
248
+ };
249
+ if (hasHTML.length > 0)
250
+ return {
251
+ language: "html",
252
+ entry: hasHTML.find(f => config.entries.html === f),
253
+ };
254
+ if (hasJS.length > 0)
255
+ return {
256
+ language: "node",
257
+ entry: hasJS.find(f => config.entries.node === f),
258
+ };
259
+
260
+ return {
261
+ language: null,
262
+ entry: null,
263
+ };
264
+ };
265
+
266
+ export const filterFiles = (files: Array<string>, basePath = ".") =>
267
+ files
268
+ .map((ex: string) => ({
269
+ path: basePath + "/" + ex,
270
+ name: ex,
271
+ hidden: !shouldBeVisible({
272
+ name: ex,
273
+ path: basePath + "/" + ex,
274
+ } as IFile),
275
+ }))
276
+ .sort((f1, f2) => {
277
+ const score: { [key: string]: number } = {
278
+ // sorting priority
279
+ "index.html": 1,
280
+ "styles.css": 2,
281
+ "styles.scss": 2,
282
+ "style.css": 2,
283
+ "style.scss": 2,
284
+ "index.css": 2,
285
+ "index.scss": 2,
286
+ "index.js": 3,
287
+ };
288
+ return score[f1.name] < score[f2.name] ? -1 : 1;
289
+ });
290
+
291
+ export default {
292
+ exercise,
293
+ detect,
294
+ filterFiles,
295
+ };