@learnpack/learnpack 5.0.353 → 5.0.355
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/commands/serve.js +1 -1
- package/lib/scripts/descriptionsS3Backfill.js +142 -22
- package/lib/utils/awsCredentials.d.ts +20 -0
- package/lib/utils/awsCredentials.js +43 -0
- package/lib/utils/descriptions/backfillEvents.d.ts +60 -0
- package/lib/utils/descriptions/backfillEvents.js +107 -0
- package/lib/utils/descriptions/generateCourseDescriptions.d.ts +7 -0
- package/lib/utils/descriptions/generateCourseDescriptions.js +3 -0
- package/lib/utils/descriptions/publishStage.js +14 -3
- package/lib/utils/descriptions/resumePublication.js +17 -2
- package/lib/utils/descriptions/s3Storage.js +13 -6
- package/lib/utils/packageManifest.d.ts +8 -0
- package/lib/utils/packageManifest.js +8 -0
- package/lib/utils/repair/legacyPackageRepair.d.ts +131 -0
- package/lib/utils/repair/legacyPackageRepair.js +492 -0
- package/lib/utils/repair/repairStorage.d.ts +68 -0
- package/lib/utils/repair/repairStorage.js +89 -0
- package/lib/utils/s3/packageManifestBackfill.js +3 -8
- package/lib/utils/s3/packageSourcesAudit.d.ts +75 -0
- package/lib/utils/s3/packageSourcesAudit.js +184 -0
- package/package.json +3 -1
- package/src/commands/serve.ts +1 -1
- package/src/scripts/README.md +244 -0
- package/src/scripts/descriptionsS3Backfill.ts +188 -20
- package/src/utils/awsCredentials.ts +57 -0
- package/src/utils/descriptions/backfillEvents.ts +152 -0
- package/src/utils/descriptions/generateCourseDescriptions.ts +10 -0
- package/src/utils/descriptions/publishStage.ts +394 -382
- package/src/utils/descriptions/resumePublication.ts +217 -200
- package/src/utils/descriptions/s3Storage.ts +214 -206
- package/src/utils/packageManifest.ts +8 -1
- package/src/utils/repair/legacyPackageRepair.ts +731 -0
- package/src/utils/repair/repairStorage.ts +168 -0
- package/src/utils/s3/packageManifestBackfill.ts +771 -776
- package/src/utils/s3/packageSourcesAudit.ts +311 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Bucket } from "@google-cloud/storage";
|
|
2
|
+
import { AwsClient } from "../s3/packageManifestBackfill";
|
|
3
|
+
/**
|
|
4
|
+
* The two buckets a package lives in, behind one interface.
|
|
5
|
+
*
|
|
6
|
+
* A legacy package is repaired **twice, independently** — once against the
|
|
7
|
+
* published snapshot in S3 and once against the draft in GCS — never by copying
|
|
8
|
+
* one into the other. The two hold different content (the draft keeps moving
|
|
9
|
+
* after a publication) and the synthesized structure is a projection of that
|
|
10
|
+
* content: a syllabus derived from the snapshot could name steps the draft does
|
|
11
|
+
* not have. This is the same rule `mirrorDescriptions` enforces with its content
|
|
12
|
+
* hash; deriving from each bucket needs no guard at all.
|
|
13
|
+
*
|
|
14
|
+
* What crosses between them is only the descriptions, through
|
|
15
|
+
* `descriptionsS3Backfill --mirror-draft`, which is hash-guarded per step.
|
|
16
|
+
*/
|
|
17
|
+
export type RepairTarget = "s3" | "gcs";
|
|
18
|
+
export declare const ALL_REPAIR_TARGETS: RepairTarget[];
|
|
19
|
+
/**
|
|
20
|
+
* Where each input lives. The two buckets do not agree on this, and the
|
|
21
|
+
* disagreement is load-bearing:
|
|
22
|
+
*
|
|
23
|
+
* - S3 reads `{slug}/config.json` first (`fetchPackageSources`), which is where
|
|
24
|
+
* the publish writes it into the zip.
|
|
25
|
+
* - GCS reads `courses/{slug}/.learn/config.json` — the publish route downloads
|
|
26
|
+
* that one before anything else, so a config written anywhere else in the
|
|
27
|
+
* draft would not make the package publishable.
|
|
28
|
+
*/
|
|
29
|
+
export type PackageLayout = {
|
|
30
|
+
learnJsonKey(slug: string): string;
|
|
31
|
+
/** Config locations in resolution order; the first is the write target. */
|
|
32
|
+
configKeys(slug: string): string[];
|
|
33
|
+
syllabusKey(slug: string): string;
|
|
34
|
+
/** Sidebar locations in resolution order; the first is the write target. */
|
|
35
|
+
sidebarKeys(slug: string): string[];
|
|
36
|
+
exercisesPrefix(slug: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Whether `config.json` is the authority on the exercise list, or the folder
|
|
39
|
+
* listing is.
|
|
40
|
+
*
|
|
41
|
+
* This decides the **language keys** of the synthesized syllabus, and the two
|
|
42
|
+
* buckets genuinely disagree:
|
|
43
|
+
*
|
|
44
|
+
* - S3 reads the published `config.json` (`fetchPackageSources`), where these
|
|
45
|
+
* legacy packages carry `us` for the unsuffixed README.
|
|
46
|
+
* - GCS has no such reader. Both the publish-time manifest and the
|
|
47
|
+
* descriptions pipeline go through `configBuilder.buildConfig`, which
|
|
48
|
+
* derives exercises from the listing and maps an unsuffixed `README.md` to
|
|
49
|
+
* `en` — regardless of what the draft's `.learn/config.json` says.
|
|
50
|
+
*
|
|
51
|
+
* A syllabus keyed the other way is not read at all: the manifest would look
|
|
52
|
+
* up `translations["en"]`, find nothing, and publish `description: null` for
|
|
53
|
+
* every step. So each side is built the way its own reader will read it.
|
|
54
|
+
*/
|
|
55
|
+
exercisesFromConfig: boolean;
|
|
56
|
+
};
|
|
57
|
+
export type RepairStorage = {
|
|
58
|
+
target: RepairTarget;
|
|
59
|
+
layout: PackageLayout;
|
|
60
|
+
readJson<T>(key: string): Promise<T | null>;
|
|
61
|
+
readText(key: string): Promise<string | null>;
|
|
62
|
+
listKeys(prefix: string): Promise<string[]>;
|
|
63
|
+
writeJson(key: string, value: unknown): Promise<void>;
|
|
64
|
+
};
|
|
65
|
+
export declare const s3Layout: PackageLayout;
|
|
66
|
+
export declare const gcsLayout: PackageLayout;
|
|
67
|
+
export declare function createS3RepairStorage(s3: AwsClient, bucket: string): RepairStorage;
|
|
68
|
+
export declare function createGcsRepairStorage(bucket: Bucket): RepairStorage;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.gcsLayout = exports.s3Layout = exports.ALL_REPAIR_TARGETS = void 0;
|
|
4
|
+
exports.createS3RepairStorage = createS3RepairStorage;
|
|
5
|
+
exports.createGcsRepairStorage = createGcsRepairStorage;
|
|
6
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
7
|
+
const packageManifestBackfill_1 = require("../s3/packageManifestBackfill");
|
|
8
|
+
exports.ALL_REPAIR_TARGETS = ["s3", "gcs"];
|
|
9
|
+
exports.s3Layout = {
|
|
10
|
+
learnJsonKey: slug => `${slug}/learn.json`,
|
|
11
|
+
configKeys: slug => [`${slug}/config.json`, `${slug}/.learn/config.json`],
|
|
12
|
+
syllabusKey: slug => `${slug}/.learn/initialSyllabus.json`,
|
|
13
|
+
// 25 of the published packages keep their sidebar at the root. It has to be
|
|
14
|
+
// resolved before writing: `fetchPackageSources` prefers the `.learn/` copy,
|
|
15
|
+
// so a synthesized one would shadow the published titles.
|
|
16
|
+
sidebarKeys: slug => [
|
|
17
|
+
`${slug}/.learn/sidebar.json`,
|
|
18
|
+
`${slug}/sidebar.json`,
|
|
19
|
+
],
|
|
20
|
+
exercisesPrefix: slug => `${slug}/exercises/`,
|
|
21
|
+
exercisesFromConfig: true,
|
|
22
|
+
};
|
|
23
|
+
exports.gcsLayout = {
|
|
24
|
+
learnJsonKey: slug => `courses/${slug}/learn.json`,
|
|
25
|
+
configKeys: slug => [
|
|
26
|
+
`courses/${slug}/.learn/config.json`,
|
|
27
|
+
`courses/${slug}/config.json`,
|
|
28
|
+
],
|
|
29
|
+
syllabusKey: slug => `courses/${slug}/.learn/initialSyllabus.json`,
|
|
30
|
+
sidebarKeys: slug => [`courses/${slug}/.learn/sidebar.json`],
|
|
31
|
+
exercisesPrefix: slug => `courses/${slug}/exercises/`,
|
|
32
|
+
exercisesFromConfig: false,
|
|
33
|
+
};
|
|
34
|
+
function createS3RepairStorage(s3, bucket) {
|
|
35
|
+
return {
|
|
36
|
+
target: "s3",
|
|
37
|
+
layout: exports.s3Layout,
|
|
38
|
+
readJson: key => (0, packageManifestBackfill_1.fetchJsonObject)(s3, bucket, key),
|
|
39
|
+
readText: key => (0, packageManifestBackfill_1.fetchTextObject)(s3, bucket, key),
|
|
40
|
+
listKeys: prefix => (0, packageManifestBackfill_1.listObjectKeys)(s3, bucket, prefix),
|
|
41
|
+
async writeJson(key, value) {
|
|
42
|
+
await (0, packageManifestBackfill_1.withRetry)(() => s3.send(new client_s3_1.PutObjectCommand({
|
|
43
|
+
Bucket: bucket,
|
|
44
|
+
Key: key,
|
|
45
|
+
Body: JSON.stringify(value, null, 2),
|
|
46
|
+
ContentType: "application/json",
|
|
47
|
+
})));
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function createGcsRepairStorage(bucket) {
|
|
52
|
+
const download = async (key) => {
|
|
53
|
+
try {
|
|
54
|
+
const [buf] = await bucket.file(key).download();
|
|
55
|
+
return buf.toString();
|
|
56
|
+
}
|
|
57
|
+
catch (_a) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
target: "gcs",
|
|
63
|
+
layout: exports.gcsLayout,
|
|
64
|
+
async readJson(key) {
|
|
65
|
+
const content = await download(key);
|
|
66
|
+
if (content === null) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(content);
|
|
71
|
+
}
|
|
72
|
+
catch (_a) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
readText: download,
|
|
77
|
+
async listKeys(prefix) {
|
|
78
|
+
const [files] = await bucket.getFiles({ prefix });
|
|
79
|
+
return files.map(file => file.name);
|
|
80
|
+
},
|
|
81
|
+
async writeJson(key, value) {
|
|
82
|
+
await bucket
|
|
83
|
+
.file(key)
|
|
84
|
+
.save(Buffer.from(JSON.stringify(value, null, 2), "utf8"), {
|
|
85
|
+
contentType: "application/json",
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -16,6 +16,7 @@ exports.getBatchExitCode = getBatchExitCode;
|
|
|
16
16
|
const tslib_1 = require("tslib");
|
|
17
17
|
const client_cloudfront_1 = require("@aws-sdk/client-cloudfront");
|
|
18
18
|
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
19
|
+
const awsCredentials_1 = require("../awsCredentials");
|
|
19
20
|
const packageManifest_1 = require("../packageManifest");
|
|
20
21
|
const README_FILENAME_PATTERN = /^readme(?:\.[a-z]{2})?\.md$/i;
|
|
21
22
|
const INDEX_HTML_PATTERN = /^[^/]+\/index\.html$/;
|
|
@@ -389,18 +390,12 @@ async function runBatch(options, clients) {
|
|
|
389
390
|
const s3 = (clients === null || clients === void 0 ? void 0 : clients.s3) ||
|
|
390
391
|
new client_s3_1.S3Client({
|
|
391
392
|
region: options.region,
|
|
392
|
-
credentials:
|
|
393
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID || "",
|
|
394
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "",
|
|
395
|
-
},
|
|
393
|
+
credentials: (0, awsCredentials_1.requireAwsCredentials)(),
|
|
396
394
|
});
|
|
397
395
|
const cf = (clients === null || clients === void 0 ? void 0 : clients.cf) ||
|
|
398
396
|
new client_cloudfront_1.CloudFrontClient({
|
|
399
397
|
region: "us-east-1",
|
|
400
|
-
credentials:
|
|
401
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID || "",
|
|
402
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "",
|
|
403
|
-
},
|
|
398
|
+
credentials: (0, awsCredentials_1.requireAwsCredentials)(),
|
|
404
399
|
});
|
|
405
400
|
let slugs = options.slug ?
|
|
406
401
|
[options.slug] :
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { AwsClient } from "./packageManifestBackfill";
|
|
2
|
+
/**
|
|
3
|
+
* Inventory of the files a published package needs for its
|
|
4
|
+
* `package-manifest.json` to be generated at full fidelity.
|
|
5
|
+
*
|
|
6
|
+
* This reads **key names only** — one paginated `ListObjectsV2` over the bucket,
|
|
7
|
+
* zero `GetObject`. Cheap enough to run over the whole catalogue on demand, and
|
|
8
|
+
* safe at any time: it never writes.
|
|
9
|
+
*
|
|
10
|
+
* What it answers: which packages would *fail* the manifest backfill (no
|
|
11
|
+
* `config.json`, no exercises), and which ones only produce a *degraded*
|
|
12
|
+
* manifest (no `initialSyllabus.json` → every lesson typed `READ`, ids derived
|
|
13
|
+
* from the folder name, and descriptions permanently `null`, because the
|
|
14
|
+
* descriptions pipeline builds its work list from the syllabus keys and cannot
|
|
15
|
+
* see a course that has none).
|
|
16
|
+
*
|
|
17
|
+
* Companion of `legacyPackageRepair.ts`, which writes what this reports missing.
|
|
18
|
+
*/
|
|
19
|
+
export type PackageSourcesAuditEntry = {
|
|
20
|
+
slug: string;
|
|
21
|
+
/** A published package is a prefix holding an `index.html` at its root. */
|
|
22
|
+
hasIndexHtml: boolean;
|
|
23
|
+
/** Required: without it the manifest backfill reports `failed`. */
|
|
24
|
+
hasConfigJson: boolean;
|
|
25
|
+
configJsonKey: string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Optional *as a key*: `config.json` may carry the same metadata under
|
|
28
|
+
* `config`. Only a `GetObject` could tell, so `false` here means "resolves
|
|
29
|
+
* through the fallback, or not at all" — not "broken".
|
|
30
|
+
*/
|
|
31
|
+
hasLearnJson: boolean;
|
|
32
|
+
hasSyllabus: boolean;
|
|
33
|
+
hasSidebar: boolean;
|
|
34
|
+
sidebarKey: string | null;
|
|
35
|
+
hasManifest: boolean;
|
|
36
|
+
/** Folders directly under `exercises/` holding at least one object. */
|
|
37
|
+
exerciseFolders: number;
|
|
38
|
+
/** `README.md` / `README.xx.md` files found in those folders. */
|
|
39
|
+
readmeCount: number;
|
|
40
|
+
/** Missing inputs, named as in the docs, for quick reporting. */
|
|
41
|
+
missing: string[];
|
|
42
|
+
/** True when the manifest backfill cannot produce a manifest at all. */
|
|
43
|
+
blocking: boolean;
|
|
44
|
+
};
|
|
45
|
+
export type PackageSourcesAuditSummary = {
|
|
46
|
+
missingConfigJson: number;
|
|
47
|
+
missingLearnJson: number;
|
|
48
|
+
missingSyllabus: number;
|
|
49
|
+
missingSidebar: number;
|
|
50
|
+
missingManifest: number;
|
|
51
|
+
noExerciseFolders: number;
|
|
52
|
+
blocking: number;
|
|
53
|
+
};
|
|
54
|
+
export type PackageSourcesAuditReport = {
|
|
55
|
+
generatedAt: string;
|
|
56
|
+
bucket: string;
|
|
57
|
+
scannedKeys: number;
|
|
58
|
+
totalPackages: number;
|
|
59
|
+
summary: PackageSourcesAuditSummary;
|
|
60
|
+
packages: PackageSourcesAuditEntry[];
|
|
61
|
+
};
|
|
62
|
+
export type AuditOptions = {
|
|
63
|
+
/** Restrict the scan to a single package (uses a prefixed listing). */
|
|
64
|
+
slug?: string;
|
|
65
|
+
/** Keep prefixes with no `index.html` (drafts, leftovers). Default false. */
|
|
66
|
+
includeUnpublished?: boolean;
|
|
67
|
+
};
|
|
68
|
+
export declare function summarize(packages: PackageSourcesAuditEntry[]): PackageSourcesAuditSummary;
|
|
69
|
+
/**
|
|
70
|
+
* One paginated pass over the bucket, folding each page into the accumulator.
|
|
71
|
+
*
|
|
72
|
+
* Deliberately not built on `listObjectKeys`: that one materializes every key in
|
|
73
|
+
* the catalogue before the caller sees the first one.
|
|
74
|
+
*/
|
|
75
|
+
export declare function auditPackageSources(s3: AwsClient, bucket: string, options?: AuditOptions): Promise<PackageSourcesAuditReport>;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.summarize = summarize;
|
|
4
|
+
exports.auditPackageSources = auditPackageSources;
|
|
5
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
6
|
+
const packageManifestBackfill_1 = require("./packageManifestBackfill");
|
|
7
|
+
const README_FILENAME_PATTERN = /^readme(?:\.[a-z]{2})?\.md$/i;
|
|
8
|
+
const ROOT_FILES = new Set([
|
|
9
|
+
"index.html",
|
|
10
|
+
"learn.json",
|
|
11
|
+
"config.json",
|
|
12
|
+
".learn/config.json",
|
|
13
|
+
".learn/initialSyllabus.json",
|
|
14
|
+
".learn/sidebar.json",
|
|
15
|
+
"sidebar.json",
|
|
16
|
+
".learn/package-manifest.json",
|
|
17
|
+
]);
|
|
18
|
+
function emptyAccumulator(slug) {
|
|
19
|
+
return {
|
|
20
|
+
slug,
|
|
21
|
+
hasIndexHtml: false,
|
|
22
|
+
configJsonKey: null,
|
|
23
|
+
hasLearnJson: false,
|
|
24
|
+
hasSyllabus: false,
|
|
25
|
+
sidebarKey: null,
|
|
26
|
+
hasManifest: false,
|
|
27
|
+
exerciseFolders: new Set(),
|
|
28
|
+
readmeCount: 0,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Fold one object key into the accumulator map. */
|
|
32
|
+
function classifyKey(entries, key) {
|
|
33
|
+
const separatorIndex = key.indexOf("/");
|
|
34
|
+
if (separatorIndex <= 0) {
|
|
35
|
+
// Object at the bucket root: not part of any package.
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const slug = key.slice(0, separatorIndex);
|
|
39
|
+
const rest = key.slice(separatorIndex + 1);
|
|
40
|
+
if (!rest) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const exerciseParts = rest.startsWith("exercises/") ? rest.split("/") : [];
|
|
44
|
+
// `exercises/<slug>/<file>`: shorter paths are the folder marker itself.
|
|
45
|
+
const exerciseSlug = exerciseParts.length >= 3 && exerciseParts[1] ? exerciseParts[1] : null;
|
|
46
|
+
if (!ROOT_FILES.has(rest) && !exerciseSlug) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
let entry = entries.get(slug);
|
|
50
|
+
if (!entry) {
|
|
51
|
+
entry = emptyAccumulator(slug);
|
|
52
|
+
entries.set(slug, entry);
|
|
53
|
+
}
|
|
54
|
+
if (exerciseSlug) {
|
|
55
|
+
entry.exerciseFolders.add(exerciseSlug);
|
|
56
|
+
const filename = exerciseParts[exerciseParts.length - 1];
|
|
57
|
+
if (exerciseParts.length === 3 && README_FILENAME_PATTERN.test(filename)) {
|
|
58
|
+
entry.readmeCount += 1;
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
switch (rest) {
|
|
63
|
+
case "index.html":
|
|
64
|
+
entry.hasIndexHtml = true;
|
|
65
|
+
break;
|
|
66
|
+
case "learn.json":
|
|
67
|
+
entry.hasLearnJson = true;
|
|
68
|
+
break;
|
|
69
|
+
// The root copy wins over `.learn/config.json`, matching the order
|
|
70
|
+
// `fetchPackageSources` resolves them in. Same for the sidebar, inverted.
|
|
71
|
+
case "config.json":
|
|
72
|
+
entry.configJsonKey = key;
|
|
73
|
+
break;
|
|
74
|
+
case ".learn/config.json":
|
|
75
|
+
entry.configJsonKey = entry.configJsonKey || key;
|
|
76
|
+
break;
|
|
77
|
+
case ".learn/sidebar.json":
|
|
78
|
+
entry.sidebarKey = key;
|
|
79
|
+
break;
|
|
80
|
+
case "sidebar.json":
|
|
81
|
+
entry.sidebarKey = entry.sidebarKey || key;
|
|
82
|
+
break;
|
|
83
|
+
case ".learn/initialSyllabus.json":
|
|
84
|
+
entry.hasSyllabus = true;
|
|
85
|
+
break;
|
|
86
|
+
case ".learn/package-manifest.json":
|
|
87
|
+
entry.hasManifest = true;
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function finalizeEntry(entry) {
|
|
94
|
+
const missing = [];
|
|
95
|
+
if (!entry.configJsonKey) {
|
|
96
|
+
missing.push("config.json");
|
|
97
|
+
}
|
|
98
|
+
if (!entry.hasLearnJson) {
|
|
99
|
+
missing.push("learn.json");
|
|
100
|
+
}
|
|
101
|
+
if (!entry.hasSyllabus) {
|
|
102
|
+
missing.push("initialSyllabus.json");
|
|
103
|
+
}
|
|
104
|
+
if (!entry.sidebarKey) {
|
|
105
|
+
missing.push("sidebar.json");
|
|
106
|
+
}
|
|
107
|
+
if (entry.exerciseFolders.size === 0) {
|
|
108
|
+
missing.push("exercises/");
|
|
109
|
+
}
|
|
110
|
+
if (!entry.hasManifest) {
|
|
111
|
+
missing.push("package-manifest.json");
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
slug: entry.slug,
|
|
115
|
+
hasIndexHtml: entry.hasIndexHtml,
|
|
116
|
+
hasConfigJson: Boolean(entry.configJsonKey),
|
|
117
|
+
configJsonKey: entry.configJsonKey,
|
|
118
|
+
hasLearnJson: entry.hasLearnJson,
|
|
119
|
+
hasSyllabus: entry.hasSyllabus,
|
|
120
|
+
hasSidebar: Boolean(entry.sidebarKey),
|
|
121
|
+
sidebarKey: entry.sidebarKey,
|
|
122
|
+
hasManifest: entry.hasManifest,
|
|
123
|
+
exerciseFolders: entry.exerciseFolders.size,
|
|
124
|
+
readmeCount: entry.readmeCount,
|
|
125
|
+
missing,
|
|
126
|
+
// A missing `learn.json` is not blocking on its own: `config.config` can
|
|
127
|
+
// stand in for it, and only a download would tell.
|
|
128
|
+
blocking: !entry.configJsonKey || entry.exerciseFolders.size === 0,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function count(packages, predicate) {
|
|
132
|
+
return packages.filter(entry => predicate(entry)).length;
|
|
133
|
+
}
|
|
134
|
+
function summarize(packages) {
|
|
135
|
+
return {
|
|
136
|
+
missingConfigJson: count(packages, entry => !entry.hasConfigJson),
|
|
137
|
+
missingLearnJson: count(packages, entry => !entry.hasLearnJson),
|
|
138
|
+
missingSyllabus: count(packages, entry => !entry.hasSyllabus),
|
|
139
|
+
missingSidebar: count(packages, entry => !entry.hasSidebar),
|
|
140
|
+
missingManifest: count(packages, entry => !entry.hasManifest),
|
|
141
|
+
noExerciseFolders: count(packages, entry => entry.exerciseFolders === 0),
|
|
142
|
+
blocking: count(packages, entry => entry.blocking),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* One paginated pass over the bucket, folding each page into the accumulator.
|
|
147
|
+
*
|
|
148
|
+
* Deliberately not built on `listObjectKeys`: that one materializes every key in
|
|
149
|
+
* the catalogue before the caller sees the first one.
|
|
150
|
+
*/
|
|
151
|
+
async function auditPackageSources(s3, bucket, options = {}) {
|
|
152
|
+
const entries = new Map();
|
|
153
|
+
const prefix = options.slug ? `${options.slug}/` : undefined;
|
|
154
|
+
let continuationToken;
|
|
155
|
+
let scannedKeys = 0;
|
|
156
|
+
do {
|
|
157
|
+
// eslint-disable-next-line no-await-in-loop -- S3 pagination is sequential
|
|
158
|
+
const response = (await (0, packageManifestBackfill_1.withRetry)(() => s3.send(new client_s3_1.ListObjectsV2Command({
|
|
159
|
+
Bucket: bucket,
|
|
160
|
+
Prefix: prefix,
|
|
161
|
+
ContinuationToken: continuationToken,
|
|
162
|
+
}))));
|
|
163
|
+
for (const item of response.Contents || []) {
|
|
164
|
+
if (!item.Key) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
scannedKeys += 1;
|
|
168
|
+
classifyKey(entries, item.Key);
|
|
169
|
+
}
|
|
170
|
+
continuationToken = response.NextContinuationToken;
|
|
171
|
+
} while (continuationToken);
|
|
172
|
+
const packages = [...entries.values()]
|
|
173
|
+
.filter(entry => options.includeUnpublished || entry.hasIndexHtml)
|
|
174
|
+
.map(entry => finalizeEntry(entry))
|
|
175
|
+
.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
176
|
+
return {
|
|
177
|
+
generatedAt: new Date().toISOString(),
|
|
178
|
+
bucket,
|
|
179
|
+
scannedKeys,
|
|
180
|
+
totalPackages: packages.length,
|
|
181
|
+
summary: summarize(packages),
|
|
182
|
+
packages,
|
|
183
|
+
};
|
|
184
|
+
}
|
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.355",
|
|
5
5
|
"author": "Alejandro Sanchez @alesanchezr",
|
|
6
6
|
"contributors": [
|
|
7
7
|
{
|
|
@@ -163,7 +163,9 @@
|
|
|
163
163
|
]
|
|
164
164
|
},
|
|
165
165
|
"scripts": {
|
|
166
|
+
"audit:package-sources": "ts-node --project ./tsconfig.json ./scripts/audit-package-sources.ts",
|
|
166
167
|
"backfill:package-manifest": "ts-node --project ./tsconfig.json ./scripts/backfill-package-manifest-s3.ts",
|
|
168
|
+
"repair:legacy-packages": "ts-node --project ./tsconfig.json ./scripts/repair-legacy-packages.ts",
|
|
167
169
|
"copy-assets": "npx cpy src/creatorDist/**/* lib/creatorDist --parents --verbose && npx cpy src/utils/templates/**/* lib/utils/templates --parents --verbose && npx cpy src/lua/**/* lib/lua --parents --verbose",
|
|
168
170
|
"tsc": "tsc -b",
|
|
169
171
|
"postpack": "rm -f oclif.manifest.json && eslint . --ext .ts --config .eslintrc",
|
package/src/commands/serve.ts
CHANGED
|
@@ -6731,7 +6731,7 @@ class ServeCommand extends SessionCommand {
|
|
|
6731
6731
|
|
|
6732
6732
|
const buffer = Buffer.from(content, "base64")
|
|
6733
6733
|
|
|
6734
|
-
if (isImageFile(item.relativePath)) {
|
|
6734
|
+
if (item.isAsset || isImageFile(item.relativePath)) {
|
|
6735
6735
|
// eslint-disable-next-line no-await-in-loop -- Sequential processing to avoid rate limits
|
|
6736
6736
|
await uploadBinaryToBucket(
|
|
6737
6737
|
bucket,
|