@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.
@@ -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
- const resp = await fetch("https://raw.githubusercontent.com/learnpack/ide/master/package.json");
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) || "./";
@@ -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
- const res = await fetch("https://raw.githubusercontent.com/learnpack/ide/master/package.json");
35
- const json = await res.json();
36
- version = json.version || "4.0.0";
37
- if (!version)
38
- throw (0, errors_1.InternalError)(`Coding Editor version was not found on learnpack repository, check the config.editor.version property on learn.json`);
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 [major, minor] = version.split(".");
41
- const versionPrefix = `${major}.${minor}`;
42
- let tags;
43
- try {
44
- const tagsRes = await fetch("https://raw.githubusercontent.com/learnpack/ide/master/versions.json");
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.name === "EEXIST") {
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 (response.statusCode === 302 || response.statusCode === 301) {
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.365",
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 fetch = require("node-fetch")
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
- const resp = await fetch(
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(`Exercise ${slug} not found on the configuration`)
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
  }
@@ -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("Downloading the LearnPack coding UI, this may take a minute...")
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
- if (!version)
47
- throw InternalError(
48
- `Coding Editor version was not found on learnpack repository, check the config.editor.version property on learn.json`
49
- )
50
- }
52
+ const [cloudTags, githubTags] = await Promise.all([
53
+ fetchTagList(IDE_CLOUDFRONT_MANIFEST),
54
+ fetchTagList(IDE_GITHUB_MANIFEST),
55
+ ])
51
56
 
52
- const [major, minor] = version.split(".")
53
- const versionPrefix = `${major}.${minor}`
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
- "Error reading versions.json, defaulting to version 4.0.2",
68
- error
60
+ `No IDE version found for ${version} in the versions manifests, defaulting to version 4.0.2`
69
61
  )
70
- version = "4.0.2"
71
- tags = [`learnpack-${version}.tar.gz`]
62
+ release = { version: "4.0.2", source: "github" }
72
63
  }
73
64
 
74
- let matchingTags
75
- try {
76
- if (tags.length === 0 || !tags)
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 ${latestVersion} was not found on learnpack repository, check the config.editor.version property on learn.json`
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
- url += versionNumber > 2 ? `?raw=true` : `/app.tar.gz?raw=true`
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.name === "EEXIST") {
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 (response.statusCode === 302 || response.statusCode === 301) {
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
+ }