@learnpack/learnpack 1.0.0
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.
- package/README.md +695 -0
- package/bin/run +5 -0
- package/bin/run.cmd +3 -0
- package/oclif.manifest.json +1 -0
- package/package.json +111 -0
- package/plugin/command/compile.js +17 -0
- package/plugin/command/test.js +29 -0
- package/plugin/index.js +6 -0
- package/plugin/plugin.js +71 -0
- package/plugin/utils.js +78 -0
- package/src/commands/audit.js +243 -0
- package/src/commands/clean.js +27 -0
- package/src/commands/download.js +52 -0
- package/src/commands/hello.js +20 -0
- package/src/commands/init.js +133 -0
- package/src/commands/login.js +45 -0
- package/src/commands/logout.js +39 -0
- package/src/commands/publish.js +78 -0
- package/src/commands/start.js +169 -0
- package/src/commands/test.js +85 -0
- package/src/index.js +1 -0
- package/src/managers/config/allowed_files.js +12 -0
- package/src/managers/config/defaults.js +32 -0
- package/src/managers/config/exercise.js +212 -0
- package/src/managers/config/index.js +342 -0
- package/src/managers/file.js +137 -0
- package/src/managers/server/index.js +62 -0
- package/src/managers/server/routes.js +151 -0
- package/src/managers/session.js +83 -0
- package/src/managers/socket.js +185 -0
- package/src/managers/test.js +77 -0
- package/src/ui/download.js +48 -0
- package/src/utils/BaseCommand.js +34 -0
- package/src/utils/SessionCommand.js +46 -0
- package/src/utils/api.js +164 -0
- package/src/utils/audit.js +114 -0
- package/src/utils/console.js +16 -0
- package/src/utils/errors.js +90 -0
- package/src/utils/exercisesQueue.js +45 -0
- package/src/utils/fileQueue.js +194 -0
- package/src/utils/misc.js +26 -0
- package/src/utils/templates/gitignore.txt +20 -0
- package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.es.md +26 -0
- package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.md +25 -0
- package/src/utils/templates/incremental/README.ejs +5 -0
- package/src/utils/templates/incremental/README.es.ejs +5 -0
- package/src/utils/templates/isolated/01-hello-world/README.es.md +27 -0
- package/src/utils/templates/isolated/01-hello-world/README.md +27 -0
- package/src/utils/templates/isolated/README.ejs +5 -0
- package/src/utils/templates/isolated/README.es.ejs +5 -0
- package/src/utils/templates/no-grading/README.ejs +5 -0
- package/src/utils/templates/no-grading/README.es.ejs +5 -0
- package/src/utils/validators.js +15 -0
- package/src/utils/watcher.js +24 -0
package/src/utils/api.js
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
const Console = require('../utils/console');
|
2
|
+
const _fetch = require('node-fetch');
|
3
|
+
const storage = require('node-persist');
|
4
|
+
const cli = require("cli-ux").default
|
5
|
+
const HOST = "https://learnpack.herokuapp.com";
|
6
|
+
|
7
|
+
const fetch = async (url, options={}) => {
|
8
|
+
|
9
|
+
let headers = { "Content-Type": "application/json" }
|
10
|
+
let session = null;
|
11
|
+
try{
|
12
|
+
session = await storage.getItem('bc-payload');
|
13
|
+
if(session.token && session.token != "" && !url.includes("/token")) headers["Authorization"] = "Token "+session.token;
|
14
|
+
}
|
15
|
+
catch(err){}
|
16
|
+
|
17
|
+
try{
|
18
|
+
const resp = await _fetch(url, {
|
19
|
+
...options,
|
20
|
+
headers: { ...headers, ...options.headers }
|
21
|
+
})
|
22
|
+
|
23
|
+
if(resp.status >= 200 && resp.status < 300) return await resp.json()
|
24
|
+
else if(resp.status === 401) throw APIError("Invalid authentication credentials", 401)
|
25
|
+
else if(resp.status === 404) throw APIError("Package not found", 404)
|
26
|
+
else if(resp.status >= 500) throw APIError("Impossible to connect with the server", 500)
|
27
|
+
else if(resp.status >= 400){
|
28
|
+
const error = await resp.json()
|
29
|
+
if(error.detail || error.error){
|
30
|
+
throw APIError(error.detail || error.error)
|
31
|
+
}else if(error.non_field_errors){
|
32
|
+
throw APIError(non_field_errors[0], error)
|
33
|
+
}else if (typeof error === "object"){
|
34
|
+
for(let key in error){
|
35
|
+
throw APIError(`${key}: ${error[key][0]}`, error)
|
36
|
+
}
|
37
|
+
}else{
|
38
|
+
throw APIError("Uknown error")
|
39
|
+
}
|
40
|
+
}
|
41
|
+
else throw APIError("Uknown error")
|
42
|
+
}
|
43
|
+
catch(error){
|
44
|
+
Console.error(error.message);
|
45
|
+
throw error;
|
46
|
+
}
|
47
|
+
}
|
48
|
+
const login = async (identification, password) => {
|
49
|
+
|
50
|
+
try{
|
51
|
+
cli.action.start('Looking for credentials...')
|
52
|
+
await cli.wait(1000)
|
53
|
+
const data = await fetch(`${HOST}/v1/auth/token/`, {
|
54
|
+
body: JSON.stringify({ identification, password }),
|
55
|
+
method: 'post'
|
56
|
+
});
|
57
|
+
cli.action.stop('ready')
|
58
|
+
return data
|
59
|
+
}
|
60
|
+
catch(err){
|
61
|
+
Console.error(err.message);
|
62
|
+
Console.debug(err);
|
63
|
+
}
|
64
|
+
}
|
65
|
+
const publish = async (config) => {
|
66
|
+
|
67
|
+
const keys = ['difficulty', 'language', 'skills', 'technologies', 'slug', 'repository', 'author', 'title'];
|
68
|
+
|
69
|
+
let payload = {}
|
70
|
+
keys.forEach(k => config[k] ? payload[k] = config[k] : null);
|
71
|
+
try{
|
72
|
+
Console.log("Package to publish: ", payload)
|
73
|
+
cli.action.start('Updating package information...')
|
74
|
+
await cli.wait(1000)
|
75
|
+
const data = await fetch(`${HOST}/v1/package/${config.slug}`,{
|
76
|
+
method: 'PUT',
|
77
|
+
body: JSON.stringify(payload)
|
78
|
+
})
|
79
|
+
cli.action.stop('ready')
|
80
|
+
return data
|
81
|
+
}
|
82
|
+
catch(err){
|
83
|
+
Console.log("payload", payload)
|
84
|
+
Console.error(err.message);
|
85
|
+
Console.debug(err);
|
86
|
+
throw err;
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
const update = async (config) => {
|
91
|
+
|
92
|
+
try{
|
93
|
+
cli.action.start('Updating package information...')
|
94
|
+
await cli.wait(1000)
|
95
|
+
const data = await fetch(`${HOST}/v1/package/`,{
|
96
|
+
method: 'POST',
|
97
|
+
body: JSON.stringify(config)
|
98
|
+
})
|
99
|
+
cli.action.stop('ready')
|
100
|
+
return data
|
101
|
+
}
|
102
|
+
catch(err){
|
103
|
+
Console.error(err.message);
|
104
|
+
Console.debug(err);
|
105
|
+
throw err;
|
106
|
+
}
|
107
|
+
}
|
108
|
+
|
109
|
+
const getPackage = async (slug) => {
|
110
|
+
try{
|
111
|
+
cli.action.start('Downloading package information...')
|
112
|
+
await cli.wait(1000)
|
113
|
+
const data = await fetch(`${HOST}/v1/package/${slug}`)
|
114
|
+
cli.action.stop('ready')
|
115
|
+
return data
|
116
|
+
}
|
117
|
+
catch(err){
|
118
|
+
if(err.status == 404) Console.error(`Package ${slug} does not exist`);
|
119
|
+
else Console.error(`Package ${slug} does not exist`);
|
120
|
+
Console.debug(err);
|
121
|
+
throw err;
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
const getLangs = async () => {
|
126
|
+
try{
|
127
|
+
cli.action.start('Downloading language options...')
|
128
|
+
await cli.wait(1000)
|
129
|
+
const data = await fetch(`${HOST}/v1/package/language`)
|
130
|
+
cli.action.stop('ready')
|
131
|
+
return data;
|
132
|
+
}
|
133
|
+
catch(err){
|
134
|
+
if(err.status == 404) Console.error(`Package ${slug} does not exist`);
|
135
|
+
else Console.error(`Package ${slug} does not exist`);
|
136
|
+
Console.debug(err);
|
137
|
+
throw err;
|
138
|
+
}
|
139
|
+
}
|
140
|
+
|
141
|
+
|
142
|
+
const getAllPackages = async ({ lang='', slug='' }) => {
|
143
|
+
try{
|
144
|
+
cli.action.start('Downloading packages...')
|
145
|
+
await cli.wait(1000)
|
146
|
+
const data = await fetch(`${HOST}/v1/package/all?limit=100&language=${lang}&slug=${slug}`)
|
147
|
+
cli.action.stop('ready')
|
148
|
+
return data;
|
149
|
+
}
|
150
|
+
catch(err){
|
151
|
+
Console.error(`Package ${slug} does not exist`);
|
152
|
+
Console.debug(err);
|
153
|
+
throw err;
|
154
|
+
}
|
155
|
+
}
|
156
|
+
|
157
|
+
const APIError = (error, code) => {
|
158
|
+
const message = error.message || error;
|
159
|
+
const _err = new Error(message);
|
160
|
+
_err.status = code || 400;
|
161
|
+
return _err;
|
162
|
+
}
|
163
|
+
|
164
|
+
module.exports = {login, publish, update, getPackage, getLangs, getAllPackages }
|
@@ -0,0 +1,114 @@
|
|
1
|
+
const Console = require('./console')
|
2
|
+
const fetch = require('node-fetch')
|
3
|
+
const fs = require('fs')
|
4
|
+
|
5
|
+
module.exports = {
|
6
|
+
// This function checks if a url is valid.
|
7
|
+
isUrl: async (url, errors, counter) => {
|
8
|
+
let regex_url = /(https?:\/\/[a-zA-Z_\-.\/0-9]+)/gm
|
9
|
+
counter.links.total++
|
10
|
+
if (!regex_url.exec(url)) {
|
11
|
+
counter.links.error++
|
12
|
+
errors.push({ exercise: null, msg: `The repository value of the configuration file is not a link: ${url}` })
|
13
|
+
return false;
|
14
|
+
}
|
15
|
+
let res = await fetch(url, { method: "HEAD" });
|
16
|
+
if (!res.ok) {
|
17
|
+
counter.links.error++
|
18
|
+
errors.push({ exercise: null, msg: `The link of the repository is broken: ${url}` })
|
19
|
+
}
|
20
|
+
return true;
|
21
|
+
},
|
22
|
+
checkForEmptySpaces: (str) => {
|
23
|
+
let isEmpty = true;
|
24
|
+
for(let letter of str){
|
25
|
+
if (letter !== " ") {
|
26
|
+
isEmpty = false;
|
27
|
+
return isEmpty;
|
28
|
+
}
|
29
|
+
}
|
30
|
+
return isEmpty;
|
31
|
+
},
|
32
|
+
checkLearnpackClean: (configObj, errors) => {
|
33
|
+
if(fs.existsSync(configObj.config.outputPath) || fs.existsSync(configObj.config.dirPath + "/_app") || fs.existsSync(configObj.config.dirPath + "/reports") || fs.existsSync(configObj.config.dirPath + "/resets") || fs.existsSync(configObj.config.dirPath + "/app.tar.gz") || fs.existsSync(configObj.config.dirPath + "/config.json") || fs.existsSync(configObj.config.dirPath + "/vscode_queue.json")) {
|
34
|
+
errors.push({ exercise: null, msg: `You have to run learnpack clean command` })
|
35
|
+
}
|
36
|
+
},
|
37
|
+
findInFile: (types, content) => {
|
38
|
+
const regex = {
|
39
|
+
relative_images: /!\[.*\]\s*\((((\.\/)?(\.{2}\/){1,5})(.*\/)*(.[^\/\s]*\.[a-zA-Z]{2,4})[^\s]*)\)/gm,
|
40
|
+
external_images: /!\[.*\]\((https?:\/(\/{1}[^/)]+)+\/?)\)/gm,
|
41
|
+
markdown_links: /(\s)+\[.*\]\((https?:\/(\/{1}[^/)]+)+\/?)\)/gm,
|
42
|
+
url: /(https?:\/\/[a-zA-Z_\-.\/0-9]+)/gm,
|
43
|
+
uploadcare: /https:\/\/ucarecdn.com\/(?:.*\/)*([a-zA-Z_\-.\/0-9]+)/gm
|
44
|
+
}
|
45
|
+
|
46
|
+
const validTypes = Object.keys(regex);
|
47
|
+
if (!Array.isArray(types)) types = [types];
|
48
|
+
|
49
|
+
let findings = {}
|
50
|
+
|
51
|
+
types.forEach(type => {
|
52
|
+
if (!validTypes.includes(type)) throw Error("Invalid type: " + type)
|
53
|
+
else findings[type] = {};
|
54
|
+
});
|
55
|
+
|
56
|
+
types.forEach(type => {
|
57
|
+
|
58
|
+
let count = 0;
|
59
|
+
let m;
|
60
|
+
while ((m = regex[type].exec(content)) !== null) {
|
61
|
+
// This is necessary to avoid infinite loops with zero-width matches
|
62
|
+
if (m.index === regex.lastIndex) {
|
63
|
+
regex.lastIndex++;
|
64
|
+
}
|
65
|
+
|
66
|
+
// The result can be accessed through the `m`-variable.
|
67
|
+
// m.forEach((match, groupIndex) => values.push(match));
|
68
|
+
count++;
|
69
|
+
|
70
|
+
findings[type][m[0]] = {
|
71
|
+
content: m[0],
|
72
|
+
absUrl: m[1],
|
73
|
+
mdUrl: m[2],
|
74
|
+
relUrl: m[6]
|
75
|
+
}
|
76
|
+
}
|
77
|
+
})
|
78
|
+
|
79
|
+
return findings;
|
80
|
+
},
|
81
|
+
// This function checks if there are errors, and show them in the console at the end.
|
82
|
+
showErrors: (errors, counter) => {
|
83
|
+
return new Promise((resolve, reject) => {
|
84
|
+
if (errors) {
|
85
|
+
if (errors.length > 0) {
|
86
|
+
Console.log("Checking for errors...")
|
87
|
+
errors.forEach((error, i) => Console.error(`${i + 1}) ${error.msg} ${error.exercise != null ? `(Exercise: ${error.exercise})` : ""}`))
|
88
|
+
Console.error(` We found ${errors.length} errors among ${counter.images.total} images, ${counter.links.total} link, ${counter.readmeFiles} README files and ${counter.exercises} exercises.`)
|
89
|
+
process.exit(1)
|
90
|
+
} else {
|
91
|
+
Console.success(`We didn't find any errors in this repository among ${counter.images.total} images, ${counter.links.total} link, ${counter.readmeFiles} README files and ${counter.exercises} exercises.`)
|
92
|
+
process.exit(0)
|
93
|
+
}
|
94
|
+
resolve("SUCCESS")
|
95
|
+
} else {
|
96
|
+
reject("Failed")
|
97
|
+
}
|
98
|
+
})
|
99
|
+
},
|
100
|
+
// This function checks if there are warnings, and show them in the console at the end.
|
101
|
+
showWarnings: (warnings) => {
|
102
|
+
return new Promise((resolve, reject) => {
|
103
|
+
if (warnings) {
|
104
|
+
if (warnings.length > 0) {
|
105
|
+
Console.log("Checking for warnings...")
|
106
|
+
warnings.forEach((warning, i) => Console.warning(`${i + 1}) ${warning.msg} ${warning.exercise ? `File: ${warning.exercise}` : ""}`))
|
107
|
+
}
|
108
|
+
resolve("SUCCESS")
|
109
|
+
} else {
|
110
|
+
reject("Failed")
|
111
|
+
}
|
112
|
+
})
|
113
|
+
}
|
114
|
+
}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
const chalk = require("chalk")
|
2
|
+
|
3
|
+
module.exports = {
|
4
|
+
// _debug: true,
|
5
|
+
_debug: process.env.DEBUG == 'true',
|
6
|
+
startDebug: function(){ this._debug = true; },
|
7
|
+
log: (msg, ...args) => console.log(chalk.gray(msg), ...args),
|
8
|
+
error: (msg, ...args) => console.log(chalk.red('⨉ '+msg), ...args),
|
9
|
+
success: (msg, ...args) => console.log(chalk.green('✓ '+msg), ...args),
|
10
|
+
info: (msg, ...args) => console.log(chalk.blue('ⓘ '+msg), ...args),
|
11
|
+
help: (msg) => console.log(`${chalk.white.bold('⚠ help:')} ${chalk.white(msg)}`),
|
12
|
+
warning: (msg) => console.log(`${chalk.yellow('⚠ warning:')} ${chalk.yellow(msg)}`),
|
13
|
+
debug(...args){
|
14
|
+
this._debug && console.log(chalk.magentaBright(`⚠ debug: `), args)
|
15
|
+
}
|
16
|
+
}
|
@@ -0,0 +1,90 @@
|
|
1
|
+
const fetch = require("node-fetch");
|
2
|
+
const Console = require("./console");
|
3
|
+
let solutions = null;
|
4
|
+
const uknown = {
|
5
|
+
video: "https://www.youtube.com/watch?v=gD1Sa99GiE4",
|
6
|
+
message: "Uknown internal error",
|
7
|
+
slug: "uknown",
|
8
|
+
gif: "https://github.com/breatheco-de/breathecode-cli/blob/master/docs/errors/uknown.gif?raw=true"
|
9
|
+
};
|
10
|
+
|
11
|
+
const getSolution = (slug=null) => {
|
12
|
+
|
13
|
+
if(!slug) Console.debug(`Getting solution templates from the learnpack repository`);
|
14
|
+
else Console.debug(`Getting solution for ${slug}`, solutions);
|
15
|
+
|
16
|
+
|
17
|
+
if(!solutions){
|
18
|
+
Console.debug("Fetching for errors.json on github");
|
19
|
+
fetch('https://raw.githubusercontent.com/breatheco-de/breathecode-cli/master/docs/errors/errors.json')
|
20
|
+
.then(r => r.json()).then(_s => solutions = _s);
|
21
|
+
return uknown;
|
22
|
+
}
|
23
|
+
if(typeof solutions[slug] === "undefined" || !slug) return uknown;
|
24
|
+
else return solutions[slug];
|
25
|
+
}
|
26
|
+
const ValidationError = (error) => {
|
27
|
+
const message = error.message || error;
|
28
|
+
const _err = new Error(message);
|
29
|
+
_err.status = 400;
|
30
|
+
_err.type = 'validation-error';
|
31
|
+
|
32
|
+
const sol = getSolution(error.slug);
|
33
|
+
_err.video = sol.video;
|
34
|
+
_err.gif = sol.gif;
|
35
|
+
_err.message = typeof message === "string" ? message : sol.message;
|
36
|
+
return _err;
|
37
|
+
}
|
38
|
+
const NotFoundError = (error) => {
|
39
|
+
const message = error.message || error;
|
40
|
+
const _err = new Error(message);
|
41
|
+
_err.status = 400;
|
42
|
+
_err.type = 'not-found-error';
|
43
|
+
|
44
|
+
const sol = getSolution(error.slug);
|
45
|
+
_err.video = sol.video;
|
46
|
+
_err.gif = sol.gif;
|
47
|
+
_err.message = typeof message === "string" ? message : sol.message;
|
48
|
+
return _err;
|
49
|
+
}
|
50
|
+
const CompilerError = (error) => {
|
51
|
+
const message = error.message || error;
|
52
|
+
const _err = new Error(message);
|
53
|
+
_err.status = 400;
|
54
|
+
_err.type = 'compiler-error';
|
55
|
+
|
56
|
+
const sol = getSolution(error.slug);
|
57
|
+
_err.video = sol.video;
|
58
|
+
_err.gif = sol.gif;
|
59
|
+
_err.message = typeof message === "string" ? message : sol.message;
|
60
|
+
return _err;
|
61
|
+
}
|
62
|
+
const TestingError = (error) => {
|
63
|
+
const message = error.message || error;
|
64
|
+
const _err = new Error(message);
|
65
|
+
_err.status = 400;
|
66
|
+
_err.type = 'testing-error';
|
67
|
+
return _err;
|
68
|
+
}
|
69
|
+
const AuthError = (error) => {
|
70
|
+
const message = error.message || error;
|
71
|
+
const _err = new Error(message);
|
72
|
+
_err.status = 403;
|
73
|
+
_err.type = 'auth-error';
|
74
|
+
return _err;
|
75
|
+
}
|
76
|
+
const InternalError = (error) => {
|
77
|
+
const message = error.message || error;
|
78
|
+
const _err = new Error(message);
|
79
|
+
_err.status = 500;
|
80
|
+
_err.type = 'internal-error';
|
81
|
+
|
82
|
+
const sol = getSolution(error.slug);
|
83
|
+
_err.video = sol.video;
|
84
|
+
_err.gif = sol.gif;
|
85
|
+
_err.message = typeof message === "string" ? message : sol.message;
|
86
|
+
return _err;
|
87
|
+
}
|
88
|
+
|
89
|
+
getSolution();
|
90
|
+
module.exports = { ValidationError, CompilerError, TestingError, NotFoundError, InternalError, AuthError };
|
@@ -0,0 +1,45 @@
|
|
1
|
+
class Exercise {
|
2
|
+
constructor(exercise) {
|
3
|
+
this.exercise = exercise;
|
4
|
+
}
|
5
|
+
|
6
|
+
test(sessionConfig, config, socket) {
|
7
|
+
if (this.exercise.language) {
|
8
|
+
socket.log(
|
9
|
+
"testing",
|
10
|
+
`Testing exercise ${this.exercise.slug} using ${this.exercise.language} engine`
|
11
|
+
);
|
12
|
+
|
13
|
+
sessionConfig.runHook("action", {
|
14
|
+
action: "test",
|
15
|
+
socket,
|
16
|
+
configuration: config,
|
17
|
+
exercise: this.exercise,
|
18
|
+
});
|
19
|
+
} else {
|
20
|
+
socket.onTestingFinised({ result: "success" });
|
21
|
+
}
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
class ExercisesQueue {
|
26
|
+
constructor(exercises) {
|
27
|
+
this.exercises = exercises.map((exercise) => {
|
28
|
+
return new Exercise(exercise);
|
29
|
+
});
|
30
|
+
}
|
31
|
+
|
32
|
+
pop() {
|
33
|
+
return this.exercises.shift();
|
34
|
+
}
|
35
|
+
|
36
|
+
isEmpty() {
|
37
|
+
return this.size() === 0;
|
38
|
+
}
|
39
|
+
|
40
|
+
size() {
|
41
|
+
return this.exercises.length;
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
module.exports = ExercisesQueue;
|
@@ -0,0 +1,194 @@
|
|
1
|
+
const logger = require('../utils/console')
|
2
|
+
const fs = require("fs")
|
3
|
+
const em = require('events')
|
4
|
+
const XXH = require('xxhashjs')
|
5
|
+
|
6
|
+
// possible events to dispatch
|
7
|
+
let events = {
|
8
|
+
START_EXERCISE: "start_exercise",
|
9
|
+
INIT: "initializing",
|
10
|
+
RUNNING: "configuration_loaded",
|
11
|
+
END: "connection_ended",
|
12
|
+
RESET_EXERCISE: "reset_exercise",
|
13
|
+
OPEN_FILES: "open_files",
|
14
|
+
OPEN_WINDOW: "open_window",
|
15
|
+
INSTRUCTIONS_CLOSED: "instructions_closed"
|
16
|
+
}
|
17
|
+
|
18
|
+
let options = {
|
19
|
+
path: null,
|
20
|
+
create: false
|
21
|
+
}
|
22
|
+
let lastHash = null
|
23
|
+
let watcher = null // subscribe to file and listen to changes
|
24
|
+
let actions = null // action queue
|
25
|
+
|
26
|
+
const loadDispatcher = (opts) => {
|
27
|
+
|
28
|
+
actions = [{ name: "initializing", time: now() }]
|
29
|
+
logger.debug(`Loading from ${opts.path}`)
|
30
|
+
|
31
|
+
let exists = fs.existsSync(opts.path);
|
32
|
+
if(opts.create){
|
33
|
+
if(exists) actions.push({ name: "reset", time: now() })
|
34
|
+
fs.writeFileSync(opts.path, JSON.stringify(actions), { flag: "w"})
|
35
|
+
exists = true
|
36
|
+
}
|
37
|
+
|
38
|
+
if(!exists) throw Error(`Invalid queue path, missing file at: ${opts.path}`)
|
39
|
+
|
40
|
+
let incomingActions = []
|
41
|
+
try{
|
42
|
+
const content = fs.readFileSync(opts.path, 'utf-8')
|
43
|
+
incomingActions = JSON.parse(content)
|
44
|
+
if(!Array.isArray(incomingActions)) incomingActions = []
|
45
|
+
}
|
46
|
+
catch(error){
|
47
|
+
incomingActions = []
|
48
|
+
logger.debug(`Error loading VSCode Actions file`)
|
49
|
+
}
|
50
|
+
|
51
|
+
logger.debug(`Actions load `, incomingActions)
|
52
|
+
return incomingActions
|
53
|
+
}
|
54
|
+
|
55
|
+
|
56
|
+
const enqueue = (name, data) => {
|
57
|
+
|
58
|
+
|
59
|
+
if(!Object.values(events).includes(name)){
|
60
|
+
logger.debug(`Invalid event ${name}`)
|
61
|
+
throw Error(`Invalid action ${name}`)
|
62
|
+
}
|
63
|
+
|
64
|
+
if(!actions) actions = []
|
65
|
+
|
66
|
+
actions.push({ name, time: now(), data: data })
|
67
|
+
logger.debug(`EMIT -> ${name}:Exporting changes to ${options.path}`)
|
68
|
+
|
69
|
+
return fs.writeFileSync(options.path, JSON.stringify(actions))
|
70
|
+
}
|
71
|
+
const now = () => {
|
72
|
+
const hrTime = process.hrtime()
|
73
|
+
return hrTime[0] * 1000000 + hrTime[1] / 1000
|
74
|
+
}
|
75
|
+
const loadFile = (filePath) => {
|
76
|
+
|
77
|
+
if(!fs.existsSync(filePath)) throw Error(`No queue.json file to load on ${filePath}`);
|
78
|
+
|
79
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
80
|
+
const newHash = XXH.h32( content, 0xABCD ).toString(16);
|
81
|
+
const isUpdated = lastHash != newHash
|
82
|
+
lastHash = newHash
|
83
|
+
const incomingActions = JSON.parse(content)
|
84
|
+
return { isUpdated, incomingActions }
|
85
|
+
}
|
86
|
+
|
87
|
+
const dequeue = () => {
|
88
|
+
|
89
|
+
// first time dequeue loads
|
90
|
+
if(!actions) actions = []
|
91
|
+
|
92
|
+
const { isUpdated, incomingActions } = loadFile(options.path, 'utf8')
|
93
|
+
|
94
|
+
if(!isUpdated){
|
95
|
+
|
96
|
+
/**
|
97
|
+
* make sure no tasks are executed from the queue by matching both
|
98
|
+
* queues (the incoming with current one)
|
99
|
+
*/
|
100
|
+
actions = incomingActions
|
101
|
+
logger.debug(`No new actions to process: ${actions.length}/${incomingActions.length}`)
|
102
|
+
return null
|
103
|
+
}
|
104
|
+
|
105
|
+
// do i need to reset actions to zero?
|
106
|
+
if(actions.length > 0 && actions[0].time != incomingActions[0].time){
|
107
|
+
actions = []
|
108
|
+
}
|
109
|
+
|
110
|
+
let action = incomingActions[actions.length]
|
111
|
+
logger.debug("Dequeing action ", action)
|
112
|
+
actions.push(action)
|
113
|
+
return action
|
114
|
+
}
|
115
|
+
|
116
|
+
const pull = (callback) => {
|
117
|
+
logger.debug("Pulling actions")
|
118
|
+
let incoming = dequeue()
|
119
|
+
while(incoming){
|
120
|
+
callback(incoming)
|
121
|
+
incoming = dequeue()
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
const reset = (callback) => {
|
126
|
+
logger.debug("Queue reseted")
|
127
|
+
actions = []
|
128
|
+
if(fs.existsSync(options.path)){
|
129
|
+
const success = fs.writeFileSync(options.path, "[]")
|
130
|
+
if(success) callback()
|
131
|
+
}
|
132
|
+
}
|
133
|
+
|
134
|
+
const onPull = (callback) => {
|
135
|
+
|
136
|
+
const chokidar = require('chokidar')
|
137
|
+
|
138
|
+
logger.debug("Starting to listen...")
|
139
|
+
try{
|
140
|
+
loadFile(options.path)
|
141
|
+
}catch{
|
142
|
+
logger.debug("No previeues queue file, waiting for it to be created...")
|
143
|
+
}
|
144
|
+
|
145
|
+
if(!watcher){
|
146
|
+
logger.debug(`Watching ${options.path}`)
|
147
|
+
watcher = chokidar.watch(`${options.path}`, {
|
148
|
+
persistent: true
|
149
|
+
})
|
150
|
+
}
|
151
|
+
else logger.debug("Already watching queue path")
|
152
|
+
|
153
|
+
watcher
|
154
|
+
.on('add', path => pull(callback))
|
155
|
+
.on('change', path => pull(callback))
|
156
|
+
|
157
|
+
return true
|
158
|
+
}
|
159
|
+
|
160
|
+
const onReset = (callback) => {
|
161
|
+
|
162
|
+
const chokidar = require('chokidar')
|
163
|
+
|
164
|
+
if(!watcher){
|
165
|
+
logger.debug(`Watching ${options.path}`)
|
166
|
+
watcher = chokidar.watch(`${options.path}`, {
|
167
|
+
persistent: true
|
168
|
+
})
|
169
|
+
}
|
170
|
+
|
171
|
+
watcher.on('unlink', path => reset(callback))
|
172
|
+
|
173
|
+
return true
|
174
|
+
}
|
175
|
+
|
176
|
+
|
177
|
+
module.exports = {
|
178
|
+
events,
|
179
|
+
dispatcher: (opts = {}) => {
|
180
|
+
if(!actions){
|
181
|
+
options = { ...options, ...opts }
|
182
|
+
logger.debug("Initializing queue dispatcher", options)
|
183
|
+
actions = loadDispatcher(options)
|
184
|
+
}
|
185
|
+
return { enqueue, events }
|
186
|
+
},
|
187
|
+
listener: (opts = {}) => {
|
188
|
+
if(!actions){
|
189
|
+
options = { ...options, ...opts }
|
190
|
+
logger.debug("Initializing queue listener", options)
|
191
|
+
}
|
192
|
+
return { onPull, onReset, events }
|
193
|
+
}
|
194
|
+
}
|
@@ -0,0 +1,26 @@
|
|
1
|
+
const prioritizeHTMLFile = (entryFiles) => {
|
2
|
+
let files = [];
|
3
|
+
|
4
|
+
// Find the html file and put it as latest in the files array
|
5
|
+
// in order to keep the html file opened in vscode plugin
|
6
|
+
const index = entryFiles.findIndex((file) => {
|
7
|
+
return /.*\.html$/.test(file);
|
8
|
+
});
|
9
|
+
|
10
|
+
if (index !== -1) {
|
11
|
+
for (let i = 0; i < entryFiles.length; i++) {
|
12
|
+
if (i !== index) {
|
13
|
+
files.push(entryFiles[i]);
|
14
|
+
}
|
15
|
+
}
|
16
|
+
files.push(entryFiles[index]);
|
17
|
+
} else {
|
18
|
+
files = entryFiles;
|
19
|
+
}
|
20
|
+
|
21
|
+
return files;
|
22
|
+
};
|
23
|
+
|
24
|
+
module.exports = {
|
25
|
+
prioritizeHTMLFile,
|
26
|
+
};
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# configuration and readme
|
2
|
+
!.gitignore
|
3
|
+
!.gitpod.yml
|
4
|
+
!.gitpod.Dockerfile
|
5
|
+
!learn.json
|
6
|
+
!README.md
|
7
|
+
|
8
|
+
# exercises
|
9
|
+
!.learn/
|
10
|
+
!.learn/*
|
11
|
+
.learn/_app
|
12
|
+
.learn/.session
|
13
|
+
.learn/dist
|
14
|
+
.learn/app.tar.gz
|
15
|
+
.learn/config.json
|
16
|
+
|
17
|
+
# python compiled files
|
18
|
+
*.pyc
|
19
|
+
__pycache__/
|
20
|
+
.pytest_cache/
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# `01` Hola Mundo
|
2
|
+
|
3
|
+
Puedes tener un archivo README el cual será como una página de un libro, sin archivos de código.
|
4
|
+
|
5
|
+
También puedes agregar un archivo `README.[lenguaje].md` para traducciones, por ejemplo `README.es.md` para español.
|
6
|
+
|
7
|
+
|
8
|
+
## Inserta videos
|
9
|
+
|
10
|
+
Si quieres incluir algún video introductorio para cada ejercicio, agrega la propiedad `intro` en el inicio del README.md para ese ejercicio en particular:
|
11
|
+
|
12
|
+
```markdown
|
13
|
+
---
|
14
|
+
intro: "https://www.youtube.com/watch?v=YkgkThdzX-8"
|
15
|
+
---
|
16
|
+
```
|
17
|
+
|
18
|
+
Tambien puedes agregar un video explicando la solución para cada ejercicio agregando la propiedad `tutorial` al inicio del markdown del README.md correspondiente:
|
19
|
+
|
20
|
+
|
21
|
+
```markdown
|
22
|
+
---
|
23
|
+
intro: "https://www.youtube.com/watch?v=YkgkThdzX-8"
|
24
|
+
tutorial: "https://www.youtube.com/watch?v=YkgkThdzX-8"
|
25
|
+
---
|
26
|
+
```
|