@learnpack/learnpack 5.0.365 → 5.0.366
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/lib/managers/config/index.js +2 -5
- package/lib/managers/file.js +19 -51
- package/lib/managers/ideReleases.d.ts +24 -0
- package/lib/managers/ideReleases.js +111 -0
- package/package.json +1 -1
- package/src/managers/config/index.ts +17 -31
- package/src/managers/file.ts +30 -70
- package/src/managers/ideReleases.ts +142 -0
|
@@ -12,11 +12,10 @@ const sidebarGenerator_1 = require("../../utils/sidebarGenerator");
|
|
|
12
12
|
const defaults_1 = require("./defaults");
|
|
13
13
|
const exercise_1 = require("./exercise");
|
|
14
14
|
const file_1 = require("../file");
|
|
15
|
+
const ideReleases_1 = require("../ideReleases");
|
|
15
16
|
/* exercise folder name standard */
|
|
16
17
|
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
17
18
|
// eslint-disable-next-line
|
|
18
|
-
const fetch = require("node-fetch");
|
|
19
|
-
// eslint-disable-next-line
|
|
20
19
|
const chalk = require("chalk");
|
|
21
20
|
/* exercise folder name standard */
|
|
22
21
|
/**
|
|
@@ -186,9 +185,7 @@ exports.default = async ({ grading, mode, disableGrading, version, }) => {
|
|
|
186
185
|
configObj.config.editor.version = version;
|
|
187
186
|
else if (configObj.config.editor.version === null) {
|
|
188
187
|
console_1.default.debug("Config version not found, downloading default.");
|
|
189
|
-
|
|
190
|
-
const packageJSON = await resp.json();
|
|
191
|
-
configObj.config.editor.version = packageJSON.version || "5.0.0";
|
|
188
|
+
configObj.config.editor.version = await (0, ideReleases_1.getLatestEditorVersion)();
|
|
192
189
|
}
|
|
193
190
|
configObj.config.dirPath = "./" + confPath.base;
|
|
194
191
|
configObj.config.exercisesPath = getExercisesPath(confPath.base) || "./";
|
package/lib/managers/file.js
CHANGED
|
@@ -9,6 +9,7 @@ const targz = require("targz");
|
|
|
9
9
|
const console_1 = require("../utils/console");
|
|
10
10
|
const https = require("https");
|
|
11
11
|
const errors_1 = require("../utils/errors");
|
|
12
|
+
const ideReleases_1 = require("./ideReleases");
|
|
12
13
|
// eslint-disable-next-line
|
|
13
14
|
const fetch = require("node-fetch");
|
|
14
15
|
const decompress = (sourcePath, destinationPath) => new Promise((resolve, reject) => {
|
|
@@ -30,54 +31,22 @@ const decompress = (sourcePath, destinationPath) => new Promise((resolve, reject
|
|
|
30
31
|
exports.decompress = decompress;
|
|
31
32
|
const downloadEditor = async (version, destination) => {
|
|
32
33
|
console_1.default.info("Downloading the LearnPack coding UI, this may take a minute...");
|
|
33
|
-
if (!version)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
if (!version)
|
|
35
|
+
version = await (0, ideReleases_1.getLatestEditorVersion)();
|
|
36
|
+
const [cloudTags, githubTags] = await Promise.all([
|
|
37
|
+
(0, ideReleases_1.fetchTagList)(ideReleases_1.IDE_CLOUDFRONT_MANIFEST),
|
|
38
|
+
(0, ideReleases_1.fetchTagList)(ideReleases_1.IDE_GITHUB_MANIFEST),
|
|
39
|
+
]);
|
|
40
|
+
let release = (0, ideReleases_1.resolveEditorRelease)(version, cloudTags, githubTags);
|
|
41
|
+
if (!release) {
|
|
42
|
+
console_1.default.debug(`No IDE version found for ${version} in the versions manifests, defaulting to version 4.0.2`);
|
|
43
|
+
release = { version: "4.0.2", source: "github" };
|
|
39
44
|
}
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
tags = await tagsRes.json();
|
|
46
|
-
if (!Array.isArray(tags)) {
|
|
47
|
-
throw new TypeError(`Invalid versions.json format`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
console_1.default.debug("Error reading versions.json, defaulting to version 4.0.2", error);
|
|
52
|
-
version = "4.0.2";
|
|
53
|
-
tags = [`learnpack-${version}.tar.gz`];
|
|
54
|
-
}
|
|
55
|
-
let matchingTags;
|
|
56
|
-
try {
|
|
57
|
-
if (tags.length === 0 || !tags)
|
|
58
|
-
throw (0, errors_1.InternalError)(`No found tags in versions.json ${tags}`);
|
|
59
|
-
matchingTags = tags
|
|
60
|
-
.filter(tag => tag.includes(`learnpack-${versionPrefix}`))
|
|
61
|
-
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
|
62
|
-
if (matchingTags.length === 0)
|
|
63
|
-
throw (0, errors_1.InternalError)(`No matching version found for prefix ${versionPrefix} in the versions.json`);
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
console_1.default.debug("Error processing tags, defaulting to version 4.0.2", error);
|
|
67
|
-
version = "4.0.2";
|
|
68
|
-
matchingTags = [`learnpack-${version}.tar.gz`];
|
|
69
|
-
}
|
|
70
|
-
const latestVersion = matchingTags[0]
|
|
71
|
-
.replace("learnpack-", "")
|
|
72
|
-
.replace(".tar.gz", "");
|
|
73
|
-
const versionNumber = parseInt(latestVersion.split(".")[0]);
|
|
74
|
-
let url = `https://github.com/learnpack/coding-ide/blob/${latestVersion}/dist`;
|
|
75
|
-
if (versionNumber > 2)
|
|
76
|
-
url = `https://github.com/learnpack/ide/blob/master/bin/learnpack-${latestVersion}.tar.gz`;
|
|
77
|
-
const resp2 = await fetch(url, { method: "HEAD" });
|
|
78
|
-
if (!resp2.ok)
|
|
79
|
-
throw (0, errors_1.InternalError)(`Coding Editor ${latestVersion} was not found on learnpack repository, check the config.editor.version property on learn.json`);
|
|
80
|
-
url += versionNumber > 2 ? `?raw=true` : `/app.tar.gz?raw=true`;
|
|
45
|
+
const url = (0, ideReleases_1.editorTarballUrl)(release.version, release.source);
|
|
46
|
+
const resp = await fetch(url, { method: "HEAD", timeout: ideReleases_1.FETCH_TIMEOUT_MS });
|
|
47
|
+
if (!resp.ok)
|
|
48
|
+
throw (0, errors_1.InternalError)(`Coding Editor ${release.version} was not found on ${release.source}, check the config.editor.version property on learn.json`);
|
|
49
|
+
console_1.default.debug(`Using IDE ${release.version} from ${release.source}`);
|
|
81
50
|
return (0, exports.download)(url, destination);
|
|
82
51
|
};
|
|
83
52
|
exports.downloadEditor = downloadEditor;
|
|
@@ -92,7 +61,7 @@ const download = (url, dest) => {
|
|
|
92
61
|
});
|
|
93
62
|
file.on("error", err => {
|
|
94
63
|
file.close();
|
|
95
|
-
if (err.
|
|
64
|
+
if (err.code === "EEXIST") {
|
|
96
65
|
console_1.default.debug("File already exists");
|
|
97
66
|
resolve("File already exists");
|
|
98
67
|
}
|
|
@@ -103,11 +72,10 @@ const download = (url, dest) => {
|
|
|
103
72
|
});
|
|
104
73
|
response.pipe(file);
|
|
105
74
|
}
|
|
106
|
-
else if (
|
|
107
|
-
// Console.debug("Servers redirected to "+response.headers.location)
|
|
75
|
+
else if ([301, 302, 303, 307, 308].includes(response.statusCode || 0)) {
|
|
108
76
|
// Recursively follow redirects, only a 200 will resolve.
|
|
109
77
|
if (response.headers.location) {
|
|
110
|
-
(0, exports.download)(response.headers.location, dest)
|
|
78
|
+
(0, exports.download)(new URL(response.headers.location, url).toString(), dest)
|
|
111
79
|
.then(() => resolve(/* */ ""))
|
|
112
80
|
.catch(error => {
|
|
113
81
|
console_1.default.error(error);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const IDE_CLOUDFRONT_BASE = "https://d3a7w3e3g8ydll.cloudfront.net/learnpack/ide";
|
|
2
|
+
export declare const IDE_CLOUDFRONT_MANIFEST = "https://d3a7w3e3g8ydll.cloudfront.net/learnpack/ide/versions.json";
|
|
3
|
+
export declare const IDE_GITHUB_MANIFEST = "https://raw.githubusercontent.com/learnpack/ide/master/versions.json";
|
|
4
|
+
export declare const IDE_GITHUB_PACKAGE = "https://raw.githubusercontent.com/learnpack/ide/master/package.json";
|
|
5
|
+
export declare const FETCH_TIMEOUT_MS = 10000;
|
|
6
|
+
export type TEditorSource = "cloudfront" | "github";
|
|
7
|
+
export interface IEditorRelease {
|
|
8
|
+
version: string;
|
|
9
|
+
source: TEditorSource;
|
|
10
|
+
}
|
|
11
|
+
interface IParsedVersion {
|
|
12
|
+
version: string;
|
|
13
|
+
major: number;
|
|
14
|
+
minor: number;
|
|
15
|
+
patch: number;
|
|
16
|
+
}
|
|
17
|
+
export declare const parseVersion: (version: string) => IParsedVersion | null;
|
|
18
|
+
export declare const parseTag: (tag: string) => IParsedVersion | null;
|
|
19
|
+
export declare const highestVersion: (tags: string[] | null) => string | null;
|
|
20
|
+
export declare const resolveEditorRelease: (requested: string, cloudTags: string[] | null, githubTags: string[] | null) => IEditorRelease | null;
|
|
21
|
+
export declare const editorTarballUrl: (version: string, source: TEditorSource) => string;
|
|
22
|
+
export declare const fetchTagList: (url: string) => Promise<string[] | null>;
|
|
23
|
+
export declare const getLatestEditorVersion: () => Promise<string>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getLatestEditorVersion = exports.fetchTagList = exports.editorTarballUrl = exports.resolveEditorRelease = exports.highestVersion = exports.parseTag = exports.parseVersion = exports.FETCH_TIMEOUT_MS = exports.IDE_GITHUB_PACKAGE = exports.IDE_GITHUB_MANIFEST = exports.IDE_CLOUDFRONT_MANIFEST = exports.IDE_CLOUDFRONT_BASE = void 0;
|
|
4
|
+
const console_1 = require("../utils/console");
|
|
5
|
+
// eslint-disable-next-line
|
|
6
|
+
const fetch = require("node-fetch");
|
|
7
|
+
// IDE tarballs published from 5.0.417 on live on CloudFront; older ones stay in the
|
|
8
|
+
// learnpack/ide GitHub repo, whose versions.json is frozen. Both lists are merged.
|
|
9
|
+
exports.IDE_CLOUDFRONT_BASE = "https://d3a7w3e3g8ydll.cloudfront.net/learnpack/ide";
|
|
10
|
+
exports.IDE_CLOUDFRONT_MANIFEST = `${exports.IDE_CLOUDFRONT_BASE}/versions.json`;
|
|
11
|
+
exports.IDE_GITHUB_MANIFEST = "https://raw.githubusercontent.com/learnpack/ide/master/versions.json";
|
|
12
|
+
exports.IDE_GITHUB_PACKAGE = "https://raw.githubusercontent.com/learnpack/ide/master/package.json";
|
|
13
|
+
exports.FETCH_TIMEOUT_MS = 10000;
|
|
14
|
+
const DEFAULT_EDITOR_VERSION = "5.0.0";
|
|
15
|
+
const parseVersion = (version) => {
|
|
16
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim());
|
|
17
|
+
if (!match)
|
|
18
|
+
return null;
|
|
19
|
+
return {
|
|
20
|
+
version: match.slice(1, 4).join("."),
|
|
21
|
+
major: Number(match[1]),
|
|
22
|
+
minor: Number(match[2]),
|
|
23
|
+
patch: Number(match[3]),
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
exports.parseVersion = parseVersion;
|
|
27
|
+
// "learnpack-5.0.417.tar.gz" -> parsed 5.0.417; anything else -> null
|
|
28
|
+
const parseTag = (tag) => {
|
|
29
|
+
const match = /^learnpack-(\d+\.\d+\.\d+)\.tar\.gz$/.exec(tag);
|
|
30
|
+
return match ? (0, exports.parseVersion)(match[1]) : null;
|
|
31
|
+
};
|
|
32
|
+
exports.parseTag = parseTag;
|
|
33
|
+
const compareVersions = (a, b) => a.major - b.major || a.minor - b.minor || a.patch - b.patch;
|
|
34
|
+
const parseTags = (tags) => (tags || []).map(exports.parseTag).filter((v) => v !== null);
|
|
35
|
+
// Highest version listed in a manifest, or null when there is none.
|
|
36
|
+
const highestVersion = (tags) => {
|
|
37
|
+
const sorted = parseTags(tags).sort(compareVersions);
|
|
38
|
+
return sorted.length > 0 ? sorted[sorted.length - 1].version : null;
|
|
39
|
+
};
|
|
40
|
+
exports.highestVersion = highestVersion;
|
|
41
|
+
// Picks the tarball to download for the requested version. Only major.minor of the
|
|
42
|
+
// request is used and the highest patch available wins, as the CLI always did.
|
|
43
|
+
// The source is CloudFront when the version is in its manifest, GitHub otherwise.
|
|
44
|
+
const resolveEditorRelease = (requested, cloudTags, githubTags) => {
|
|
45
|
+
const [major, minor] = requested.split(".").map(part => Number(part));
|
|
46
|
+
if (!Number.isInteger(major) || !Number.isInteger(minor))
|
|
47
|
+
return null;
|
|
48
|
+
const cloud = parseTags(cloudTags);
|
|
49
|
+
const candidates = [...cloud, ...parseTags(githubTags)]
|
|
50
|
+
.filter(v => v.major === major && v.minor === minor)
|
|
51
|
+
.sort(compareVersions);
|
|
52
|
+
if (candidates.length === 0)
|
|
53
|
+
return null;
|
|
54
|
+
const { version } = candidates[candidates.length - 1];
|
|
55
|
+
const source = cloud.some(v => v.version === version) ?
|
|
56
|
+
"cloudfront" :
|
|
57
|
+
"github";
|
|
58
|
+
return { version, source };
|
|
59
|
+
};
|
|
60
|
+
exports.resolveEditorRelease = resolveEditorRelease;
|
|
61
|
+
const editorTarballUrl = (version, source) => {
|
|
62
|
+
if (source === "cloudfront") {
|
|
63
|
+
return `${exports.IDE_CLOUDFRONT_BASE}/learnpack-${version}.tar.gz`;
|
|
64
|
+
}
|
|
65
|
+
const major = Number.parseInt(version.split(".")[0], 10);
|
|
66
|
+
return major > 2 ?
|
|
67
|
+
`https://github.com/learnpack/ide/blob/master/bin/learnpack-${version}.tar.gz?raw=true` :
|
|
68
|
+
`https://github.com/learnpack/coding-ide/blob/${version}/dist/app.tar.gz?raw=true`;
|
|
69
|
+
};
|
|
70
|
+
exports.editorTarballUrl = editorTarballUrl;
|
|
71
|
+
// Fetches a versions manifest. Never throws: any failure returns null.
|
|
72
|
+
const fetchTagList = async (url) => {
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetch(url, { timeout: exports.FETCH_TIMEOUT_MS });
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
console_1.default.debug(`Could not read ${url}: ${res.status} ${res.statusText}`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const json = await res.json();
|
|
80
|
+
if (!Array.isArray(json) || !json.every(tag => typeof tag === "string")) {
|
|
81
|
+
console_1.default.debug(`Invalid versions manifest format at ${url}`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return json;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
console_1.default.debug(`Could not read ${url}`, error);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
exports.fetchTagList = fetchTagList;
|
|
92
|
+
// Latest published IDE version: CloudFront manifest, then GitHub package.json, then a default.
|
|
93
|
+
const getLatestEditorVersion = async () => {
|
|
94
|
+
const fromCloud = (0, exports.highestVersion)(await (0, exports.fetchTagList)(exports.IDE_CLOUDFRONT_MANIFEST));
|
|
95
|
+
if (fromCloud)
|
|
96
|
+
return fromCloud;
|
|
97
|
+
try {
|
|
98
|
+
const res = await fetch(exports.IDE_GITHUB_PACKAGE, { timeout: exports.FETCH_TIMEOUT_MS });
|
|
99
|
+
if (res.ok) {
|
|
100
|
+
const json = await res.json();
|
|
101
|
+
if (typeof (json === null || json === void 0 ? void 0 : json.version) === "string" && (0, exports.parseVersion)(json.version)) {
|
|
102
|
+
return json.version;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console_1.default.debug("Could not read the IDE package.json from GitHub", error);
|
|
108
|
+
}
|
|
109
|
+
return DEFAULT_EDITOR_VERSION;
|
|
110
|
+
};
|
|
111
|
+
exports.getLatestEditorVersion = getLatestEditorVersion;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@learnpack/learnpack",
|
|
3
3
|
"description": "Seamlessly build, sell and/or take interactive & auto-graded tutorials, start learning now or build a new tutorial to your audience.",
|
|
4
|
-
"version": "5.0.
|
|
4
|
+
"version": "5.0.366",
|
|
5
5
|
"author": "Alejandro Sanchez @alesanchezr",
|
|
6
6
|
"contributors": [
|
|
7
7
|
{
|
|
@@ -17,6 +17,7 @@ import defaults from "./defaults"
|
|
|
17
17
|
import { exercise } from "./exercise"
|
|
18
18
|
|
|
19
19
|
import { rmSync } from "../file"
|
|
20
|
+
import { getLatestEditorVersion } from "../ideReleases"
|
|
20
21
|
import { IConfigObj, TMode, TAgent } from "../../models/config"
|
|
21
22
|
import {
|
|
22
23
|
IConfigManagerAttributes,
|
|
@@ -29,9 +30,7 @@ import { IExercise } from "../../models/exercise-obj"
|
|
|
29
30
|
const execAsync = promisify(exec)
|
|
30
31
|
|
|
31
32
|
// eslint-disable-next-line
|
|
32
|
-
const
|
|
33
|
-
// eslint-disable-next-line
|
|
34
|
-
const chalk = require("chalk")
|
|
33
|
+
const chalk = require("chalk");
|
|
35
34
|
|
|
36
35
|
/* exercise folder name standard */
|
|
37
36
|
|
|
@@ -167,8 +166,7 @@ export default async ({
|
|
|
167
166
|
configPath: confPath.config,
|
|
168
167
|
},
|
|
169
168
|
})
|
|
170
|
-
if (!configObj.config)
|
|
171
|
-
throw InternalError("Config object not found")
|
|
169
|
+
if (!configObj.config) throw InternalError("Config object not found")
|
|
172
170
|
|
|
173
171
|
configObj.config.outputPath = confPath.base + "/dist"
|
|
174
172
|
|
|
@@ -263,15 +261,10 @@ throw InternalError("Config object not found")
|
|
|
263
261
|
configObj.config.editor.mode =
|
|
264
262
|
process.env.TERM_PROGRAM === "vscode" ? "extension" : "preview"
|
|
265
263
|
|
|
266
|
-
if (version)
|
|
267
|
-
configObj.config.editor.version = version
|
|
264
|
+
if (version) configObj.config.editor.version = version
|
|
268
265
|
else if (configObj.config.editor.version === null) {
|
|
269
266
|
Console.debug("Config version not found, downloading default.")
|
|
270
|
-
|
|
271
|
-
"https://raw.githubusercontent.com/learnpack/ide/master/package.json"
|
|
272
|
-
)
|
|
273
|
-
const packageJSON = await resp.json()
|
|
274
|
-
configObj.config.editor.version = packageJSON.version || "5.0.0"
|
|
267
|
+
configObj.config.editor.version = await getLatestEditorVersion()
|
|
275
268
|
}
|
|
276
269
|
|
|
277
270
|
configObj.config.dirPath = "./" + confPath.base
|
|
@@ -303,8 +296,7 @@ configObj.config.editor.version = version
|
|
|
303
296
|
|
|
304
297
|
// Execute the code and replace the value
|
|
305
298
|
const { stdout } = await execAsync(variable.source)
|
|
306
|
-
if (!configObj.config || !configObj.config.variables)
|
|
307
|
-
return
|
|
299
|
+
if (!configObj.config || !configObj.config.variables) return
|
|
308
300
|
|
|
309
301
|
configObj.config.variables[v] = stdout.trim()
|
|
310
302
|
} catch (error) {
|
|
@@ -332,16 +324,14 @@ return
|
|
|
332
324
|
const map: any = {
|
|
333
325
|
python3: "python",
|
|
334
326
|
}
|
|
335
|
-
if (map[_l])
|
|
336
|
-
return map[_l]
|
|
327
|
+
if (map[_l]) return map[_l]
|
|
337
328
|
return _l
|
|
338
329
|
}
|
|
339
330
|
|
|
340
331
|
// decode aliases
|
|
341
332
|
language = alias(language)
|
|
342
333
|
|
|
343
|
-
if (this.validLanguages[language])
|
|
344
|
-
return true
|
|
334
|
+
if (this.validLanguages[language]) return true
|
|
345
335
|
|
|
346
336
|
Console.debug(`Validating engine for ${language} compilation`)
|
|
347
337
|
let result = shell.exec("learnpack plugins", { silent: true })
|
|
@@ -430,8 +420,7 @@ return true
|
|
|
430
420
|
const exercise = (configObj.exercises || []).find(
|
|
431
421
|
ex => ex.slug === slug
|
|
432
422
|
)
|
|
433
|
-
if (!exercise)
|
|
434
|
-
throw ValidationError(`Exercise ${slug} not found`)
|
|
423
|
+
if (!exercise) throw ValidationError(`Exercise ${slug} not found`)
|
|
435
424
|
|
|
436
425
|
return exercise
|
|
437
426
|
},
|
|
@@ -450,8 +439,7 @@ throw ValidationError(`Exercise ${slug} not found`)
|
|
|
450
439
|
exercise.files.forEach((f: IFile) => {
|
|
451
440
|
if (configObj.config) {
|
|
452
441
|
const _path = configObj.config.outputPath + "/" + f.name
|
|
453
|
-
if (f.hidden === false && fs.existsSync(_path))
|
|
454
|
-
fs.unlinkSync(_path)
|
|
442
|
+
if (f.hidden === false && fs.existsSync(_path)) fs.unlinkSync(_path)
|
|
455
443
|
}
|
|
456
444
|
})
|
|
457
445
|
|
|
@@ -472,7 +460,9 @@ fs.unlinkSync(_path)
|
|
|
472
460
|
ex => ex.slug === slug
|
|
473
461
|
)
|
|
474
462
|
if (!exercise)
|
|
475
|
-
throw ValidationError(
|
|
463
|
+
throw ValidationError(
|
|
464
|
+
`Exercise ${slug} not found on the configuration`
|
|
465
|
+
)
|
|
476
466
|
|
|
477
467
|
if (configObj.config) {
|
|
478
468
|
for (const fileName of fs.readdirSync(
|
|
@@ -494,8 +484,7 @@ fs.unlinkSync(_path)
|
|
|
494
484
|
if (name === path.basename(configObj?.config?.dirPath || ""))
|
|
495
485
|
return false
|
|
496
486
|
// ignore folders that start with a dot
|
|
497
|
-
if (name.charAt(0) === "." || name.charAt(0) === "_")
|
|
498
|
-
return false
|
|
487
|
+
if (name.charAt(0) === "." || name.charAt(0) === "_") return false
|
|
499
488
|
|
|
500
489
|
return fs.lstatSync(source).isDirectory()
|
|
501
490
|
}
|
|
@@ -506,8 +495,7 @@ return false
|
|
|
506
495
|
.map(name => path.join(source, name))
|
|
507
496
|
.filter(isDirectory)
|
|
508
497
|
// add the .learn folder
|
|
509
|
-
if (!fs.existsSync(confPath.base))
|
|
510
|
-
fs.mkdirSync(confPath.base)
|
|
498
|
+
if (!fs.existsSync(confPath.base)) fs.mkdirSync(confPath.base)
|
|
511
499
|
// add the outout folder where webpack will publish the the html/css/js files
|
|
512
500
|
if (
|
|
513
501
|
configObj.config &&
|
|
@@ -537,8 +525,7 @@ fs.mkdirSync(confPath.base)
|
|
|
537
525
|
createExercise: (slug: string, content: string, language: string) => {
|
|
538
526
|
try {
|
|
539
527
|
const dirPath = `${configObj.config?.exercisesPath}/${slug}`
|
|
540
|
-
if (!fs.existsSync(dirPath))
|
|
541
|
-
fs.mkdirSync(dirPath, { recursive: true })
|
|
528
|
+
if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true })
|
|
542
529
|
const isEnglish = language === "us" || language === "en"
|
|
543
530
|
const fileName = isEnglish ? "README.md" : `README.${language}.md`
|
|
544
531
|
fs.writeFileSync(`${dirPath}/${fileName}`, content)
|
|
@@ -623,8 +610,7 @@ function deepMerge(...sources: any): any {
|
|
|
623
610
|
value = deepMerge(acc[key], value)
|
|
624
611
|
}
|
|
625
612
|
|
|
626
|
-
if (value !== undefined)
|
|
627
|
-
acc = { ...acc, [key]: value }
|
|
613
|
+
if (value !== undefined) acc = { ...acc, [key]: value }
|
|
628
614
|
}
|
|
629
615
|
}
|
|
630
616
|
}
|
package/src/managers/file.ts
CHANGED
|
@@ -6,9 +6,18 @@ import * as targz from "targz"
|
|
|
6
6
|
import Console from "../utils/console"
|
|
7
7
|
import * as https from "https"
|
|
8
8
|
import { InternalError } from "../utils/errors"
|
|
9
|
+
import {
|
|
10
|
+
FETCH_TIMEOUT_MS,
|
|
11
|
+
IDE_CLOUDFRONT_MANIFEST,
|
|
12
|
+
IDE_GITHUB_MANIFEST,
|
|
13
|
+
editorTarballUrl,
|
|
14
|
+
fetchTagList,
|
|
15
|
+
getLatestEditorVersion,
|
|
16
|
+
resolveEditorRelease,
|
|
17
|
+
} from "./ideReleases"
|
|
9
18
|
|
|
10
19
|
// eslint-disable-next-line
|
|
11
|
-
const fetch = require("node-fetch")
|
|
20
|
+
const fetch = require("node-fetch");
|
|
12
21
|
|
|
13
22
|
export const decompress = (sourcePath: string, destinationPath: string) =>
|
|
14
23
|
new Promise((resolve, reject) => {
|
|
@@ -34,81 +43,33 @@ export const downloadEditor = async (
|
|
|
34
43
|
version: string | undefined,
|
|
35
44
|
destination: string
|
|
36
45
|
) => {
|
|
37
|
-
Console.info(
|
|
46
|
+
Console.info(
|
|
47
|
+
"Downloading the LearnPack coding UI, this may take a minute..."
|
|
48
|
+
)
|
|
38
49
|
|
|
39
|
-
if (!version)
|
|
40
|
-
const res = await fetch(
|
|
41
|
-
"https://raw.githubusercontent.com/learnpack/ide/master/package.json"
|
|
42
|
-
)
|
|
43
|
-
const json = await res.json()
|
|
44
|
-
version = json.version || "4.0.0"
|
|
50
|
+
if (!version) version = await getLatestEditorVersion()
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
52
|
+
const [cloudTags, githubTags] = await Promise.all([
|
|
53
|
+
fetchTagList(IDE_CLOUDFRONT_MANIFEST),
|
|
54
|
+
fetchTagList(IDE_GITHUB_MANIFEST),
|
|
55
|
+
])
|
|
51
56
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
let tags
|
|
56
|
-
try {
|
|
57
|
-
const tagsRes = await fetch(
|
|
58
|
-
"https://raw.githubusercontent.com/learnpack/ide/master/versions.json"
|
|
59
|
-
)
|
|
60
|
-
tags = await tagsRes.json()
|
|
61
|
-
|
|
62
|
-
if (!Array.isArray(tags)) {
|
|
63
|
-
throw new TypeError(`Invalid versions.json format`)
|
|
64
|
-
}
|
|
65
|
-
} catch (error) {
|
|
57
|
+
let release = resolveEditorRelease(version, cloudTags, githubTags)
|
|
58
|
+
if (!release) {
|
|
66
59
|
Console.debug(
|
|
67
|
-
|
|
68
|
-
error
|
|
60
|
+
`No IDE version found for ${version} in the versions manifests, defaulting to version 4.0.2`
|
|
69
61
|
)
|
|
70
|
-
|
|
71
|
-
tags = [`learnpack-${version}.tar.gz`]
|
|
62
|
+
release = { version: "4.0.2", source: "github" }
|
|
72
63
|
}
|
|
73
64
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
throw InternalError(`No found tags in versions.json ${tags}`)
|
|
78
|
-
|
|
79
|
-
matchingTags = tags
|
|
80
|
-
.filter(tag => tag.includes(`learnpack-${versionPrefix}`))
|
|
81
|
-
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))
|
|
82
|
-
|
|
83
|
-
if (matchingTags.length === 0)
|
|
84
|
-
throw InternalError(
|
|
85
|
-
`No matching version found for prefix ${versionPrefix} in the versions.json`
|
|
86
|
-
)
|
|
87
|
-
} catch (error) {
|
|
88
|
-
Console.debug("Error processing tags, defaulting to version 4.0.2", error)
|
|
89
|
-
version = "4.0.2"
|
|
90
|
-
matchingTags = [`learnpack-${version}.tar.gz`]
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const latestVersion = matchingTags[0]
|
|
94
|
-
.replace("learnpack-", "")
|
|
95
|
-
.replace(".tar.gz", "")
|
|
96
|
-
|
|
97
|
-
const versionNumber = parseInt(latestVersion.split(".")[0])
|
|
98
|
-
|
|
99
|
-
let url = `https://github.com/learnpack/coding-ide/blob/${latestVersion}/dist`
|
|
100
|
-
|
|
101
|
-
if (versionNumber > 2)
|
|
102
|
-
url = `https://github.com/learnpack/ide/blob/master/bin/learnpack-${latestVersion}.tar.gz`
|
|
103
|
-
|
|
104
|
-
const resp2 = await fetch(url, { method: "HEAD" })
|
|
105
|
-
if (!resp2.ok)
|
|
65
|
+
const url = editorTarballUrl(release.version, release.source)
|
|
66
|
+
const resp = await fetch(url, { method: "HEAD", timeout: FETCH_TIMEOUT_MS })
|
|
67
|
+
if (!resp.ok)
|
|
106
68
|
throw InternalError(
|
|
107
|
-
`Coding Editor ${
|
|
69
|
+
`Coding Editor ${release.version} was not found on ${release.source}, check the config.editor.version property on learn.json`
|
|
108
70
|
)
|
|
109
71
|
|
|
110
|
-
|
|
111
|
-
|
|
72
|
+
Console.debug(`Using IDE ${release.version} from ${release.source}`)
|
|
112
73
|
return download(url, destination)
|
|
113
74
|
}
|
|
114
75
|
|
|
@@ -123,7 +84,7 @@ export const download = (url: string, dest: string) => {
|
|
|
123
84
|
})
|
|
124
85
|
file.on("error", err => {
|
|
125
86
|
file.close()
|
|
126
|
-
if (err.
|
|
87
|
+
if ((err as NodeJS.ErrnoException).code === "EEXIST") {
|
|
127
88
|
Console.debug("File already exists")
|
|
128
89
|
resolve("File already exists")
|
|
129
90
|
} else {
|
|
@@ -132,11 +93,10 @@ export const download = (url: string, dest: string) => {
|
|
|
132
93
|
}
|
|
133
94
|
})
|
|
134
95
|
response.pipe(file)
|
|
135
|
-
} else if (
|
|
136
|
-
// Console.debug("Servers redirected to "+response.headers.location)
|
|
96
|
+
} else if ([301, 302, 303, 307, 308].includes(response.statusCode || 0)) {
|
|
137
97
|
// Recursively follow redirects, only a 200 will resolve.
|
|
138
98
|
if (response.headers.location) {
|
|
139
|
-
download(response.headers.location, dest)
|
|
99
|
+
download(new URL(response.headers.location, url).toString(), dest)
|
|
140
100
|
.then(() => resolve(/* */ ""))
|
|
141
101
|
.catch(error => {
|
|
142
102
|
Console.error(error)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import Console from "../utils/console"
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line
|
|
4
|
+
const fetch = require("node-fetch");
|
|
5
|
+
|
|
6
|
+
// IDE tarballs published from 5.0.417 on live on CloudFront; older ones stay in the
|
|
7
|
+
// learnpack/ide GitHub repo, whose versions.json is frozen. Both lists are merged.
|
|
8
|
+
export const IDE_CLOUDFRONT_BASE =
|
|
9
|
+
"https://d3a7w3e3g8ydll.cloudfront.net/learnpack/ide"
|
|
10
|
+
export const IDE_CLOUDFRONT_MANIFEST = `${IDE_CLOUDFRONT_BASE}/versions.json`
|
|
11
|
+
export const IDE_GITHUB_MANIFEST =
|
|
12
|
+
"https://raw.githubusercontent.com/learnpack/ide/master/versions.json"
|
|
13
|
+
export const IDE_GITHUB_PACKAGE =
|
|
14
|
+
"https://raw.githubusercontent.com/learnpack/ide/master/package.json"
|
|
15
|
+
|
|
16
|
+
export const FETCH_TIMEOUT_MS = 10_000
|
|
17
|
+
const DEFAULT_EDITOR_VERSION = "5.0.0"
|
|
18
|
+
|
|
19
|
+
export type TEditorSource = "cloudfront" | "github";
|
|
20
|
+
|
|
21
|
+
export interface IEditorRelease {
|
|
22
|
+
version: string;
|
|
23
|
+
source: TEditorSource;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface IParsedVersion {
|
|
27
|
+
version: string;
|
|
28
|
+
major: number;
|
|
29
|
+
minor: number;
|
|
30
|
+
patch: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const parseVersion = (version: string): IParsedVersion | null => {
|
|
34
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim())
|
|
35
|
+
if (!match) return null
|
|
36
|
+
return {
|
|
37
|
+
version: match.slice(1, 4).join("."),
|
|
38
|
+
major: Number(match[1]),
|
|
39
|
+
minor: Number(match[2]),
|
|
40
|
+
patch: Number(match[3]),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// "learnpack-5.0.417.tar.gz" -> parsed 5.0.417; anything else -> null
|
|
45
|
+
export const parseTag = (tag: string): IParsedVersion | null => {
|
|
46
|
+
const match = /^learnpack-(\d+\.\d+\.\d+)\.tar\.gz$/.exec(tag)
|
|
47
|
+
return match ? parseVersion(match[1]) : null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const compareVersions = (a: IParsedVersion, b: IParsedVersion): number =>
|
|
51
|
+
a.major - b.major || a.minor - b.minor || a.patch - b.patch
|
|
52
|
+
|
|
53
|
+
const parseTags = (tags: string[] | null): IParsedVersion[] =>
|
|
54
|
+
(tags || []).map(parseTag).filter((v): v is IParsedVersion => v !== null)
|
|
55
|
+
|
|
56
|
+
// Highest version listed in a manifest, or null when there is none.
|
|
57
|
+
export const highestVersion = (tags: string[] | null): string | null => {
|
|
58
|
+
const sorted = parseTags(tags).sort(compareVersions)
|
|
59
|
+
return sorted.length > 0 ? sorted[sorted.length - 1].version : null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Picks the tarball to download for the requested version. Only major.minor of the
|
|
63
|
+
// request is used and the highest patch available wins, as the CLI always did.
|
|
64
|
+
// The source is CloudFront when the version is in its manifest, GitHub otherwise.
|
|
65
|
+
export const resolveEditorRelease = (
|
|
66
|
+
requested: string,
|
|
67
|
+
cloudTags: string[] | null,
|
|
68
|
+
githubTags: string[] | null
|
|
69
|
+
): IEditorRelease | null => {
|
|
70
|
+
const [major, minor] = requested.split(".").map(part => Number(part))
|
|
71
|
+
if (!Number.isInteger(major) || !Number.isInteger(minor)) return null
|
|
72
|
+
|
|
73
|
+
const cloud = parseTags(cloudTags)
|
|
74
|
+
const candidates = [...cloud, ...parseTags(githubTags)]
|
|
75
|
+
.filter(v => v.major === major && v.minor === minor)
|
|
76
|
+
.sort(compareVersions)
|
|
77
|
+
|
|
78
|
+
if (candidates.length === 0) return null
|
|
79
|
+
|
|
80
|
+
const { version } = candidates[candidates.length - 1]
|
|
81
|
+
const source: TEditorSource = cloud.some(v => v.version === version) ?
|
|
82
|
+
"cloudfront" :
|
|
83
|
+
"github"
|
|
84
|
+
|
|
85
|
+
return { version, source }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const editorTarballUrl = (
|
|
89
|
+
version: string,
|
|
90
|
+
source: TEditorSource
|
|
91
|
+
): string => {
|
|
92
|
+
if (source === "cloudfront") {
|
|
93
|
+
return `${IDE_CLOUDFRONT_BASE}/learnpack-${version}.tar.gz`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const major = Number.parseInt(version.split(".")[0], 10)
|
|
97
|
+
return major > 2 ?
|
|
98
|
+
`https://github.com/learnpack/ide/blob/master/bin/learnpack-${version}.tar.gz?raw=true` :
|
|
99
|
+
`https://github.com/learnpack/coding-ide/blob/${version}/dist/app.tar.gz?raw=true`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Fetches a versions manifest. Never throws: any failure returns null.
|
|
103
|
+
export const fetchTagList = async (url: string): Promise<string[] | null> => {
|
|
104
|
+
try {
|
|
105
|
+
const res = await fetch(url, { timeout: FETCH_TIMEOUT_MS })
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
Console.debug(`Could not read ${url}: ${res.status} ${res.statusText}`)
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const json = await res.json()
|
|
112
|
+
if (!Array.isArray(json) || !json.every(tag => typeof tag === "string")) {
|
|
113
|
+
Console.debug(`Invalid versions manifest format at ${url}`)
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return json
|
|
118
|
+
} catch (error) {
|
|
119
|
+
Console.debug(`Could not read ${url}`, error)
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Latest published IDE version: CloudFront manifest, then GitHub package.json, then a default.
|
|
125
|
+
export const getLatestEditorVersion = async (): Promise<string> => {
|
|
126
|
+
const fromCloud = highestVersion(await fetchTagList(IDE_CLOUDFRONT_MANIFEST))
|
|
127
|
+
if (fromCloud) return fromCloud
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const res = await fetch(IDE_GITHUB_PACKAGE, { timeout: FETCH_TIMEOUT_MS })
|
|
131
|
+
if (res.ok) {
|
|
132
|
+
const json = await res.json()
|
|
133
|
+
if (typeof json?.version === "string" && parseVersion(json.version)) {
|
|
134
|
+
return json.version
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
Console.debug("Could not read the IDE package.json from GitHub", error)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return DEFAULT_EDITOR_VERSION
|
|
142
|
+
}
|