@learnpack/learnpack 1.0.0 → 2.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 +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
@@ -0,0 +1,87 @@
|
|
1
|
+
import * as chalk from "chalk";
|
2
|
+
|
3
|
+
const getMatches = (reg: RegExp, content: string) => {
|
4
|
+
const 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)
|
9
|
+
reg.lastIndex++;
|
10
|
+
|
11
|
+
// The result can be accessed through the `m`-variable.
|
12
|
+
inputs.push(m[1] || null);
|
13
|
+
}
|
14
|
+
|
15
|
+
return inputs;
|
16
|
+
};
|
17
|
+
|
18
|
+
const cleanStdout = (buffer: string, inputs: string[]) => {
|
19
|
+
if (Array.isArray(inputs))
|
20
|
+
for (let i = 0; i < inputs.length; i++)
|
21
|
+
if (inputs[i])
|
22
|
+
buffer = buffer.replace(inputs[i], "");
|
23
|
+
|
24
|
+
return buffer;
|
25
|
+
};
|
26
|
+
|
27
|
+
const indent = (string: string, options: any, count = 1) => {
|
28
|
+
options = {
|
29
|
+
indent: " ",
|
30
|
+
includeEmptyLines: false,
|
31
|
+
...options,
|
32
|
+
};
|
33
|
+
|
34
|
+
if (typeof string !== "string") {
|
35
|
+
throw new TypeError(
|
36
|
+
`Expected \`input\` to be a \`string\`, got \`${typeof string}\``
|
37
|
+
);
|
38
|
+
}
|
39
|
+
|
40
|
+
if (typeof count !== "number") {
|
41
|
+
throw new TypeError(
|
42
|
+
`Expected \`count\` to be a \`number\`, got \`${typeof count}\``
|
43
|
+
);
|
44
|
+
}
|
45
|
+
|
46
|
+
if (count < 0) {
|
47
|
+
throw new RangeError(
|
48
|
+
`Expected \`count\` to be at least 0, got \`${count}\``
|
49
|
+
);
|
50
|
+
}
|
51
|
+
|
52
|
+
if (typeof options.indent !== "string") {
|
53
|
+
throw new TypeError(
|
54
|
+
`Expected \`options.indent\` to be a \`string\`, got \`${typeof options.indent}\``
|
55
|
+
);
|
56
|
+
}
|
57
|
+
|
58
|
+
if (count === 0) {
|
59
|
+
return string;
|
60
|
+
}
|
61
|
+
|
62
|
+
const regex = options.includeEmptyLines ? /^/gm : /^(?!\s*$)/gm;
|
63
|
+
|
64
|
+
return string.replace(regex, options.indent.repeat(count));
|
65
|
+
};
|
66
|
+
|
67
|
+
const Console = {
|
68
|
+
// _debug: true,
|
69
|
+
_debug: process.env.DEBUG === "true",
|
70
|
+
startDebug: function () {
|
71
|
+
this._debug = true;
|
72
|
+
},
|
73
|
+
log: (msg: string, ...args: any[]) => console.log(chalk.gray(msg), ...args),
|
74
|
+
error: (msg: string, ...args: any[]) =>
|
75
|
+
console.log(chalk.red("⨉ " + msg), ...args),
|
76
|
+
success: (msg: string, ...args: any[]) =>
|
77
|
+
console.log(chalk.green("✓ " + msg), ...args),
|
78
|
+
info: (msg: string, ...args: any[]) =>
|
79
|
+
console.log(chalk.blue("ⓘ " + msg), ...args),
|
80
|
+
help: (msg: string) =>
|
81
|
+
console.log(`${chalk.white.bold("⚠ help:")} ${chalk.white(msg)}`),
|
82
|
+
debug(...args: any[]) {
|
83
|
+
this._debug && console.log(chalk.magentaBright(`⚠ debug: `), args);
|
84
|
+
},
|
85
|
+
};
|
86
|
+
|
87
|
+
export default { getMatches, cleanStdout, indent, Console };
|
@@ -0,0 +1 @@
|
|
1
|
+
declare module '*';
|
@@ -0,0 +1,71 @@
|
|
1
|
+
import {prompt} from 'enquirer'
|
2
|
+
import Console from '../utils/console'
|
3
|
+
import api from '../utils/api'
|
4
|
+
// import fetch from 'node-fetch'
|
5
|
+
|
6
|
+
import {ILanguage} from '../models/language'
|
7
|
+
import {IPackage} from '../models/package'
|
8
|
+
|
9
|
+
export const askPackage = async () => {
|
10
|
+
Console.info('No package was specified')
|
11
|
+
const languages = await api.getLangs()
|
12
|
+
|
13
|
+
return new Promise((resolve, reject) => {
|
14
|
+
if (languages.length === 0) {
|
15
|
+
// reject(new Error('No categories available'))
|
16
|
+
reject('No categories available')
|
17
|
+
// return null;
|
18
|
+
}
|
19
|
+
|
20
|
+
// let packages = []
|
21
|
+
prompt([
|
22
|
+
{
|
23
|
+
type: 'select',
|
24
|
+
name: 'lang',
|
25
|
+
message: 'What language do you want to practice?',
|
26
|
+
choices: languages.map((l: ILanguage) => ({
|
27
|
+
message: l.title,
|
28
|
+
name: l.slug,
|
29
|
+
})),
|
30
|
+
},
|
31
|
+
])
|
32
|
+
.then(({lang}: any) => {
|
33
|
+
return (async () => {
|
34
|
+
const response = await api.getAllPackages({lang})
|
35
|
+
const packages = response.results
|
36
|
+
if (packages.length === 0) {
|
37
|
+
const error = new Error(`No packages found for language ${lang}`)
|
38
|
+
Console.error(error.message) // TODO: Look this
|
39
|
+
return error
|
40
|
+
}
|
41
|
+
|
42
|
+
return prompt([
|
43
|
+
{
|
44
|
+
type: 'select',
|
45
|
+
name: 'pack',
|
46
|
+
message: 'Choose one of the packages available',
|
47
|
+
choices: packages.map((l: IPackage) => ({
|
48
|
+
message: `${l.title}, difficulty: ${l.difficulty}, downloads: ${
|
49
|
+
l.downloads
|
50
|
+
} ${
|
51
|
+
l.skills.length > 0 ? `(Skills: ${l.skills.join(',')})` : ''
|
52
|
+
}`,
|
53
|
+
name: l.slug,
|
54
|
+
})),
|
55
|
+
},
|
56
|
+
])
|
57
|
+
})()
|
58
|
+
})
|
59
|
+
.then((resp: any) => {
|
60
|
+
if (!resp)
|
61
|
+
reject(resp.message || resp)
|
62
|
+
else
|
63
|
+
resolve(resp.pack)
|
64
|
+
})
|
65
|
+
.catch(error => {
|
66
|
+
Console.error(error.message || error)
|
67
|
+
})
|
68
|
+
})
|
69
|
+
}
|
70
|
+
|
71
|
+
export default {askPackage}
|
@@ -0,0 +1,48 @@
|
|
1
|
+
import { Command } from "@oclif/command";
|
2
|
+
import Console from "./console";
|
3
|
+
import { createInterface } from "readline";
|
4
|
+
// import SessionManager from '../managers/session'
|
5
|
+
|
6
|
+
class BaseCommand extends Command {
|
7
|
+
async catch(err: any) {
|
8
|
+
Console.debug("COMMAND CATCH", err);
|
9
|
+
|
10
|
+
throw err;
|
11
|
+
}
|
12
|
+
|
13
|
+
async init() {
|
14
|
+
const { flags, args } = this.parse(BaseCommand);
|
15
|
+
Console.debug("COMMAND INIT");
|
16
|
+
Console.debug("These are your flags: ", flags);
|
17
|
+
Console.debug("These are your args: ", args);
|
18
|
+
|
19
|
+
// quick fix for listening to the process termination on windows
|
20
|
+
if (process.platform === "win32") {
|
21
|
+
const rl = createInterface({
|
22
|
+
input: process.stdin,
|
23
|
+
output: process.stdout,
|
24
|
+
});
|
25
|
+
|
26
|
+
rl.on("SIGINT", function () {
|
27
|
+
// process.emit('SIGINT')
|
28
|
+
// process.emit('SIGINT')
|
29
|
+
});
|
30
|
+
}
|
31
|
+
|
32
|
+
process.on("SIGINT", function () {
|
33
|
+
Console.debug("Terminated (SIGINT)");
|
34
|
+
process.exit();
|
35
|
+
});
|
36
|
+
}
|
37
|
+
|
38
|
+
async finally() {
|
39
|
+
Console.debug("COMMAND FINALLY");
|
40
|
+
// called after run and catch regardless of whether or not the command errored
|
41
|
+
}
|
42
|
+
|
43
|
+
async run() {
|
44
|
+
// console.log('running my command')
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
export default BaseCommand;
|
@@ -0,0 +1,48 @@
|
|
1
|
+
// import { flags } from "@oclif/command";
|
2
|
+
import BaseCommand from './BaseCommand'
|
3
|
+
import Console from './console'
|
4
|
+
import SessionManager from '../managers/session'
|
5
|
+
import configManager from '../managers/config/index'
|
6
|
+
import {AuthError} from './errors'
|
7
|
+
import {IConfigManager} from '../models/config-manager'
|
8
|
+
|
9
|
+
export default class SessionCommand extends BaseCommand {
|
10
|
+
session: any = null
|
11
|
+
configManager: IConfigManager | null = null
|
12
|
+
static flags: any
|
13
|
+
|
14
|
+
async initSession(flags: any, _private = false) {
|
15
|
+
try {
|
16
|
+
if (!this.configManager) {
|
17
|
+
await this.buildConfig(flags)
|
18
|
+
}
|
19
|
+
|
20
|
+
this.session = await SessionManager.get(this.configManager?.get())
|
21
|
+
if (this.session) {
|
22
|
+
Console.debug(`Session open for ${this.session.payload.email}.`)
|
23
|
+
} else {
|
24
|
+
if (_private)
|
25
|
+
throw AuthError(
|
26
|
+
'You need to log in, run the following command to continue: $ learnpack login',
|
27
|
+
)
|
28
|
+
Console.debug('No active session available', _private)
|
29
|
+
}
|
30
|
+
} catch (error) {
|
31
|
+
Console.error((error as TypeError).message)
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
async buildConfig(flags: any) {
|
36
|
+
this.configManager = await configManager(flags)
|
37
|
+
}
|
38
|
+
|
39
|
+
async catch(err: any) {
|
40
|
+
Console.debug('COMMAND CATCH', err)
|
41
|
+
throw err
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
// SessionCommand.description = `Describe the command here
|
46
|
+
// ...
|
47
|
+
// Extra documentation goes here
|
48
|
+
// `
|
package/src/utils/api.ts
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
import Console from "../utils/console";
|
2
|
+
import * as storage from "node-persist";
|
3
|
+
import cli from "cli-ux";
|
4
|
+
const HOST = "https://learnpack.herokuapp.com";
|
5
|
+
|
6
|
+
// eslint-disable-next-line
|
7
|
+
const _fetch = require("node-fetch");
|
8
|
+
|
9
|
+
interface IHeaders {
|
10
|
+
"Content-Type"?: string;
|
11
|
+
Authorization?: string;
|
12
|
+
}
|
13
|
+
|
14
|
+
interface IOptions {
|
15
|
+
headers?: IHeaders;
|
16
|
+
method?: string;
|
17
|
+
body?: string;
|
18
|
+
}
|
19
|
+
|
20
|
+
const fetch = async (url: string, options: IOptions = {}) => {
|
21
|
+
const headers: IHeaders = { "Content-Type": "application/json" };
|
22
|
+
let session = null;
|
23
|
+
try {
|
24
|
+
session = await storage.getItem("bc-payload");
|
25
|
+
if (session.token && session.token !== "" && !url.includes("/token"))
|
26
|
+
headers.Authorization = "Token " + session.token;
|
27
|
+
} catch {}
|
28
|
+
|
29
|
+
try {
|
30
|
+
const resp = await _fetch(url, {
|
31
|
+
...options,
|
32
|
+
headers: { ...headers, ...options.headers },
|
33
|
+
} as any);
|
34
|
+
|
35
|
+
if (resp.status >= 200 && resp.status < 300)
|
36
|
+
return await resp.json();
|
37
|
+
if (resp.status === 401)
|
38
|
+
throw APIError("Invalid authentication credentials", 401);
|
39
|
+
else if (resp.status === 404)
|
40
|
+
throw APIError("Package not found", 404);
|
41
|
+
else if (resp.status >= 500)
|
42
|
+
throw APIError("Impossible to connect with the server", 500);
|
43
|
+
else if (resp.status >= 400) {
|
44
|
+
const error = await resp.json();
|
45
|
+
if (error.detail || error.error) {
|
46
|
+
throw APIError(error.detail || error.error);
|
47
|
+
} else if (error.nonFieldErrors) {
|
48
|
+
throw APIError(error.nonFieldErrors[0], error);
|
49
|
+
} else if (typeof error === "object") {
|
50
|
+
if (Object.keys(error).length > 0) {
|
51
|
+
const key = error[Object.keys(error)[0]];
|
52
|
+
throw APIError(`${key}: ${error[key][0]}`, error);
|
53
|
+
}
|
54
|
+
} else {
|
55
|
+
throw APIError("Uknown error");
|
56
|
+
}
|
57
|
+
} else
|
58
|
+
throw APIError("Uknown error");
|
59
|
+
} catch (error) {
|
60
|
+
Console.error((error as TypeError).message);
|
61
|
+
throw error;
|
62
|
+
}
|
63
|
+
};
|
64
|
+
|
65
|
+
const login = async (identification: string, password: string) => {
|
66
|
+
try {
|
67
|
+
cli.action.start("Looking for credentials...");
|
68
|
+
await cli.wait(1000);
|
69
|
+
const data = await fetch(`${HOST}/v1/auth/token/`, {
|
70
|
+
body: JSON.stringify({ identification, password }),
|
71
|
+
method: "post",
|
72
|
+
});
|
73
|
+
cli.action.stop("ready");
|
74
|
+
return data;
|
75
|
+
} catch (error) {
|
76
|
+
Console.error((error as TypeError).message);
|
77
|
+
Console.debug(error);
|
78
|
+
}
|
79
|
+
};
|
80
|
+
|
81
|
+
const publish = async (config: any) => {
|
82
|
+
const keys = [
|
83
|
+
"difficulty",
|
84
|
+
"language",
|
85
|
+
"skills",
|
86
|
+
"technologies",
|
87
|
+
"slug",
|
88
|
+
"repository",
|
89
|
+
"author",
|
90
|
+
"title",
|
91
|
+
];
|
92
|
+
|
93
|
+
const payload: { [key: string]: string } = {};
|
94
|
+
for (const k of keys)
|
95
|
+
config[k] ? (payload[k] = config[k]) : null;
|
96
|
+
try {
|
97
|
+
console.log("Package to publish:", payload);
|
98
|
+
cli.action.start("Updating package information...");
|
99
|
+
await cli.wait(1000);
|
100
|
+
const data = await fetch(`${HOST}/v1/package/${config.slug}`, {
|
101
|
+
method: "PUT",
|
102
|
+
body: JSON.stringify(payload),
|
103
|
+
});
|
104
|
+
cli.action.stop("ready");
|
105
|
+
return data;
|
106
|
+
} catch (error) {
|
107
|
+
console.log("payload", payload);
|
108
|
+
Console.error((error as TypeError).message);
|
109
|
+
Console.debug(error);
|
110
|
+
throw error;
|
111
|
+
}
|
112
|
+
};
|
113
|
+
|
114
|
+
const update = async (config: any) => {
|
115
|
+
try {
|
116
|
+
cli.action.start("Updating package information...");
|
117
|
+
await cli.wait(1000);
|
118
|
+
const data = await fetch(`${HOST}/v1/package/`, {
|
119
|
+
method: "POST",
|
120
|
+
body: JSON.stringify(config),
|
121
|
+
});
|
122
|
+
cli.action.stop("ready");
|
123
|
+
return data;
|
124
|
+
} catch (error) {
|
125
|
+
Console.error((error as any).message);
|
126
|
+
Console.debug(error);
|
127
|
+
throw error;
|
128
|
+
}
|
129
|
+
};
|
130
|
+
|
131
|
+
const getPackage = async (slug: string) => {
|
132
|
+
try {
|
133
|
+
cli.action.start("Downloading package information...");
|
134
|
+
await cli.wait(1000);
|
135
|
+
const data = await fetch(`${HOST}/v1/package/${slug}`);
|
136
|
+
cli.action.stop("ready");
|
137
|
+
return data;
|
138
|
+
} catch (error) {
|
139
|
+
if ((error as any).status === 404)
|
140
|
+
Console.error(`Package ${slug} does not exist`);
|
141
|
+
else
|
142
|
+
Console.error(`Package ${slug} does not exist`);
|
143
|
+
Console.debug(error);
|
144
|
+
throw error;
|
145
|
+
}
|
146
|
+
};
|
147
|
+
|
148
|
+
const getLangs = async () => {
|
149
|
+
try {
|
150
|
+
cli.action.start("Downloading language options...");
|
151
|
+
await cli.wait(1000);
|
152
|
+
const data = await fetch(`${HOST}/v1/package/language`);
|
153
|
+
cli.action.stop("ready");
|
154
|
+
return data;
|
155
|
+
} catch (error) {
|
156
|
+
if ((error as any).status === 404)
|
157
|
+
Console.error("Package slug does not exist");
|
158
|
+
else
|
159
|
+
Console.error("Package slug does not exist");
|
160
|
+
Console.debug(error);
|
161
|
+
throw error;
|
162
|
+
}
|
163
|
+
};
|
164
|
+
|
165
|
+
const getAllPackages = async ({
|
166
|
+
lang = "",
|
167
|
+
slug = "",
|
168
|
+
}: {
|
169
|
+
lang?: string;
|
170
|
+
slug?: string;
|
171
|
+
}) => {
|
172
|
+
try {
|
173
|
+
cli.action.start("Downloading packages...");
|
174
|
+
await cli.wait(1000);
|
175
|
+
const data = await fetch(
|
176
|
+
`${HOST}/v1/package/all?limit=100&language=${lang}&slug=${slug}`
|
177
|
+
);
|
178
|
+
cli.action.stop("ready");
|
179
|
+
return data;
|
180
|
+
} catch (error) {
|
181
|
+
Console.error(`Package ${slug} does not exist`);
|
182
|
+
Console.debug(error);
|
183
|
+
throw error;
|
184
|
+
}
|
185
|
+
};
|
186
|
+
|
187
|
+
const APIError = (error: TypeError | string, code?: number) => {
|
188
|
+
const message: string = (error as TypeError).message || (error as string);
|
189
|
+
const _err = new Error(message) as any;
|
190
|
+
_err.status = code || 400;
|
191
|
+
return _err;
|
192
|
+
};
|
193
|
+
|
194
|
+
export default { login, publish, update, getPackage, getLangs, getAllPackages };
|
@@ -0,0 +1,162 @@
|
|
1
|
+
import {IAuditErrors} from '../models/audit-errors'
|
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) => {
|
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
|
+
|
127
|
+
Console.error(
|
128
|
+
` We found ${errors.length} errors among ${counter.images.total} images, ${counter.links.total} link, ${counter.readmeFiles} README files and ${counter.exercises} exercises.`,
|
129
|
+
)
|
130
|
+
process.exit(1)
|
131
|
+
} else {
|
132
|
+
Console.success(
|
133
|
+
`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.`,
|
134
|
+
)
|
135
|
+
process.exit(0)
|
136
|
+
}
|
137
|
+
} else {
|
138
|
+
reject('Failed')
|
139
|
+
}
|
140
|
+
})
|
141
|
+
},
|
142
|
+
// This function checks if there are warnings, and show them in the console at the end.
|
143
|
+
showWarnings: (warnings: IAuditErrors[]) => {
|
144
|
+
return new Promise((resolve, reject) => {
|
145
|
+
if (warnings) {
|
146
|
+
if (warnings.length > 0) {
|
147
|
+
Console.log('Checking for warnings...')
|
148
|
+
for (const [i, warning] of warnings.entries())
|
149
|
+
Console.warning(
|
150
|
+
`${i + 1}) ${warning.msg} ${
|
151
|
+
warning.exercise ? `File: ${warning.exercise}` : ''
|
152
|
+
}`,
|
153
|
+
)
|
154
|
+
}
|
155
|
+
|
156
|
+
resolve('SUCCESS')
|
157
|
+
} else {
|
158
|
+
reject('Failed')
|
159
|
+
}
|
160
|
+
})
|
161
|
+
},
|
162
|
+
}
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import * as chalk from 'chalk'
|
2
|
+
|
3
|
+
export default {
|
4
|
+
// _debug: true,
|
5
|
+
_debug: process.env.DEBUG === 'true',
|
6
|
+
startDebug: function () {
|
7
|
+
this._debug = true
|
8
|
+
},
|
9
|
+
log: (msg: string | Array<string>, ...args: Array<any>) =>
|
10
|
+
console.log(chalk.gray(msg), ...args),
|
11
|
+
error: (msg: string, ...args: Array<any>) =>
|
12
|
+
console.log(chalk.red('⨉ ' + msg), ...args),
|
13
|
+
success: (msg: string, ...args: Array<any>) =>
|
14
|
+
console.log(chalk.green('✓ ' + msg), ...args),
|
15
|
+
info: (msg: string, ...args: Array<any>) =>
|
16
|
+
console.log(chalk.blue('ⓘ ' + msg), ...args),
|
17
|
+
help: (msg: string) =>
|
18
|
+
console.log(`${chalk.white.bold('⚠ help:')} ${chalk.white(msg)}`),
|
19
|
+
debug(...args: Array<any>) {
|
20
|
+
this._debug && console.log(chalk.magentaBright('⚠ debug: '), args)
|
21
|
+
},
|
22
|
+
warning: (msg: string) =>
|
23
|
+
console.log(`${chalk.yellow('⚠ warning:')} ${chalk.yellow(msg)}`),
|
24
|
+
}
|