@learnpack/learnpack 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
```
|