@learnpack/learnpack 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +51 -398
- package/bin/run +14 -2
- package/oclif.manifest.json +1 -1
- package/package.json +135 -111
- package/src/commands/audit.ts +462 -0
- package/src/commands/clean.ts +29 -0
- package/src/commands/download.ts +62 -0
- package/src/commands/init.ts +169 -0
- package/src/commands/login.ts +42 -0
- package/src/commands/logout.ts +43 -0
- package/src/commands/publish.ts +107 -0
- package/src/commands/start.ts +229 -0
- package/src/commands/{test.js → test.ts} +19 -21
- package/src/index.ts +1 -0
- package/src/managers/config/allowed_files.ts +29 -0
- package/src/managers/config/defaults.ts +33 -0
- package/src/managers/config/exercise.ts +295 -0
- package/src/managers/config/index.ts +411 -0
- package/src/managers/file.ts +169 -0
- package/src/managers/gitpod.ts +84 -0
- package/src/managers/server/{index.js → index.ts} +26 -19
- package/src/managers/server/routes.ts +250 -0
- package/src/managers/session.ts +118 -0
- package/src/managers/socket.ts +239 -0
- package/src/managers/test.ts +83 -0
- package/src/models/action.ts +3 -0
- package/src/models/audit-errors.ts +4 -0
- package/src/models/config-manager.ts +23 -0
- package/src/models/config.ts +74 -0
- package/src/models/counter.ts +11 -0
- package/src/models/errors.ts +22 -0
- package/src/models/exercise-obj.ts +26 -0
- package/src/models/file.ts +5 -0
- package/src/models/findings.ts +18 -0
- package/src/models/flags.ts +10 -0
- package/src/models/front-matter.ts +11 -0
- package/src/models/gitpod-data.ts +19 -0
- package/src/models/language.ts +4 -0
- package/src/models/package.ts +7 -0
- package/src/models/plugin-config.ts +17 -0
- package/src/models/session.ts +26 -0
- package/src/models/socket.ts +48 -0
- package/src/models/status.ts +15 -0
- package/src/models/success-types.ts +1 -0
- package/src/plugin/command/compile.ts +17 -0
- package/src/plugin/command/test.ts +30 -0
- package/src/plugin/index.ts +6 -0
- package/src/plugin/plugin.ts +94 -0
- package/src/plugin/utils.ts +87 -0
- package/src/types/node-fetch.d.ts +1 -0
- package/src/ui/download.ts +71 -0
- package/src/utils/BaseCommand.ts +48 -0
- package/src/utils/SessionCommand.ts +48 -0
- package/src/utils/api.ts +194 -0
- package/src/utils/audit.ts +162 -0
- package/src/utils/console.ts +24 -0
- package/src/utils/errors.ts +117 -0
- package/src/utils/{exercisesQueue.js → exercisesQueue.ts} +12 -6
- package/src/utils/fileQueue.ts +198 -0
- package/src/utils/misc.ts +23 -0
- package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.es.md +2 -4
- package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.md +1 -2
- package/src/utils/templates/isolated/01-hello-world/README.es.md +1 -2
- package/src/utils/templates/isolated/01-hello-world/README.md +1 -2
- package/src/utils/templates/isolated/README.ejs +1 -1
- package/src/utils/templates/isolated/README.es.ejs +1 -1
- package/src/utils/validators.ts +18 -0
- package/src/utils/watcher.ts +27 -0
- package/plugin/command/compile.js +0 -17
- package/plugin/command/test.js +0 -29
- package/plugin/index.js +0 -6
- package/plugin/plugin.js +0 -71
- package/plugin/utils.js +0 -78
- package/src/commands/audit.js +0 -243
- package/src/commands/clean.js +0 -27
- package/src/commands/download.js +0 -52
- package/src/commands/hello.js +0 -20
- package/src/commands/init.js +0 -133
- package/src/commands/login.js +0 -45
- package/src/commands/logout.js +0 -39
- package/src/commands/publish.js +0 -78
- package/src/commands/start.js +0 -169
- package/src/index.js +0 -1
- package/src/managers/config/allowed_files.js +0 -12
- package/src/managers/config/defaults.js +0 -32
- package/src/managers/config/exercise.js +0 -212
- package/src/managers/config/index.js +0 -342
- package/src/managers/file.js +0 -137
- package/src/managers/server/routes.js +0 -151
- package/src/managers/session.js +0 -83
- package/src/managers/socket.js +0 -185
- package/src/managers/test.js +0 -77
- package/src/ui/download.js +0 -48
- package/src/utils/BaseCommand.js +0 -34
- package/src/utils/SessionCommand.js +0 -46
- package/src/utils/api.js +0 -164
- package/src/utils/audit.js +0 -114
- package/src/utils/console.js +0 -16
- package/src/utils/errors.js +0 -90
- package/src/utils/fileQueue.js +0 -194
- package/src/utils/misc.js +0 -26
- package/src/utils/validators.js +0 -15
- package/src/utils/watcher.js +0 -24
package/src/commands/publish.js
DELETED
@@ -1,78 +0,0 @@
|
|
1
|
-
const {Command, flags} = require('@oclif/command')
|
2
|
-
const { prompt } = require("enquirer")
|
3
|
-
const fetch = require('node-fetch');
|
4
|
-
const SessionCommand = require('../utils/SessionCommand')
|
5
|
-
const Console = require('../utils/console');
|
6
|
-
const api = require('../utils/api');
|
7
|
-
const { replace } = require('node-emoji');
|
8
|
-
const { validURL } = require("../utils/validators")
|
9
|
-
// const BaseCommand = require('../utils/BaseCommand');
|
10
|
-
|
11
|
-
class PublishCommand extends SessionCommand {
|
12
|
-
async init() {
|
13
|
-
const {flags} = this.parse(PublishCommand)
|
14
|
-
await this.initSession(flags, true)
|
15
|
-
}
|
16
|
-
async run() {
|
17
|
-
const {flags, args} = this.parse(PublishCommand)
|
18
|
-
|
19
|
-
// avoid annonymus sessions
|
20
|
-
if(!this.session) return;
|
21
|
-
Console.info(`Session found for ${this.session.payload.email}, publishing the package...`)
|
22
|
-
|
23
|
-
const configObject = this.configManager.get()
|
24
|
-
if(configObject.slug === undefined || !configObject.slug)
|
25
|
-
throw new Error("The package is missing a slug (unique name identifier), please check your learn.json file and make sure it has a 'slug'")
|
26
|
-
if(!validURL(configObject.repository))
|
27
|
-
throw new Error("The package has a missing or invalid 'repository' on the configuration file, it needs to be a Github URL")
|
28
|
-
else{
|
29
|
-
const validateResp = await fetch(configObject.repository);
|
30
|
-
if(validateResp.status !== 200)
|
31
|
-
throw new Error(`The specified repository URL on the configuration file does not exist or its private, only public repositories are allowed at the moment: ${configObject.repository}`)
|
32
|
-
}
|
33
|
-
|
34
|
-
// start watching for file changes
|
35
|
-
try{
|
36
|
-
const data = await api.publish({
|
37
|
-
...configObject,
|
38
|
-
author: this.session.payload.user_id
|
39
|
-
});
|
40
|
-
Console.success(`Package updated and published successfully: ${configObject.slug}`)
|
41
|
-
}catch(error){
|
42
|
-
if(error.status === 404){
|
43
|
-
const answer = await prompt([{
|
44
|
-
type: 'confirm',
|
45
|
-
name: 'create',
|
46
|
-
message: `Package with slug ${configObject.slug} does not exist, do you want to create it?`,
|
47
|
-
}])
|
48
|
-
if(answer){
|
49
|
-
const data2 = await api.update({
|
50
|
-
...configObject,
|
51
|
-
author: this.session.payload.user_id
|
52
|
-
})
|
53
|
-
Console.success(`Package created and published successfully: ${configObject.slug}`)
|
54
|
-
}
|
55
|
-
else Console.error("No answer from server")
|
56
|
-
}
|
57
|
-
else Console.error(error.message)
|
58
|
-
}
|
59
|
-
}
|
60
|
-
}
|
61
|
-
|
62
|
-
PublishCommand.description = `Describe the command here
|
63
|
-
...
|
64
|
-
Extra documentation goes here
|
65
|
-
`
|
66
|
-
PublishCommand.flags = {
|
67
|
-
// name: flags.string({char: 'n', description: 'name to print'}),
|
68
|
-
}
|
69
|
-
PublishCommand.args =[
|
70
|
-
{
|
71
|
-
name: 'package', // name of arg to show in help and reference with args[name]
|
72
|
-
required: false, // make the arg required with `required: true`
|
73
|
-
description: 'The unique string that identifies this package on learnpack', // help description
|
74
|
-
hidden: false // hide this arg from help
|
75
|
-
}
|
76
|
-
]
|
77
|
-
|
78
|
-
module.exports = PublishCommand
|
package/src/commands/start.js
DELETED
@@ -1,169 +0,0 @@
|
|
1
|
-
const path = require("path")
|
2
|
-
const {flags} = require('@oclif/command')
|
3
|
-
const SessionCommand = require('../utils/SessionCommand')
|
4
|
-
const Console = require('../utils/console')
|
5
|
-
const socket = require('../managers/socket.js')
|
6
|
-
const queue = require("../utils/fileQueue")
|
7
|
-
const { download, decompress, downloadEditor } = require('../managers/file.js')
|
8
|
-
const { prioritizeHTMLFile } = require('../utils/misc')
|
9
|
-
|
10
|
-
const createServer = require('../managers/server')
|
11
|
-
|
12
|
-
class StartCommand extends SessionCommand {
|
13
|
-
constructor(...params){
|
14
|
-
super(...params)
|
15
|
-
}
|
16
|
-
|
17
|
-
// 🛑 IMPORTANT:
|
18
|
-
// Every command that will use the configManager needs this init method
|
19
|
-
async init() {
|
20
|
-
const {flags} = this.parse(StartCommand)
|
21
|
-
await this.initSession(flags)
|
22
|
-
}
|
23
|
-
|
24
|
-
async run() {
|
25
|
-
|
26
|
-
// const {flags} = this.parse(StartCommand)
|
27
|
-
|
28
|
-
// get configuration object
|
29
|
-
const configObject = this.configManager.get()
|
30
|
-
const { config } = configObject;
|
31
|
-
|
32
|
-
// build exercises
|
33
|
-
this.configManager.buildIndex()
|
34
|
-
|
35
|
-
Console.debug(`Grading: ${config.grading} ${config.disableGrading ? "(disabled)" : ""}, editor: ${config.editor.mode} ${config.editor.version}, for ${Array.isArray(config.exercises) ? config.exercises.length : 0} exercises found`)
|
36
|
-
|
37
|
-
// download app and decompress
|
38
|
-
let resp = await downloadEditor(config.editor.version, `${config.dirPath}/app.tar.gz`)
|
39
|
-
|
40
|
-
Console.info("Decompressing LearnPack UI, this may take a minute...")
|
41
|
-
await decompress(`${config.dirPath}/app.tar.gz`, `${config.dirPath}/_app/`)
|
42
|
-
|
43
|
-
const server = await createServer(configObject, this.configManager)
|
44
|
-
|
45
|
-
const dispatcher = queue.dispatcher({ create: true, path: `${config.dirPath}/vscode_queue.json` })
|
46
|
-
|
47
|
-
// listen to socket commands
|
48
|
-
socket.start(config, server)
|
49
|
-
|
50
|
-
socket.on("open", (data) => {
|
51
|
-
Console.debug("Opening these files: ", data)
|
52
|
-
|
53
|
-
let files = prioritizeHTMLFile(data.files);
|
54
|
-
|
55
|
-
dispatcher.enqueue(dispatcher.events.OPEN_FILES, files);
|
56
|
-
socket.ready('Ready to compile...')
|
57
|
-
})
|
58
|
-
|
59
|
-
socket.on("open_window", (data) => {
|
60
|
-
Console.debug("Opening window: ", data)
|
61
|
-
dispatcher.enqueue(dispatcher.events.OPEN_WINDOW, data)
|
62
|
-
socket.ready('Ready to compile...')
|
63
|
-
})
|
64
|
-
|
65
|
-
socket.on("reset", (exercise) => {
|
66
|
-
try{
|
67
|
-
this.configManager.reset(exercise.exerciseSlug)
|
68
|
-
dispatcher.enqueue(dispatcher.events.RESET_EXERCISE, exercise.exerciseSlug)
|
69
|
-
socket.ready('Ready to compile...')
|
70
|
-
}
|
71
|
-
catch(error){
|
72
|
-
socket.error('compiler-error', error.message || "There was an error reseting the exercise")
|
73
|
-
setTimeout(() => socket.ready('Ready to compile...'), 2000)
|
74
|
-
}
|
75
|
-
})
|
76
|
-
// socket.on("preview", (data) => {
|
77
|
-
// Console.debug("Preview triggered, removing the 'preview' action ")
|
78
|
-
// socket.removeAllowed("preview")
|
79
|
-
// socket.log('ready',['Ready to compile...'])
|
80
|
-
// })
|
81
|
-
|
82
|
-
socket.on("build", async (data) => {
|
83
|
-
const exercise = this.configManager.getExercise(data.exerciseSlug)
|
84
|
-
|
85
|
-
if(!exercise.language){
|
86
|
-
socket.error('compiler-error','Impossible to detect language to build for '+data.exerciseSlug+'...')
|
87
|
-
return;
|
88
|
-
}
|
89
|
-
|
90
|
-
// validate plugins installation for compiler
|
91
|
-
//if(!this.configManager.validateEngine(exercise.language, server, socket)) return false;
|
92
|
-
|
93
|
-
socket.log('compiling','Building exercise '+data.exerciseSlug+' with '+exercise.language+'...')
|
94
|
-
const stdout = await this.config.runHook('action', {
|
95
|
-
action: 'compile',
|
96
|
-
socket, configuration: config,
|
97
|
-
exercise,
|
98
|
-
})
|
99
|
-
|
100
|
-
|
101
|
-
})
|
102
|
-
|
103
|
-
socket.on("test", async (data) => {
|
104
|
-
const exercise = this.configManager.getExercise(data.exerciseSlug)
|
105
|
-
|
106
|
-
if(!exercise.language){
|
107
|
-
socket.error('compiler-error','Impossible to detect engine language for testing for '+data.exerciseSlug+'...')
|
108
|
-
return;
|
109
|
-
}
|
110
|
-
|
111
|
-
if(config.disableGrading){
|
112
|
-
socket.ready('Grading is disabled on configuration')
|
113
|
-
return true;
|
114
|
-
}
|
115
|
-
|
116
|
-
// validate plugins installation for compiler
|
117
|
-
//if(!this.configManager.validateEngine(exercise.language, server, socket)) return false;
|
118
|
-
|
119
|
-
socket.log('testing','Testing your exercise using the '+exercise.language+' engine.')
|
120
|
-
|
121
|
-
const stdout = await this.config.runHook('action', {
|
122
|
-
action: 'test',
|
123
|
-
socket, configuration: config,
|
124
|
-
exercise,
|
125
|
-
})
|
126
|
-
this.configManager.save()
|
127
|
-
|
128
|
-
return true;
|
129
|
-
})
|
130
|
-
|
131
|
-
const terminate = () => {
|
132
|
-
Console.debug("Terminating Learnpack...")
|
133
|
-
server.terminate(() => {
|
134
|
-
this.configManager.noCurrentExercise()
|
135
|
-
dispatcher.enqueue(dispatcher.events.END)
|
136
|
-
process.exit();
|
137
|
-
})
|
138
|
-
}
|
139
|
-
|
140
|
-
server.on('close', terminate);
|
141
|
-
process.on('SIGINT', terminate);
|
142
|
-
process.on('SIGTERM', terminate);
|
143
|
-
process.on('SIGHUP', terminate);
|
144
|
-
|
145
|
-
|
146
|
-
// finish the server startup
|
147
|
-
setTimeout(() => dispatcher.enqueue(dispatcher.events.RUNNING), 1000)
|
148
|
-
|
149
|
-
// start watching for file changes
|
150
|
-
if(flags.watch) this.configManager.watchIndex((_exercises) => socket.reload(null, _exercises));
|
151
|
-
|
152
|
-
}
|
153
|
-
|
154
|
-
}
|
155
|
-
|
156
|
-
StartCommand.description = `Runs a small server with all the exercise instructions`
|
157
|
-
|
158
|
-
StartCommand.flags = {
|
159
|
-
...SessionCommand.flags,
|
160
|
-
port: flags.string({char: 'p', description: 'server port' }),
|
161
|
-
host: flags.string({char: 'h', description: 'server host' }),
|
162
|
-
disableGrading: flags.boolean({char: 'dg', description: 'disble grading functionality' }),
|
163
|
-
watch: flags.boolean({char: 'w', description: 'Watch for file changes', default: false }),
|
164
|
-
mode: flags.string({ char: 'm', description: 'Load a standalone editor or just the preview to be embeded in another editor: Choices: [standalone, preview]', options: ['standalone', 'preview'] }),
|
165
|
-
version: flags.string({ char: 'v', description: 'E.g: 1.0.1', default: null }),
|
166
|
-
grading: flags.string({ char: 'g', description: '[isolated, incremental]', options: ['isolated', 'incremental'] }),
|
167
|
-
debug: flags.boolean({char: 'd', description: 'debugger mode for more verbage', default: false })
|
168
|
-
}
|
169
|
-
module.exports = StartCommand
|
package/src/index.js
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
module.exports = require('@oclif/command')
|
@@ -1,12 +0,0 @@
|
|
1
|
-
module.exports = {
|
2
|
-
extensions: [
|
3
|
-
'py', 'java','py','ruby', 'html', 'css', 'htm', 'php', //images
|
4
|
-
'js','jsx', 'ts', //images
|
5
|
-
'sh','bash', //images
|
6
|
-
'json', 'yml', 'yaml', 'csv', 'xml', // file storage extensions
|
7
|
-
'txt', 'text', 'markdown', 'readme', // compressed files
|
8
|
-
],
|
9
|
-
names: [
|
10
|
-
'package.json', 'package-lock.json'
|
11
|
-
]
|
12
|
-
}
|
@@ -1,32 +0,0 @@
|
|
1
|
-
module.exports = {
|
2
|
-
config: {
|
3
|
-
port: 3000,
|
4
|
-
address: "http://localhost",
|
5
|
-
editor: {
|
6
|
-
mode: null, //[standalone, preview]
|
7
|
-
agent: null, //[vscode, theia]
|
8
|
-
version: null
|
9
|
-
},
|
10
|
-
dirPath: './.learn',
|
11
|
-
configPath: './learn.json',
|
12
|
-
outputPath: './.learn/dist',
|
13
|
-
publicPath: '/preview',
|
14
|
-
publicUrl: null,
|
15
|
-
language: "auto",
|
16
|
-
grading: 'isolated', // [isolated, incremental]
|
17
|
-
exercisesPath: './', // path to the folder that contains the exercises
|
18
|
-
webpackTemplate: null, // if you want webpack to use an HTML template
|
19
|
-
disableGrading: false,
|
20
|
-
disabledActions: [], //Possible: 'build', 'test' or 'reset'
|
21
|
-
actions: [], // ⚠️ deprecated, leave empty )
|
22
|
-
entries: {
|
23
|
-
html: "index.html",
|
24
|
-
vanillajs: "index.js",
|
25
|
-
react: "app.jsx",
|
26
|
-
node: "app.js",
|
27
|
-
python3: "app.py",
|
28
|
-
java: "app.java",
|
29
|
-
}
|
30
|
-
},
|
31
|
-
currentExercise: null
|
32
|
-
}
|
@@ -1,212 +0,0 @@
|
|
1
|
-
const p = require("path")
|
2
|
-
const frontMatter = require('front-matter')
|
3
|
-
const fs = require("fs")
|
4
|
-
let Console = require('../../utils/console');
|
5
|
-
const allowed = require('./allowed_files')
|
6
|
-
const { ValidationError } = require('../../utils/errors.js')
|
7
|
-
|
8
|
-
const exercise = (path, position, configObject) => {
|
9
|
-
|
10
|
-
const { config, exercises } = configObject;
|
11
|
-
let slug = p.basename(path)
|
12
|
-
|
13
|
-
if(!validateExerciseDirectoryName(slug)){
|
14
|
-
Console.error('Exercise directory "'+slug+'" has an invalid name, it has to start with two or three digits followed by words separated by underscors or hyphen (no white spaces). e.g: 01.12-hello-world')
|
15
|
-
Console.help('Verify that the folder "'+slug+'" starts with two numbers and it does not contain white spaces or weird characters.')
|
16
|
-
throw ValidationError(`This exercise has a invalid name: ${slug}`)
|
17
|
-
}
|
18
|
-
|
19
|
-
// get all the files
|
20
|
-
const files = fs.readdirSync(path)
|
21
|
-
|
22
|
-
/**
|
23
|
-
* build the translation array like:
|
24
|
-
{
|
25
|
-
"us": "path/to/Readme.md",
|
26
|
-
"es": "path/to/Readme.es.md"
|
27
|
-
}
|
28
|
-
*/
|
29
|
-
var translations = {}
|
30
|
-
files.filter(file => file.toLowerCase().includes('readme')).forEach(file => {
|
31
|
-
const parts = file.split('.')
|
32
|
-
if(parts.length === 3) translations[parts[1]] = file
|
33
|
-
else translations["us"] = file
|
34
|
-
})
|
35
|
-
|
36
|
-
// if the slug is a dot, it means there is not "exercises" folder, and its just a single README.md
|
37
|
-
if(slug == ".") slug = "default-index";
|
38
|
-
|
39
|
-
const detected = detect(configObject, files);
|
40
|
-
return {
|
41
|
-
position, path, slug, translations,
|
42
|
-
language: detected.language,
|
43
|
-
entry: detected.entry ? path + "/" + detected.entry : null, //full path to the exercise entry
|
44
|
-
title: slug || "Exercise",
|
45
|
-
graded: files.filter(file => file.toLowerCase().startsWith('test.') || file.toLowerCase().startsWith('tests.')).length > 0,
|
46
|
-
files: filterFiles(files, path),
|
47
|
-
//if the exercises was on the config before I may keep the status done
|
48
|
-
done: (Array.isArray(exercises) && typeof exercises[position] !== 'undefined' && path.substring(path.indexOf('exercises/')+10) == exercises[position].slug) ? exercises[position].done : false,
|
49
|
-
getReadme: function(lang=null){
|
50
|
-
if(lang == 'us') lang = null // <-- english is default, no need to append it to the file name
|
51
|
-
if (!fs.existsSync(`${this.path}/README${lang ? "."+lang : ''}.md`)){
|
52
|
-
Console.error(`Language ${lang} not found for exercise ${slug}, switching to default language`)
|
53
|
-
if(lang) lang = null
|
54
|
-
if (!fs.existsSync(`${this.path}/README${lang ? "."+lang : ''}.md`)) throw Error('Readme file not found for exercise: '+this.path+'/README.md')
|
55
|
-
}
|
56
|
-
let content = fs.readFileSync(`${this.path}/README${lang ? "."+lang : ''}.md`,"utf8")
|
57
|
-
// content = content.replace(/!\[.*\](../../assets/script-test.gif)/, "<div>$1</div>")
|
58
|
-
const attr = frontMatter(content)
|
59
|
-
return attr
|
60
|
-
},
|
61
|
-
getFile: function(name){
|
62
|
-
const file = this.files.find(f => f.name === name);
|
63
|
-
if (!fs.existsSync(file.path)) throw Error('File not found: '+file.path)
|
64
|
-
else if(fs.lstatSync(file.path).isDirectory()) return 'Error: This is not a file to be read, but a directory: '+file.path
|
65
|
-
|
66
|
-
// get file content
|
67
|
-
const content = fs.readFileSync(file.path)
|
68
|
-
|
69
|
-
//create reset folder
|
70
|
-
if (!fs.existsSync(`${config.dirPath}/resets`)) fs.mkdirSync(`${config.dirPath}/resets`)
|
71
|
-
if (!fs.existsSync(`${config.dirPath}/resets/`+this.slug)){
|
72
|
-
fs.mkdirSync(`${config.dirPath}/resets/`+this.slug)
|
73
|
-
if (!fs.existsSync(`${config.dirPath}/resets/${this.slug}/${name}`)){
|
74
|
-
fs.writeFileSync(`${config.dirPath}/resets/${this.slug}/${name}`, content)
|
75
|
-
}
|
76
|
-
}
|
77
|
-
|
78
|
-
return content
|
79
|
-
},
|
80
|
-
saveFile: function(name, content){
|
81
|
-
const file = this.files.find(f => f.name === name);
|
82
|
-
if (!fs.existsSync(file.path)) throw Error('File not found: '+file.path)
|
83
|
-
return fs.writeFileSync(file.path, content, 'utf8')
|
84
|
-
},
|
85
|
-
getTestReport: function(){
|
86
|
-
const _path = `${config.confPath.base}/reports/${this.slug}.json`
|
87
|
-
if (!fs.existsSync(_path)) return {}
|
88
|
-
|
89
|
-
const content = fs.readFileSync(_path)
|
90
|
-
const data = JSON.parse(content)
|
91
|
-
return data
|
92
|
-
},
|
93
|
-
}
|
94
|
-
}
|
95
|
-
|
96
|
-
const validateExerciseDirectoryName = (str) => {
|
97
|
-
if(str == "./") return true;
|
98
|
-
const regex = /^(\d{2,3}(\.\d{1,2})?-([A-Za-z0-9]+(-|_)?)+)$/
|
99
|
-
return regex.test(str)
|
100
|
-
}
|
101
|
-
|
102
|
-
const isCodable = (str) => {
|
103
|
-
const extension = p.extname(str);
|
104
|
-
return allowed.extensions.includes(extension.substring(1).toLowerCase());
|
105
|
-
}
|
106
|
-
|
107
|
-
const isNotConfiguration = (str) => {
|
108
|
-
return !allowed.names.includes(str);
|
109
|
-
}
|
110
|
-
|
111
|
-
const shouldBeVisible = function(file){
|
112
|
-
return (
|
113
|
-
// doest not have "test." on their name
|
114
|
-
(file.name.toLocaleLowerCase().indexOf('test.') == -1 && file.name.toLocaleLowerCase().indexOf('tests.') == -1 && file.name.toLocaleLowerCase().indexOf('.hide.') == -1 &&
|
115
|
-
// ignore hidden files
|
116
|
-
(file.name.charAt(0) != '.') &&
|
117
|
-
// ignore learn.json and bc.json
|
118
|
-
(file.name.toLocaleLowerCase().indexOf('learn.json') == -1) && (file.name.toLocaleLowerCase().indexOf('bc.json') == -1) &&
|
119
|
-
// ignore images, videos, vectors, etc.
|
120
|
-
isCodable(file.name) && isNotConfiguration(file.name) &&
|
121
|
-
// readme's and directories
|
122
|
-
!file.name.toLowerCase().includes("readme.") && !isDirectory(file.path) && file.name.charAt(0) != '_')
|
123
|
-
);
|
124
|
-
}
|
125
|
-
|
126
|
-
const isDirectory = source => {
|
127
|
-
//if(path.basename(source) === path.basename(config.dirPath)) return false
|
128
|
-
return fs.lstatSync(source).isDirectory()
|
129
|
-
}
|
130
|
-
|
131
|
-
/**
|
132
|
-
* Learnpack must be able to AUTOMATICALLY detect language.
|
133
|
-
* Because learnpack can work with multilang exercises.
|
134
|
-
*/
|
135
|
-
const detect = (configObject, files) => {
|
136
|
-
|
137
|
-
const { config } = configObject;
|
138
|
-
|
139
|
-
if(!config) throw Error(`No configuration found during the engine detection`)
|
140
|
-
|
141
|
-
if(!config.entries) throw Error("No configuration found for entries, please add a 'entries' object with the default file name for your exercise entry file that is going to be used while compiling, for example: index.html for html, app.py for python3, etc.")
|
142
|
-
//A language was found on the config object, but this language will only be used as last resort, learnpack will try to guess each exercise language independently based on file extension (js, jsx, html, etc.)
|
143
|
-
|
144
|
-
let hasFiles = files.filter(f => f.includes('.py'))
|
145
|
-
if(hasFiles.length > 0) return {
|
146
|
-
language: "python3",
|
147
|
-
entry: hasFiles.find(f => config.entries["python3"] === f)
|
148
|
-
}
|
149
|
-
|
150
|
-
hasFiles = files.filter(f => f.includes('.java'))
|
151
|
-
if(hasFiles.length > 0) return {
|
152
|
-
language: "java",
|
153
|
-
entry: hasFiles.find(f => config.entries["java"] === f)
|
154
|
-
}
|
155
|
-
|
156
|
-
hasFiles = files.filter(f => f.includes('.jsx'))
|
157
|
-
if(hasFiles.length > 0) return {
|
158
|
-
language: "react",
|
159
|
-
entry: hasFiles.find(f => config.entries["react"] === f)
|
160
|
-
}
|
161
|
-
const hasHTML = files.filter(f => f.includes('index.html'))
|
162
|
-
const hasIndexJS = files.find(f => f.includes('index.js'))
|
163
|
-
const hasJS = files.filter(f => f.includes('.js'))
|
164
|
-
// angular, vue, vanillajs needs to have at least 2 files (html,css,js),
|
165
|
-
// the test.js and the entry file in js
|
166
|
-
// if not its just another HTML
|
167
|
-
|
168
|
-
if(hasIndexJS && hasHTML.length > 0) return {
|
169
|
-
language: "vanillajs",
|
170
|
-
entry: hasIndexJS
|
171
|
-
}
|
172
|
-
else if(hasHTML.length > 0) return {
|
173
|
-
language: "html",
|
174
|
-
entry: hasHTML.find(f => config.entries["html"] === f)
|
175
|
-
}
|
176
|
-
else if(hasJS.length > 0) return {
|
177
|
-
language: "node",
|
178
|
-
entry: hasJS.find(f => config.entries["node"] === f)
|
179
|
-
}
|
180
|
-
|
181
|
-
return {
|
182
|
-
language: null,
|
183
|
-
entry: null
|
184
|
-
};
|
185
|
-
}
|
186
|
-
|
187
|
-
const filterFiles = (files, basePath=".") => files.map(ex => ({
|
188
|
-
path: basePath+'/'+ex,
|
189
|
-
name: ex,
|
190
|
-
hidden: !shouldBeVisible({ name: ex, path: basePath+'/'+ex })
|
191
|
-
}))
|
192
|
-
.sort((f1, f2) => {
|
193
|
-
const score = { // sorting priority
|
194
|
-
"index.html": 1,
|
195
|
-
"styles.css": 2,
|
196
|
-
"styles.scss": 2,
|
197
|
-
"style.css": 2,
|
198
|
-
"style.scss": 2,
|
199
|
-
"index.css": 2,
|
200
|
-
"index.scss": 2,
|
201
|
-
"index.js": 3,
|
202
|
-
"index.jsx": 3,
|
203
|
-
}
|
204
|
-
return score[f1.name] < score[f2.name] ? -1 : 1
|
205
|
-
});
|
206
|
-
|
207
|
-
module.exports = {
|
208
|
-
exercise,
|
209
|
-
detect,
|
210
|
-
filterFiles,
|
211
|
-
validateExerciseDirectoryName
|
212
|
-
}
|