@learnpack/learnpack 1.0.0

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