@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.
Files changed (54) hide show
  1. package/README.md +695 -0
  2. package/bin/run +5 -0
  3. package/bin/run.cmd +3 -0
  4. package/oclif.manifest.json +1 -0
  5. package/package.json +111 -0
  6. package/plugin/command/compile.js +17 -0
  7. package/plugin/command/test.js +29 -0
  8. package/plugin/index.js +6 -0
  9. package/plugin/plugin.js +71 -0
  10. package/plugin/utils.js +78 -0
  11. package/src/commands/audit.js +243 -0
  12. package/src/commands/clean.js +27 -0
  13. package/src/commands/download.js +52 -0
  14. package/src/commands/hello.js +20 -0
  15. package/src/commands/init.js +133 -0
  16. package/src/commands/login.js +45 -0
  17. package/src/commands/logout.js +39 -0
  18. package/src/commands/publish.js +78 -0
  19. package/src/commands/start.js +169 -0
  20. package/src/commands/test.js +85 -0
  21. package/src/index.js +1 -0
  22. package/src/managers/config/allowed_files.js +12 -0
  23. package/src/managers/config/defaults.js +32 -0
  24. package/src/managers/config/exercise.js +212 -0
  25. package/src/managers/config/index.js +342 -0
  26. package/src/managers/file.js +137 -0
  27. package/src/managers/server/index.js +62 -0
  28. package/src/managers/server/routes.js +151 -0
  29. package/src/managers/session.js +83 -0
  30. package/src/managers/socket.js +185 -0
  31. package/src/managers/test.js +77 -0
  32. package/src/ui/download.js +48 -0
  33. package/src/utils/BaseCommand.js +34 -0
  34. package/src/utils/SessionCommand.js +46 -0
  35. package/src/utils/api.js +164 -0
  36. package/src/utils/audit.js +114 -0
  37. package/src/utils/console.js +16 -0
  38. package/src/utils/errors.js +90 -0
  39. package/src/utils/exercisesQueue.js +45 -0
  40. package/src/utils/fileQueue.js +194 -0
  41. package/src/utils/misc.js +26 -0
  42. package/src/utils/templates/gitignore.txt +20 -0
  43. package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.es.md +26 -0
  44. package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.md +25 -0
  45. package/src/utils/templates/incremental/README.ejs +5 -0
  46. package/src/utils/templates/incremental/README.es.ejs +5 -0
  47. package/src/utils/templates/isolated/01-hello-world/README.es.md +27 -0
  48. package/src/utils/templates/isolated/01-hello-world/README.md +27 -0
  49. package/src/utils/templates/isolated/README.ejs +5 -0
  50. package/src/utils/templates/isolated/README.es.ejs +5 -0
  51. package/src/utils/templates/no-grading/README.ejs +5 -0
  52. package/src/utils/templates/no-grading/README.es.ejs +5 -0
  53. package/src/utils/validators.js +15 -0
  54. package/src/utils/watcher.js +24 -0
@@ -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
+ ```