@learnpack/learnpack 2.1.14 → 2.1.19
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 +10 -10
- package/lib/commands/audit.js +380 -273
- package/lib/managers/config/defaults.d.ts +1 -0
- package/lib/managers/config/defaults.js +1 -0
- package/lib/managers/config/index.js +1 -1
- package/lib/models/audit.d.ts +15 -0
- package/lib/models/{audit-errors.js → audit.js} +0 -0
- package/lib/models/config.d.ts +1 -0
- package/lib/utils/audit.d.ts +5 -5
- package/lib/utils/audit.js +23 -13
- package/lib/utils/watcher.d.ts +1 -2
- package/lib/utils/watcher.js +3 -9
- package/oclif.manifest.json +1 -1
- package/package.json +3 -2
- package/src/commands/audit.ts +484 -338
- package/src/managers/config/defaults.ts +1 -0
- package/src/managers/config/index.ts +1 -1
- package/src/models/audit.ts +16 -0
- package/src/models/config.ts +1 -0
- package/src/utils/audit.ts +181 -162
- package/src/utils/watcher.ts +3 -15
- package/lib/models/audit-errors.d.ts +0 -4
- package/src/models/audit-errors.ts +0 -4
@@ -14,6 +14,7 @@ export default {
|
|
14
14
|
contact: "https://github.com/learnpack/learnpack/issues/new",
|
15
15
|
language: "auto",
|
16
16
|
autoPlay: true,
|
17
|
+
projectType: "tutorial", // [tutorial, project]
|
17
18
|
grading: "isolated", // [isolated, incremental]
|
18
19
|
exercisesPath: "./", // path to the folder that contains the exercises
|
19
20
|
webpackTemplate: null, // if you want webpack to use an HTML template
|
@@ -350,7 +350,7 @@ fs.mkdirSync(confPath.base);
|
|
350
350
|
);
|
351
351
|
|
352
352
|
this.buildIndex();
|
353
|
-
watch(configObj?.config?.exercisesPath || "",
|
353
|
+
watch(configObj?.config?.exercisesPath || "", onChange)
|
354
354
|
.then((/* eventname, filename */) => {
|
355
355
|
Console.debug("Changes detected on your exercises");
|
356
356
|
this.buildIndex();
|
@@ -0,0 +1,16 @@
|
|
1
|
+
export interface IAuditErrors {
|
2
|
+
exercise?: string;
|
3
|
+
msg: string;
|
4
|
+
}
|
5
|
+
|
6
|
+
type TType = "string" | "array" | "number" | "url" | "boolean";
|
7
|
+
|
8
|
+
export interface ISchemaItem {
|
9
|
+
key: string;
|
10
|
+
mandatory: boolean;
|
11
|
+
type: TType;
|
12
|
+
max_size?: number;
|
13
|
+
allowed_extensions?: string[];
|
14
|
+
enum?: string[];
|
15
|
+
max_item_size?: number;
|
16
|
+
}
|
package/src/models/config.ts
CHANGED
@@ -55,6 +55,7 @@ export interface IConfig {
|
|
55
55
|
disableGrading: boolean; // TODO: Deprecate
|
56
56
|
actions: Array<string>; // TODO: Deprecate
|
57
57
|
autoPlay: boolean;
|
58
|
+
projectType?: string;
|
58
59
|
// TODO: nameExerciseValidation
|
59
60
|
contact?: string;
|
60
61
|
disabledActions?: Array<TConfigAction>;
|
package/src/utils/audit.ts
CHANGED
@@ -1,162 +1,181 @@
|
|
1
|
-
import {IAuditErrors} from
|
2
|
-
import {IConfigObj} from
|
3
|
-
import {ICounter} from
|
4
|
-
import {IFindings} from
|
5
|
-
import Console from
|
6
|
-
|
7
|
-
// eslint-disable-next-line
|
8
|
-
const fetch = require("node-fetch");
|
9
|
-
import * as fs from
|
10
|
-
|
11
|
-
export default {
|
12
|
-
// This function checks if a url is valid.
|
13
|
-
isUrl: async (url: string, errors: IAuditErrors[], counter: ICounter) => {
|
14
|
-
const regexUrl = /(https?:\/\/[\w./-]+)/gm
|
15
|
-
counter.links.total
|
16
|
-
if (!regexUrl.test(url)) {
|
17
|
-
counter.links.error
|
18
|
-
errors.push({
|
19
|
-
exercise: undefined,
|
20
|
-
msg: `The repository value of the configuration file is not a link: ${url}`,
|
21
|
-
})
|
22
|
-
return false
|
23
|
-
}
|
24
|
-
|
25
|
-
const res = await fetch(url, {method:
|
26
|
-
if (!res.ok) {
|
27
|
-
counter.links.error
|
28
|
-
errors.push({
|
29
|
-
exercise: undefined,
|
30
|
-
msg: `The link of the repository is broken: ${url}`,
|
31
|
-
})
|
32
|
-
}
|
33
|
-
|
34
|
-
return true
|
35
|
-
},
|
36
|
-
checkForEmptySpaces: (str: string) => {
|
37
|
-
const isEmpty = true
|
38
|
-
for (const letter of str) {
|
39
|
-
if (letter !==
|
40
|
-
return false
|
41
|
-
}
|
42
|
-
}
|
43
|
-
|
44
|
-
return isEmpty
|
45
|
-
},
|
46
|
-
checkLearnpackClean: (configObj: IConfigObj, errors: IAuditErrors[]) => {
|
47
|
-
if (
|
48
|
-
(configObj.config?.outputPath &&
|
49
|
-
fs.existsSync(configObj.config?.outputPath)) ||
|
50
|
-
fs.existsSync(`${configObj.config?.dirPath}/_app`) ||
|
51
|
-
fs.existsSync(`${configObj.config?.dirPath}/reports`) ||
|
52
|
-
fs.existsSync(`${configObj.config?.dirPath}/resets`) ||
|
53
|
-
fs.existsSync(`${configObj.config?.dirPath}/app.tar.gz`) ||
|
54
|
-
fs.existsSync(`${configObj.config?.dirPath}/config.json`) ||
|
55
|
-
fs.existsSync(`${configObj.config?.dirPath}/vscode_queue.json`)
|
56
|
-
) {
|
57
|
-
errors.push({
|
58
|
-
exercise: undefined,
|
59
|
-
msg:
|
60
|
-
})
|
61
|
-
}
|
62
|
-
},
|
63
|
-
findInFile: (types: string[], content: string) => {
|
64
|
-
const regex: any = {
|
65
|
-
relativeImages:
|
66
|
-
/!\[.*]\s*\((((\.\/)?(\.{2}\/){1,5})(.*\/)*(.[^\s/]*\.[A-Za-z]{2,4})\S*)\)/gm,
|
67
|
-
externalImages: /!\[.*]\((https?:\/(\/[^)/]+)+\/?)\)/gm,
|
68
|
-
markdownLinks: /(\s)+\[.*]\((https?:\/(\/[^)/]+)+\/?)\)/gm,
|
69
|
-
url: /(https?:\/\/[\w./-]+)/gm,
|
70
|
-
uploadcare: /https:\/\/ucarecdn.com\/(?:.*\/)*([\w./-]+)/gm,
|
71
|
-
}
|
72
|
-
|
73
|
-
const validTypes = Object.keys(regex)
|
74
|
-
if (!Array.isArray(types))
|
75
|
-
|
76
|
-
|
77
|
-
const findings: IFindings = {}
|
78
|
-
type findingsType =
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
85
|
-
for (const type of types) {
|
86
|
-
if (!validTypes.includes(type))
|
87
|
-
|
88
|
-
else
|
89
|
-
|
90
|
-
}
|
91
|
-
|
92
|
-
for (const type of types) {
|
93
|
-
let m: RegExpExecArray
|
94
|
-
while ((m = regex[type].exec(content)) !== null) {
|
95
|
-
// This is necessary to avoid infinite loops with zero-width matches
|
96
|
-
if (m.index === regex.lastIndex) {
|
97
|
-
regex.lastIndex
|
98
|
-
}
|
99
|
-
|
100
|
-
// The result can be accessed through the `m`-variable.
|
101
|
-
// m.forEach((match, groupIndex) => values.push(match));
|
102
|
-
|
103
|
-
findings[type as findingsType]![m[0]] = {
|
104
|
-
content: m[0],
|
105
|
-
absUrl: m[1],
|
106
|
-
mdUrl: m[2],
|
107
|
-
relUrl: m[6],
|
108
|
-
}
|
109
|
-
}
|
110
|
-
}
|
111
|
-
|
112
|
-
return findings
|
113
|
-
},
|
114
|
-
// This function checks if there are errors, and show them in the console at the end.
|
115
|
-
showErrors: (errors: IAuditErrors[], counter: ICounter) => {
|
116
|
-
return new Promise((resolve, reject) => {
|
117
|
-
if (errors) {
|
118
|
-
if (errors.length > 0) {
|
119
|
-
Console.log(
|
120
|
-
for (const [i, error] of errors.entries())
|
121
|
-
Console.error(
|
122
|
-
`${i + 1}) ${error.msg} ${
|
123
|
-
error.exercise ? `(Exercise: ${error.exercise})` :
|
124
|
-
}
|
125
|
-
)
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
1
|
+
import { IAuditErrors } from "../models/audit";
|
2
|
+
import { IConfigObj } from "../models/config";
|
3
|
+
import { ICounter } from "../models/counter";
|
4
|
+
import { IFindings } from "../models/findings";
|
5
|
+
import Console from "./console";
|
6
|
+
|
7
|
+
// eslint-disable-next-line
|
8
|
+
const fetch = require("node-fetch");
|
9
|
+
import * as fs from "fs";
|
10
|
+
|
11
|
+
export default {
|
12
|
+
// This function checks if a url is valid.
|
13
|
+
isUrl: async (url: string, errors: IAuditErrors[], counter: ICounter) => {
|
14
|
+
const regexUrl = /(https?:\/\/[\w./-]+)/gm;
|
15
|
+
counter.links.total++;
|
16
|
+
if (!regexUrl.test(url)) {
|
17
|
+
counter.links.error++;
|
18
|
+
errors.push({
|
19
|
+
exercise: undefined,
|
20
|
+
msg: `The repository value of the configuration file is not a link: ${url}`,
|
21
|
+
});
|
22
|
+
return false;
|
23
|
+
}
|
24
|
+
|
25
|
+
const res = await fetch(url, { method: "HEAD" });
|
26
|
+
if (!res.ok) {
|
27
|
+
counter.links.error++;
|
28
|
+
errors.push({
|
29
|
+
exercise: undefined,
|
30
|
+
msg: `The link of the repository is broken: ${url}`,
|
31
|
+
});
|
32
|
+
}
|
33
|
+
|
34
|
+
return true;
|
35
|
+
},
|
36
|
+
checkForEmptySpaces: (str: string) => {
|
37
|
+
const isEmpty = true;
|
38
|
+
for (const letter of str) {
|
39
|
+
if (letter !== " ") {
|
40
|
+
return false;
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
return isEmpty;
|
45
|
+
},
|
46
|
+
checkLearnpackClean: (configObj: IConfigObj, errors: IAuditErrors[]) => {
|
47
|
+
if (
|
48
|
+
(configObj.config?.outputPath &&
|
49
|
+
fs.existsSync(configObj.config?.outputPath)) ||
|
50
|
+
fs.existsSync(`${configObj.config?.dirPath}/_app`) ||
|
51
|
+
fs.existsSync(`${configObj.config?.dirPath}/reports`) ||
|
52
|
+
fs.existsSync(`${configObj.config?.dirPath}/resets`) ||
|
53
|
+
fs.existsSync(`${configObj.config?.dirPath}/app.tar.gz`) ||
|
54
|
+
fs.existsSync(`${configObj.config?.dirPath}/config.json`) ||
|
55
|
+
fs.existsSync(`${configObj.config?.dirPath}/vscode_queue.json`)
|
56
|
+
) {
|
57
|
+
errors.push({
|
58
|
+
exercise: undefined,
|
59
|
+
msg: "You have to run learnpack clean command",
|
60
|
+
});
|
61
|
+
}
|
62
|
+
},
|
63
|
+
findInFile: (types: string[], content: string) => {
|
64
|
+
const regex: any = {
|
65
|
+
relativeImages:
|
66
|
+
/!\[.*]\s*\((((\.\/)?(\.{2}\/){1,5})(.*\/)*(.[^\s/]*\.[A-Za-z]{2,4})\S*)\)/gm,
|
67
|
+
externalImages: /!\[.*]\((https?:\/(\/[^)/]+)+\/?)\)/gm,
|
68
|
+
markdownLinks: /(\s)+\[.*]\((https?:\/(\/[^)/]+)+\/?)\)/gm,
|
69
|
+
url: /(https?:\/\/[\w./-]+)/gm,
|
70
|
+
uploadcare: /https:\/\/ucarecdn.com\/(?:.*\/)*([\w./-]+)/gm,
|
71
|
+
};
|
72
|
+
|
73
|
+
const validTypes = Object.keys(regex);
|
74
|
+
if (!Array.isArray(types))
|
75
|
+
types = [types];
|
76
|
+
|
77
|
+
const findings: IFindings = {};
|
78
|
+
type findingsType =
|
79
|
+
| "relativeImages"
|
80
|
+
| "externalImages"
|
81
|
+
| "markdownLinks"
|
82
|
+
| "url"
|
83
|
+
| "uploadcare";
|
84
|
+
|
85
|
+
for (const type of types) {
|
86
|
+
if (!validTypes.includes(type))
|
87
|
+
throw new Error("Invalid type: " + type);
|
88
|
+
else
|
89
|
+
findings[type as findingsType] = {};
|
90
|
+
}
|
91
|
+
|
92
|
+
for (const type of types) {
|
93
|
+
let m: RegExpExecArray;
|
94
|
+
while ((m = regex[type].exec(content)) !== null) {
|
95
|
+
// This is necessary to avoid infinite loops with zero-width matches
|
96
|
+
if (m.index === regex.lastIndex) {
|
97
|
+
regex.lastIndex++;
|
98
|
+
}
|
99
|
+
|
100
|
+
// The result can be accessed through the `m`-variable.
|
101
|
+
// m.forEach((match, groupIndex) => values.push(match));
|
102
|
+
|
103
|
+
findings[type as findingsType]![m[0]] = {
|
104
|
+
content: m[0],
|
105
|
+
absUrl: m[1],
|
106
|
+
mdUrl: m[2],
|
107
|
+
relUrl: m[6],
|
108
|
+
};
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
return findings;
|
113
|
+
},
|
114
|
+
// This function checks if there are errors, and show them in the console at the end.
|
115
|
+
showErrors: (errors: IAuditErrors[], counter: ICounter | undefined) => {
|
116
|
+
return new Promise((resolve, reject) => {
|
117
|
+
if (errors) {
|
118
|
+
if (errors.length > 0) {
|
119
|
+
Console.log("Checking for errors...");
|
120
|
+
for (const [i, error] of errors.entries())
|
121
|
+
Console.error(
|
122
|
+
`${i + 1}) ${error.msg} ${
|
123
|
+
error.exercise ? `(Exercise: ${error.exercise})` : ""
|
124
|
+
}`
|
125
|
+
);
|
126
|
+
if (counter) {
|
127
|
+
Console.error(
|
128
|
+
` We found ${errors.length} error${
|
129
|
+
errors.length > 1 ? "s" : ""
|
130
|
+
} among ${counter.images.total} images, ${
|
131
|
+
counter.links.total
|
132
|
+
} link, ${counter.readmeFiles} README files and ${
|
133
|
+
counter.exercises
|
134
|
+
} exercises.`
|
135
|
+
);
|
136
|
+
} else {
|
137
|
+
Console.error(
|
138
|
+
` We found ${errors.length} error${
|
139
|
+
errors.length > 1 ? "s" : ""
|
140
|
+
} related with the project integrity.`
|
141
|
+
);
|
142
|
+
}
|
143
|
+
|
144
|
+
process.exit(1);
|
145
|
+
} else {
|
146
|
+
if (counter) {
|
147
|
+
Console.success(
|
148
|
+
`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.`
|
149
|
+
);
|
150
|
+
} else {
|
151
|
+
Console.success(`We didn't find any errors in this repository.`);
|
152
|
+
}
|
153
|
+
|
154
|
+
process.exit(0);
|
155
|
+
}
|
156
|
+
} else {
|
157
|
+
reject("Failed");
|
158
|
+
}
|
159
|
+
});
|
160
|
+
},
|
161
|
+
// This function checks if there are warnings, and show them in the console at the end.
|
162
|
+
showWarnings: (warnings: IAuditErrors[]) => {
|
163
|
+
return new Promise((resolve, reject) => {
|
164
|
+
if (warnings) {
|
165
|
+
if (warnings.length > 0) {
|
166
|
+
Console.log("Checking for warnings...");
|
167
|
+
for (const [i, warning] of warnings.entries())
|
168
|
+
Console.warning(
|
169
|
+
`${i + 1}) ${warning.msg} ${
|
170
|
+
warning.exercise ? `File: ${warning.exercise}` : ""
|
171
|
+
}`
|
172
|
+
);
|
173
|
+
}
|
174
|
+
|
175
|
+
resolve("SUCCESS");
|
176
|
+
} else {
|
177
|
+
reject("Failed");
|
178
|
+
}
|
179
|
+
});
|
180
|
+
},
|
181
|
+
};
|
package/src/utils/watcher.ts
CHANGED
@@ -3,33 +3,21 @@ import Console from "./console";
|
|
3
3
|
import * as debounce from "debounce";
|
4
4
|
import { IConfigManager } from "../models/config-manager";
|
5
5
|
|
6
|
-
export default (
|
7
|
-
path: string,
|
8
|
-
configManager: IConfigManager,
|
9
|
-
reloadSocket: () => void
|
10
|
-
) =>
|
6
|
+
export default (path: string, reloadSocket: () => void) =>
|
11
7
|
new Promise((resolve /* , reject */) => {
|
12
8
|
Console.debug("PATH:", path);
|
13
9
|
const watcher = chokidar.watch(path, {
|
14
10
|
// TODO: This watcher is not ready yet
|
11
|
+
ignored: /^(?=.*(\.\w+)$)(?!.*\.md$).*$/gm,
|
15
12
|
ignoreInitial: true,
|
16
13
|
});
|
17
14
|
const onChange = (eventname: string, _filename: string) => {
|
18
15
|
resolve(eventname /* , filename */);
|
19
|
-
|
20
|
-
if (
|
21
|
-
/^(?=.*(\\\w+)$).*$/gm.test(_filename) === false ||
|
22
|
-
/^(?=.*(\.\w+)$)(?!.*\.md$).*$/gm.test(_filename) === false
|
23
|
-
) {
|
24
|
-
configManager.buildIndex();
|
25
|
-
} else {
|
26
|
-
reloadSocket();
|
27
|
-
}
|
16
|
+
reloadSocket();
|
28
17
|
};
|
29
18
|
|
30
19
|
watcher.on("all", debounce(onChange, 500, true));
|
31
20
|
// watcher.on('all', onChange)
|
32
|
-
|
33
21
|
process.on("SIGINT", function () {
|
34
22
|
watcher.close();
|
35
23
|
process.exit();
|