@learnpack/learnpack 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +695 -0
- package/bin/run +5 -0
- package/bin/run.cmd +3 -0
- package/oclif.manifest.json +1 -0
- package/package.json +111 -0
- package/plugin/command/compile.js +17 -0
- package/plugin/command/test.js +29 -0
- package/plugin/index.js +6 -0
- package/plugin/plugin.js +71 -0
- package/plugin/utils.js +78 -0
- package/src/commands/audit.js +243 -0
- package/src/commands/clean.js +27 -0
- package/src/commands/download.js +52 -0
- package/src/commands/hello.js +20 -0
- package/src/commands/init.js +133 -0
- package/src/commands/login.js +45 -0
- package/src/commands/logout.js +39 -0
- package/src/commands/publish.js +78 -0
- package/src/commands/start.js +169 -0
- package/src/commands/test.js +85 -0
- package/src/index.js +1 -0
- package/src/managers/config/allowed_files.js +12 -0
- package/src/managers/config/defaults.js +32 -0
- package/src/managers/config/exercise.js +212 -0
- package/src/managers/config/index.js +342 -0
- package/src/managers/file.js +137 -0
- package/src/managers/server/index.js +62 -0
- package/src/managers/server/routes.js +151 -0
- package/src/managers/session.js +83 -0
- package/src/managers/socket.js +185 -0
- package/src/managers/test.js +77 -0
- package/src/ui/download.js +48 -0
- package/src/utils/BaseCommand.js +34 -0
- package/src/utils/SessionCommand.js +46 -0
- package/src/utils/api.js +164 -0
- package/src/utils/audit.js +114 -0
- package/src/utils/console.js +16 -0
- package/src/utils/errors.js +90 -0
- package/src/utils/exercisesQueue.js +45 -0
- package/src/utils/fileQueue.js +194 -0
- package/src/utils/misc.js +26 -0
- package/src/utils/templates/gitignore.txt +20 -0
- package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.es.md +26 -0
- package/src/utils/templates/incremental/.learn/exercises/01-hello-world/README.md +25 -0
- package/src/utils/templates/incremental/README.ejs +5 -0
- package/src/utils/templates/incremental/README.es.ejs +5 -0
- package/src/utils/templates/isolated/01-hello-world/README.es.md +27 -0
- package/src/utils/templates/isolated/01-hello-world/README.md +27 -0
- package/src/utils/templates/isolated/README.ejs +5 -0
- package/src/utils/templates/isolated/README.es.ejs +5 -0
- package/src/utils/templates/no-grading/README.ejs +5 -0
- package/src/utils/templates/no-grading/README.es.ejs +5 -0
- package/src/utils/validators.js +15 -0
- package/src/utils/watcher.js +24 -0
package/package.json
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
{
|
2
|
+
"name": "@learnpack/learnpack",
|
3
|
+
"description": "Seamlessly build or take interactive & auto-graded tutorials, start learning now or build a tutorial.",
|
4
|
+
"version": "1.0.0",
|
5
|
+
"author": "Alejandro Sanchez @alesanchezr",
|
6
|
+
"bin": {
|
7
|
+
"learnpack": "bin/run"
|
8
|
+
},
|
9
|
+
"bugs": {
|
10
|
+
"url": "https://github.com/learnpack/learnpack-cli/issues"
|
11
|
+
},
|
12
|
+
"dependencies": {
|
13
|
+
"@oclif/command": "^1.6.1",
|
14
|
+
"@oclif/config": "^1.15.1",
|
15
|
+
"@oclif/plugin-help": "^3.1.0",
|
16
|
+
"@oclif/plugin-plugins": "^1.8.0",
|
17
|
+
"@oclif/plugin-warn-if-update-available": "^1.7.0",
|
18
|
+
"body-parser": "^1.19.0",
|
19
|
+
"chalk": "^4.1.0",
|
20
|
+
"chokidar": "^3.4.0",
|
21
|
+
"cli-ux": "^5.4.6",
|
22
|
+
"cors": "^2.8.5",
|
23
|
+
"debounce": "^1.2.0",
|
24
|
+
"dotenv": "^8.2.0",
|
25
|
+
"enquirer": "^2.3.6",
|
26
|
+
"eta": "^1.2.0",
|
27
|
+
"express": "^4.17.1",
|
28
|
+
"front-matter": "^4.0.2",
|
29
|
+
"fs-extra": "^9.1.0",
|
30
|
+
"git-repo-info": "^2.1.1",
|
31
|
+
"moment": "^2.27.0",
|
32
|
+
"node-emoji": "^1.10.0",
|
33
|
+
"node-fetch": "^2.6.0",
|
34
|
+
"node-persist": "^3.1.0",
|
35
|
+
"prompts": "^2.3.2",
|
36
|
+
"request": "^2.88.2",
|
37
|
+
"shelljs": "^0.8.4",
|
38
|
+
"socket.io": "^2.3.0",
|
39
|
+
"targz": "^1.0.1",
|
40
|
+
"validator": "^13.1.1",
|
41
|
+
"xxhashjs": "^0.2.2"
|
42
|
+
},
|
43
|
+
"devDependencies": {
|
44
|
+
"@oclif/dev-cli": "^1.22.2",
|
45
|
+
"@oclif/test": "^1.2.6",
|
46
|
+
"chai": "^4.2.0",
|
47
|
+
"eslint": "^5.16.0",
|
48
|
+
"eslint-config-oclif": "^3.1.0",
|
49
|
+
"globby": "^10.0.2",
|
50
|
+
"mocha": "^5.2.0",
|
51
|
+
"nyc": "^14.1.1",
|
52
|
+
"pre-commit": "^1.2.2"
|
53
|
+
},
|
54
|
+
"engines": {
|
55
|
+
"node": ">=12.0.0"
|
56
|
+
},
|
57
|
+
"files": [
|
58
|
+
"/bin",
|
59
|
+
"/npm-shrinkwrap.json",
|
60
|
+
"/oclif.manifest.json",
|
61
|
+
"/plugin",
|
62
|
+
"/src"
|
63
|
+
],
|
64
|
+
"homepage": "https://learnpack.co",
|
65
|
+
"keywords": [
|
66
|
+
"learn",
|
67
|
+
"learn to code",
|
68
|
+
"tutorial",
|
69
|
+
"tutorials",
|
70
|
+
"coding tutorials",
|
71
|
+
"education",
|
72
|
+
"coding education"
|
73
|
+
],
|
74
|
+
"license": "UNLICENSED",
|
75
|
+
"main": "src/index.js",
|
76
|
+
"oclif": {
|
77
|
+
"commands": "./src/commands",
|
78
|
+
"bin": "learnpack",
|
79
|
+
"plugins": [
|
80
|
+
"@oclif/plugin-help",
|
81
|
+
"@oclif/plugin-plugins",
|
82
|
+
"@oclif/plugin-warn-if-update-available"
|
83
|
+
],
|
84
|
+
"permanent_plugins": [
|
85
|
+
"@oclif/plugin-help",
|
86
|
+
"@oclif/plugin-plugins",
|
87
|
+
"@oclif/plugin-warn-if-update-available"
|
88
|
+
]
|
89
|
+
},
|
90
|
+
"repository": {
|
91
|
+
"type": "git",
|
92
|
+
"url": "git+https://github.com/learnpack/learnpack-cli.git"
|
93
|
+
},
|
94
|
+
"pre-commit": {
|
95
|
+
"silent": true,
|
96
|
+
"run": [
|
97
|
+
"pre"
|
98
|
+
]
|
99
|
+
},
|
100
|
+
"scripts": {
|
101
|
+
"postpack": "rm -f oclif.manifest.json",
|
102
|
+
"posttest": "eslint .",
|
103
|
+
"pre": "node ./test/precommit/index.js",
|
104
|
+
"prepack": "oclif-dev manifest && oclif-dev readme",
|
105
|
+
"test": "nyc mocha --forbid-only \"test/**/*.test.js\"",
|
106
|
+
"version": "oclif-dev readme && git add README.md"
|
107
|
+
},
|
108
|
+
"directories": {
|
109
|
+
"test": "test"
|
110
|
+
}
|
111
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
|
2
|
+
const CompilationError = (messages) => {
|
3
|
+
const _err = new Error(messages)
|
4
|
+
_err.status = 400
|
5
|
+
_err.stdout = messages
|
6
|
+
_err.type = 'compiler-error'
|
7
|
+
return _err
|
8
|
+
}
|
9
|
+
|
10
|
+
module.exports = {
|
11
|
+
CompilationError,
|
12
|
+
default: async ({ action, ...rest }) => {
|
13
|
+
|
14
|
+
const stdout = await action.run(rest)
|
15
|
+
return stdout
|
16
|
+
}
|
17
|
+
}
|
@@ -0,0 +1,29 @@
|
|
1
|
+
const fs = require('fs')
|
2
|
+
|
3
|
+
const TestingError = (messages) => {
|
4
|
+
const _err = new Error(messages)
|
5
|
+
_err.status = 400
|
6
|
+
_err.stdout = messages
|
7
|
+
_err.type = 'testing-error'
|
8
|
+
return _err
|
9
|
+
}
|
10
|
+
|
11
|
+
module.exports = {
|
12
|
+
TestingError,
|
13
|
+
default: async function(args){
|
14
|
+
const { action, configuration, socket, exercise } = args;
|
15
|
+
|
16
|
+
if (!fs.existsSync(`${configuration.dirPath}/reports`)){
|
17
|
+
// reports directory
|
18
|
+
fs.mkdirSync(`${configuration.dirPath}/reports`);
|
19
|
+
}
|
20
|
+
|
21
|
+
// compile
|
22
|
+
const stdout = await action.run(args)
|
23
|
+
|
24
|
+
// mark exercise as done
|
25
|
+
exercise.done = true;
|
26
|
+
|
27
|
+
return stdout
|
28
|
+
}
|
29
|
+
}
|
package/plugin/index.js
ADDED
package/plugin/plugin.js
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
const shell = require('shelljs')
|
2
|
+
/**
|
3
|
+
* Main Plugin Runner, it defines the behavior of a learnpack plugin
|
4
|
+
* dividing it in "actions" like: Compile, test, etc.
|
5
|
+
* @param {object} pluginConfig Configuration object that must defined language and each possible action.
|
6
|
+
*/
|
7
|
+
module.exports = (pluginConfig) => {
|
8
|
+
return async (args) => {
|
9
|
+
const { action, exercise, socket, configuration } = args
|
10
|
+
|
11
|
+
|
12
|
+
if(pluginConfig.language === undefined) throw Error(`Missing language on the plugin configuration object`)
|
13
|
+
|
14
|
+
if(typeof action !== "string"){
|
15
|
+
throw Error("Missing action property on hook details")
|
16
|
+
}
|
17
|
+
|
18
|
+
if(!exercise || exercise === undefined){
|
19
|
+
throw Error("Missing exercise information")
|
20
|
+
}
|
21
|
+
|
22
|
+
// if the action does not exist I don't do anything
|
23
|
+
if(pluginConfig[action] === undefined){
|
24
|
+
console.log(`Ignoring ${action}`)
|
25
|
+
return () => null
|
26
|
+
}
|
27
|
+
|
28
|
+
// ignore if the plugin language its not the same as the exercise language
|
29
|
+
if(exercise.language !== pluginConfig.language){
|
30
|
+
return () => null
|
31
|
+
}
|
32
|
+
|
33
|
+
if( !exercise.files || exercise.files.length == 0){
|
34
|
+
throw Error(`No files to process`)
|
35
|
+
}
|
36
|
+
|
37
|
+
try{
|
38
|
+
const _action = pluginConfig[action]
|
39
|
+
|
40
|
+
if(_action == null || typeof _action != 'object') throw Error(`The ${pluginConfig.language} ${action} module must export an object configuration`)
|
41
|
+
if(_action.validate === undefined) throw Error(`Missing validate method for ${pluginConfig.language} ${action}`)
|
42
|
+
if(_action.run === undefined) throw Error(`Missing run method for ${pluginConfig.language} ${action}`)
|
43
|
+
if(_action.dependencies !== undefined){
|
44
|
+
if(!Array.isArray(_action.dependencies)) throw Error(`${action}.dependencies must be an array of package names`)
|
45
|
+
|
46
|
+
_action.dependencies.forEach(packageName => {
|
47
|
+
if (!shell.which(packageName)) {
|
48
|
+
throw Error(`🚫 You need to have ${packageName} installed to run test the exercises`);
|
49
|
+
}
|
50
|
+
})
|
51
|
+
}
|
52
|
+
const valid = await _action.validate(({ exercise, configuration }))
|
53
|
+
if(valid){
|
54
|
+
// look for the command standard implementation and execute it
|
55
|
+
const execute = require("./command/"+action+".js").default
|
56
|
+
// no matter the command, the response must always be a stdout
|
57
|
+
const stdout = await execute({ ...args, action: _action, configuration })
|
58
|
+
|
59
|
+
// Map the action names to socket messaging standards
|
60
|
+
const actionToSuccessMapper = { compile: 'compiler', test: 'testing' }
|
61
|
+
|
62
|
+
socket.success(actionToSuccessMapper[action], stdout)
|
63
|
+
return stdout
|
64
|
+
}
|
65
|
+
}
|
66
|
+
catch(error){
|
67
|
+
if(error.type == undefined) socket.fatal(error)
|
68
|
+
else socket.error(error.type, error.stdout)
|
69
|
+
}
|
70
|
+
}
|
71
|
+
}
|
package/plugin/utils.js
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
const chalk = require("chalk")
|
2
|
+
|
3
|
+
const getMatches = (reg, content) => {
|
4
|
+
let inputs = [];
|
5
|
+
let m;
|
6
|
+
while ((m = reg.exec(content)) !== null) {
|
7
|
+
// This is necessary to avoid infinite loops with zero-width matches
|
8
|
+
if (m.index === reg.lastIndex) reg.lastIndex++;
|
9
|
+
|
10
|
+
// The result can be accessed through the `m`-variable.
|
11
|
+
inputs.push(m[1] || null);
|
12
|
+
}
|
13
|
+
return inputs;
|
14
|
+
}
|
15
|
+
|
16
|
+
const cleanStdout = (buffer, inputs) => {
|
17
|
+
if(Array.isArray(inputs))
|
18
|
+
for(let i = 0; i < inputs.length; i++)
|
19
|
+
if(inputs[i]) buffer = buffer.replace(inputs[i],'');
|
20
|
+
|
21
|
+
return buffer;
|
22
|
+
}
|
23
|
+
|
24
|
+
const indent = (string, count = 1, options) => {
|
25
|
+
options = {
|
26
|
+
indent: ' ',
|
27
|
+
includeEmptyLines: false,
|
28
|
+
...options
|
29
|
+
};
|
30
|
+
|
31
|
+
if (typeof string !== 'string') {
|
32
|
+
throw new TypeError(
|
33
|
+
`Expected \`input\` to be a \`string\`, got \`${typeof string}\``
|
34
|
+
);
|
35
|
+
}
|
36
|
+
|
37
|
+
if (typeof count !== 'number') {
|
38
|
+
throw new TypeError(
|
39
|
+
`Expected \`count\` to be a \`number\`, got \`${typeof count}\``
|
40
|
+
);
|
41
|
+
}
|
42
|
+
|
43
|
+
if (count < 0) {
|
44
|
+
throw new RangeError(
|
45
|
+
`Expected \`count\` to be at least 0, got \`${count}\``
|
46
|
+
);
|
47
|
+
}
|
48
|
+
|
49
|
+
if (typeof options.indent !== 'string') {
|
50
|
+
throw new TypeError(
|
51
|
+
`Expected \`options.indent\` to be a \`string\`, got \`${typeof options.indent}\``
|
52
|
+
);
|
53
|
+
}
|
54
|
+
|
55
|
+
if (count === 0) {
|
56
|
+
return string;
|
57
|
+
}
|
58
|
+
|
59
|
+
const regex = options.includeEmptyLines ? /^/gm : /^(?!\s*$)/gm;
|
60
|
+
|
61
|
+
return string.replace(regex, options.indent.repeat(count));
|
62
|
+
};
|
63
|
+
|
64
|
+
const Console = {
|
65
|
+
// _debug: true,
|
66
|
+
_debug: process.env.DEBUG == 'true',
|
67
|
+
startDebug: function(){ this._debug = true; },
|
68
|
+
log: (msg, ...args) => console.log(chalk.gray(msg), ...args),
|
69
|
+
error: (msg, ...args) => console.log(chalk.red('⨉ '+msg), ...args),
|
70
|
+
success: (msg, ...args) => console.log(chalk.green('✓ '+msg), ...args),
|
71
|
+
info: (msg, ...args) => console.log(chalk.blue('ⓘ '+msg), ...args),
|
72
|
+
help: (msg) => console.log(`${chalk.white.bold('⚠ help:')} ${chalk.white(msg)}`),
|
73
|
+
debug(...args){
|
74
|
+
this._debug && console.log(chalk.magentaBright(`⚠ debug: `), args)
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
module.exports = { getMatches, cleanStdout, indent, Console };
|
@@ -0,0 +1,243 @@
|
|
1
|
+
const fs = require('fs')
|
2
|
+
const fetch = require('node-fetch')
|
3
|
+
const {validateExerciseDirectoryName} = require('../managers/config/exercise.js')
|
4
|
+
const { flags } = require('@oclif/command')
|
5
|
+
const Console = require('../utils/console')
|
6
|
+
const { isUrl, findInFile, checkLearnpackClean, checkForEmptySpaces, showErrors, showWarnings } = require('../utils/audit')
|
7
|
+
const SessionCommand = require('../utils/SessionCommand');
|
8
|
+
const fm = require("front-matter")
|
9
|
+
const path = require('path')
|
10
|
+
|
11
|
+
class AuditCommand extends SessionCommand {
|
12
|
+
async init() {
|
13
|
+
const { flags } = this.parse(AuditCommand)
|
14
|
+
await this.initSession(flags)
|
15
|
+
}
|
16
|
+
async run() {
|
17
|
+
const { flags } = this.parse(AuditCommand)
|
18
|
+
|
19
|
+
Console.log("Running command audit...")
|
20
|
+
|
21
|
+
// Get configuration object.
|
22
|
+
const config = this.configManager.get();
|
23
|
+
|
24
|
+
let errors = []
|
25
|
+
let warnings = []
|
26
|
+
let counter = {
|
27
|
+
images: {
|
28
|
+
error: 0,
|
29
|
+
total: 0,
|
30
|
+
},
|
31
|
+
links: {
|
32
|
+
error: 0,
|
33
|
+
total: 0,
|
34
|
+
},
|
35
|
+
exercises: 0,
|
36
|
+
readmeFiles: 0
|
37
|
+
}
|
38
|
+
|
39
|
+
// Checks if learnpack clean has been run
|
40
|
+
checkLearnpackClean(config, errors)
|
41
|
+
|
42
|
+
// Build exercises if they are not built yet.
|
43
|
+
if (!this.configManager.get().exercises) this.configManager.buildIndex()
|
44
|
+
|
45
|
+
// Check if the exercises folder has some files within any ./exercise
|
46
|
+
const exercisesPath = config.config.exercisesPath
|
47
|
+
fs.readdir(exercisesPath, (err, files) => {
|
48
|
+
if (err) {
|
49
|
+
return console.log('Unable to scan directory: ' + err);
|
50
|
+
}
|
51
|
+
//listing all files using forEach
|
52
|
+
files.forEach(function (file) {
|
53
|
+
// Do whatever you want to do with the file
|
54
|
+
let filePath = path.join(exercisesPath, file)
|
55
|
+
if (fs.statSync(filePath).isFile()) warnings.push({ exercise: file, msg: `This file is not inside any exercise folder.` })
|
56
|
+
});
|
57
|
+
})
|
58
|
+
|
59
|
+
// This function checks that each of the url's are working.
|
60
|
+
const checkUrl = async (file, exercise) => {
|
61
|
+
if (!fs.existsSync(file.path)) return false
|
62
|
+
const content = fs.readFileSync(file.path).toString();
|
63
|
+
let isEmpty = checkForEmptySpaces(content);
|
64
|
+
if (isEmpty === true || content == false) errors.push({ exercise: exercise.title, msg: `This file (${file.name}) doesn't have any content inside.` })
|
65
|
+
const frontmatter = fm(content).attributes
|
66
|
+
for (const attribute in frontmatter) {
|
67
|
+
if (attribute === "intro" || attribute === "tutorial") {
|
68
|
+
counter.links.total++
|
69
|
+
try {
|
70
|
+
let res = await fetch(frontmatter[attribute], { method: "HEAD" });
|
71
|
+
if (!res.ok) {
|
72
|
+
counter.links.error++;
|
73
|
+
errors.push({ exercise: exercise.title, msg: `This link is broken (${res.ok}): ${frontmatter[attribute]}` })
|
74
|
+
}
|
75
|
+
}
|
76
|
+
catch (error) {
|
77
|
+
counter.links.error++;
|
78
|
+
errors.push({ exercise: exercise.title, msg: `This link is broken: ${frontmatter[attribute]}` })
|
79
|
+
}
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
// Check url's of each README file.
|
84
|
+
const findings = findInFile(["relative_images", "external_images", "markdown_links"], content);
|
85
|
+
for (const finding in findings) {
|
86
|
+
let obj = findings[finding];
|
87
|
+
// Valdites all the relative path images.
|
88
|
+
if (finding === "relative_images" && Object.keys(obj).length > 0) {
|
89
|
+
for (const img in obj) {
|
90
|
+
// Validates if the image is in the assets folder.
|
91
|
+
counter.images.total++
|
92
|
+
let relativePath = path.relative(exercise.path.replace(/\\/gm, "/"), `${config.config.dirPath}/assets/${obj[img].relUrl}`).replace(/\\/gm, "/")
|
93
|
+
if (relativePath != obj[img].absUrl.split("?").shift()) {
|
94
|
+
counter.images.error++;
|
95
|
+
errors.push({ exercise: exercise.title, msg: `This relative path (${obj[img].relUrl}) is not pointing to the assets folder.` })
|
96
|
+
}
|
97
|
+
if (!fs.existsSync(`${config.config.dirPath}/assets/${obj[img].relUrl}`)) {
|
98
|
+
counter.images.error++;
|
99
|
+
errors.push({ exercise: exercise.title, msg: `The file ${obj[img].relUrl} doesn't exist in the assets folder.` })
|
100
|
+
}
|
101
|
+
}
|
102
|
+
} else if (finding === "external_images" && Object.keys(obj).length > 0) {
|
103
|
+
// Valdites all the aboslute path images.
|
104
|
+
for (const img in obj) {
|
105
|
+
counter.images.total++
|
106
|
+
try {
|
107
|
+
let res = await fetch(obj[img].absUrl, { method: "HEAD" });
|
108
|
+
if (!res.ok) {
|
109
|
+
counter.images.error++;
|
110
|
+
errors.push({ exercise: exercise.title, msg: `This link is broken: ${obj[img].absUrl}` })
|
111
|
+
}
|
112
|
+
}
|
113
|
+
catch (error) {
|
114
|
+
counter.images.error++;
|
115
|
+
errors.push({ exercise: exercise.title, msg: `This link is broken: ${obj[img].absUrl}` })
|
116
|
+
}
|
117
|
+
}
|
118
|
+
} else if (finding === "markdown_links" && Object.keys(obj).length > 0) {
|
119
|
+
for (const link in obj) {
|
120
|
+
counter.links.total++
|
121
|
+
try {
|
122
|
+
let res = await fetch(obj[link].mdUrl, { method: "HEAD" });
|
123
|
+
if (res.status > 399 && res.status < 200) {
|
124
|
+
counter.links.error++;
|
125
|
+
errors.push({ exercise: exercise.title, msg: `This link is broken: ${obj[link].mdUrl}` })
|
126
|
+
}
|
127
|
+
}
|
128
|
+
catch (error) {
|
129
|
+
counter.links.error++;
|
130
|
+
errors.push({ exercise: exercise.title, msg: `This link is broken: ${obj[link].mdUrl}` })
|
131
|
+
}
|
132
|
+
}
|
133
|
+
}
|
134
|
+
}
|
135
|
+
return true
|
136
|
+
}
|
137
|
+
|
138
|
+
// This function is being created because the find method doesn't work with promises.
|
139
|
+
const find = async (file, lang, exercise) => {
|
140
|
+
if (file.name === lang) {
|
141
|
+
await checkUrl(file, exercise)
|
142
|
+
return true
|
143
|
+
}
|
144
|
+
return false
|
145
|
+
}
|
146
|
+
|
147
|
+
Console.info(' Checking if the config file is fine...')
|
148
|
+
// These two lines check if the 'slug' property is inside the configuration object.
|
149
|
+
Console.debug("Checking if the slug property is inside the configuration object...")
|
150
|
+
if (!config.config.slug) errors.push({ exercise: null, msg: "The slug property is not in the configuration object" })
|
151
|
+
|
152
|
+
// These two lines check if the 'repository' property is inside the configuration object.
|
153
|
+
Console.debug("Checking if the repository property is inside the configuration object...")
|
154
|
+
if (!config.config.repository) errors.push({ exercise: null, msg: "The repository property is not in the configuration object" })
|
155
|
+
else isUrl(config.config.repository, errors, counter)
|
156
|
+
|
157
|
+
// These two lines check if the 'description' property is inside the configuration object.
|
158
|
+
Console.debug("Checking if the description property is inside the configuration object...")
|
159
|
+
if (!config.config.description) errors.push({ exercise: null, msg: "The description property is not in the configuration object" })
|
160
|
+
|
161
|
+
if (errors.length == 0) Console.log("The config file is ok")
|
162
|
+
|
163
|
+
// Validates if images and links are working at every README file.
|
164
|
+
const exercises = config.exercises
|
165
|
+
let readmeFiles = []
|
166
|
+
|
167
|
+
if (exercises.length > 0) {
|
168
|
+
Console.info(' Checking if the images are working...')
|
169
|
+
for (const index in exercises) {
|
170
|
+
let exercise = exercises[index]
|
171
|
+
if (!validateExerciseDirectoryName(exercise.title)) errors.push({exercise: exercise.title, msg: `The exercise ${exercise.title} has an invalid name.`})
|
172
|
+
let readmeFilesCount = { exercise: exercise.title, count: 0 };
|
173
|
+
if (Object.keys(exercise.translations).length == 0) errors.push({ exercise: exercise.title, msg: `The exercise ${exercise.title} doesn't have a README.md file.` })
|
174
|
+
|
175
|
+
if (exercise.language == "python3" || exercise.language == "python") {
|
176
|
+
exercise.files.map(f => f).find(f => {
|
177
|
+
if (f.path.includes('test.py') || f.path.includes('tests.py')) {
|
178
|
+
const content = fs.readFileSync(f.path).toString();
|
179
|
+
let isEmpty = checkForEmptySpaces(content);
|
180
|
+
if (isEmpty === true || content == false) errors.push({ exercise: exercise.title, msg: `This file (${f.name}) doesn't have any content inside.` })
|
181
|
+
}
|
182
|
+
});
|
183
|
+
}
|
184
|
+
else {
|
185
|
+
exercise.files.map(f => f).find(f => {
|
186
|
+
if (f.path.includes('test.js') || f.path.includes('tests.js')) {
|
187
|
+
const content = fs.readFileSync(f.path).toString();
|
188
|
+
let isEmpty = checkForEmptySpaces(content);
|
189
|
+
if (isEmpty === true || content == false) errors.push({ exercise: exercise.title, msg: `This file (${f.name}) doesn't have any content inside.` })
|
190
|
+
}
|
191
|
+
});
|
192
|
+
}
|
193
|
+
|
194
|
+
for (const lang in exercise.translations) {
|
195
|
+
let files = []
|
196
|
+
for (const file of exercise.files) {
|
197
|
+
let found = await find(file, exercise.translations[lang], exercise)
|
198
|
+
if (found == true) readmeFilesCount = { ...readmeFilesCount, count: readmeFilesCount.count + 1 }
|
199
|
+
files.push(found)
|
200
|
+
}
|
201
|
+
if (!files.includes(true)) errors.push({ exercise: exercise.title, msg: `This exercise doesn't have a README.md file.` })
|
202
|
+
|
203
|
+
}
|
204
|
+
readmeFiles.push(readmeFilesCount)
|
205
|
+
}
|
206
|
+
} else errors.push({ exercise: null, msg: "The exercises array is empty." })
|
207
|
+
|
208
|
+
Console.log(`${counter.images.total - counter.images.error} images ok from ${counter.images.total}`)
|
209
|
+
|
210
|
+
Console.info(" Checking if important files are missing... (README's, translations, gitignore...)")
|
211
|
+
// Check if all the exercises has the same ammount of README's, this way we can check if they have the same ammount of translations.
|
212
|
+
let files = [];
|
213
|
+
readmeFiles.map((item, i, arr) => {
|
214
|
+
if (item.count !== arr[0].count) files.push(` ${item.exercise}`)
|
215
|
+
})
|
216
|
+
if (files.length > 0) {
|
217
|
+
files = files.join()
|
218
|
+
warnings.push({ exercise: null, msg: `These exercises are missing translations: ${files}` })
|
219
|
+
}
|
220
|
+
|
221
|
+
// Checks if the .gitignore file exists.
|
222
|
+
if (!fs.existsSync(`.gitignore`)) warnings.push(".gitignore file doesn't exist")
|
223
|
+
|
224
|
+
counter.exercises = exercises.length;
|
225
|
+
readmeFiles.forEach((readme) => {
|
226
|
+
counter.readmeFiles += readme.count
|
227
|
+
})
|
228
|
+
|
229
|
+
await showWarnings(warnings)
|
230
|
+
await showErrors(errors, counter)
|
231
|
+
}
|
232
|
+
}
|
233
|
+
|
234
|
+
AuditCommand.description = `Check if the configuration object has slug, description and repository property
|
235
|
+
...
|
236
|
+
Extra documentation goes here
|
237
|
+
`
|
238
|
+
|
239
|
+
AuditCommand.flags = {
|
240
|
+
// name: flags.string({char: 'n', description: 'name to print'}),
|
241
|
+
}
|
242
|
+
|
243
|
+
module.exports = AuditCommand
|
@@ -0,0 +1,27 @@
|
|
1
|
+
const {flags} = require('@oclif/command')
|
2
|
+
const Console = require('../utils/console')
|
3
|
+
const SessionCommand = require('../utils/SessionCommand')
|
4
|
+
class CleanCommand extends SessionCommand {
|
5
|
+
async init() {
|
6
|
+
const {flags} = this.parse(CleanCommand)
|
7
|
+
await this.initSession(flags)
|
8
|
+
}
|
9
|
+
async run() {
|
10
|
+
const {flags} = this.parse(CleanCommand)
|
11
|
+
|
12
|
+
this.configManager.clean()
|
13
|
+
|
14
|
+
Console.success("Package cleaned successfully, ready to publish")
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
CleanCommand.description = `Clean the configuration object
|
19
|
+
...
|
20
|
+
Extra documentation goes here
|
21
|
+
`
|
22
|
+
|
23
|
+
CleanCommand.flags = {
|
24
|
+
// name: flags.string({char: 'n', description: 'name to print'}),
|
25
|
+
}
|
26
|
+
|
27
|
+
module.exports = CleanCommand
|
@@ -0,0 +1,52 @@
|
|
1
|
+
const {Command, flags} = require('@oclif/command')
|
2
|
+
const fetch = require('node-fetch');
|
3
|
+
const { clone } = require('../managers/file.js')
|
4
|
+
const Console = require('../utils/console')
|
5
|
+
const api = require('../utils/api')
|
6
|
+
const getRepoInfo = require('git-repo-info')
|
7
|
+
const { askPackage } = require('../ui/download')
|
8
|
+
|
9
|
+
class DownloadCommand extends Command {
|
10
|
+
// async init() {
|
11
|
+
// const {flags} = this.parse(DownloadCommand)
|
12
|
+
// await this.initSession(flags)
|
13
|
+
// }
|
14
|
+
async run() {
|
15
|
+
const {flags, args} = this.parse(DownloadCommand)
|
16
|
+
// start watching for file changes
|
17
|
+
let _package = args.package
|
18
|
+
if(!_package) _package = await askPackage()
|
19
|
+
|
20
|
+
if(!_package) return null
|
21
|
+
|
22
|
+
try{
|
23
|
+
_package = _package.pack;
|
24
|
+
clone(_package.repository)
|
25
|
+
.then(result => {
|
26
|
+
Console.success(`Successfully downloaded`)
|
27
|
+
const folder = _package.repository.substring(_package.repository.lastIndexOf('/') + 1).split(".")[0];
|
28
|
+
Console.info(`You can now CD into the folder like this: $ cd ${folder}`)
|
29
|
+
})
|
30
|
+
.catch(error => Console.error(error.message || error))
|
31
|
+
}
|
32
|
+
catch(error){}
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
DownloadCommand.description = `Describe the command here
|
37
|
+
...
|
38
|
+
Extra documentation goes here
|
39
|
+
`
|
40
|
+
DownloadCommand.flags = {
|
41
|
+
// name: flags.string({char: 'n', description: 'name to print'}),
|
42
|
+
}
|
43
|
+
DownloadCommand.args =[
|
44
|
+
{
|
45
|
+
name: 'package', // name of arg to show in help and reference with args[name]
|
46
|
+
required: false, // make the arg required with `required: true`
|
47
|
+
description: 'The unique string that identifies this package on learnpack', // help description
|
48
|
+
hidden: false // hide this arg from help
|
49
|
+
}
|
50
|
+
]
|
51
|
+
|
52
|
+
module.exports = DownloadCommand
|
@@ -0,0 +1,20 @@
|
|
1
|
+
const {Command, flags} = require('@oclif/command')
|
2
|
+
|
3
|
+
class HelloCommand extends Command {
|
4
|
+
async run() {
|
5
|
+
const {flags} = this.parse(HelloCommand)
|
6
|
+
const name = flags.name || 'world'
|
7
|
+
this.log(`hello ${name} from ./src/commands/hello.js`)
|
8
|
+
}
|
9
|
+
}
|
10
|
+
|
11
|
+
HelloCommand.description = `Describe the command here
|
12
|
+
...
|
13
|
+
Extra documentation goes here
|
14
|
+
`
|
15
|
+
|
16
|
+
HelloCommand.flags = {
|
17
|
+
name: flags.string({char: 'n', description: 'name to print'}),
|
18
|
+
}
|
19
|
+
|
20
|
+
module.exports = HelloCommand
|