@learnpack/learnpack 5.0.358 → 5.0.360
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/publish.d.ts +0 -10
- package/lib/commands/publish.js +52 -168
- package/lib/commands/serve.js +52 -116
- package/lib/creatorDist/assets/{index-zrPponAn.css → index-B7s5Nmq8.css} +59 -0
- package/lib/creatorDist/assets/{index-D6pmbMe9.js → index-GYxGB_X0.js} +22609 -22363
- package/lib/creatorDist/index.html +2 -2
- package/lib/utils/api.d.ts +62 -15
- package/lib/utils/api.js +124 -45
- package/lib/utils/assetResolver.d.ts +116 -0
- package/lib/utils/assetResolver.js +268 -0
- package/lib/utils/assetSync.d.ts +48 -0
- package/lib/utils/assetSync.js +267 -0
- package/lib/utils/creatorUtilities.d.ts +1 -0
- package/lib/utils/creatorUtilities.js +17 -1
- package/package.json +1 -1
- package/src/commands/publish.ts +61 -255
- package/src/commands/serve.ts +65 -140
- package/src/creator/src/App.test.tsx +338 -0
- package/src/creator/src/App.tsx +152 -26
- package/src/creator/src/components/CourseLanguagePicker.tsx +51 -0
- package/src/creator/src/components/LanguageDetectionModal.tsx +29 -9
- package/src/creator/src/components/StepWizard.tsx +11 -3
- package/src/creator/src/components/Uploader.test.tsx +65 -0
- package/src/creator/src/components/Uploader.tsx +9 -3
- package/src/creator/src/locales/en.json +7 -0
- package/src/creator/src/locales/es.json +7 -0
- package/src/creator/src/utils/courseLanguage.test.ts +215 -0
- package/src/creator/src/utils/courseLanguage.ts +173 -0
- package/src/creator/src/utils/creationRequest.test.ts +104 -0
- package/src/creator/src/utils/creationRequest.ts +63 -0
- package/src/creator/src/utils/store.ts +7 -0
- package/src/creatorDist/assets/{index-zrPponAn.css → index-B7s5Nmq8.css} +59 -0
- package/src/creatorDist/assets/{index-D6pmbMe9.js → index-GYxGB_X0.js} +22609 -22363
- package/src/creatorDist/index.html +2 -2
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +2105 -2105
- package/src/ui/_app/sw.js +13 -0
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/api.ts +157 -42
- package/src/utils/assetResolver.ts +368 -0
- package/src/utils/assetSync.ts +343 -0
- package/src/utils/awsCredentials.ts +57 -57
- package/src/utils/creatorUtilities.ts +16 -0
- package/src/utils/descriptions/publishStage.ts +394 -394
- package/src/utils/descriptions/resumePublication.ts +217 -217
- package/src/utils/descriptions/s3Storage.ts +214 -214
- package/src/utils/s3/packageManifestBackfill.ts +771 -771
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasPublishedAsset = exports.determineAcademyMode = exports.describeAmbiguity = exports.slugCandidatesForLang = exports.dedupeLangs = exports.getAvailableLangs = exports.canonicalLang = void 0;
|
|
4
|
+
exports.resolvePackageAssets = resolvePackageAssets;
|
|
5
|
+
/**
|
|
6
|
+
* Resolves which Breathecode assets already represent a Learnpack package,
|
|
7
|
+
* one per language, before publish decides whether to create or update.
|
|
8
|
+
*
|
|
9
|
+
* The anchor is the package's `asset_ids` in Rigobot, read back by id from
|
|
10
|
+
* Breathecode. The title-derived slug is only a fallback for packages that
|
|
11
|
+
* predate `asset_ids`; it is never the criterion for a package whose ids are
|
|
12
|
+
* known. Every "could not check" outcome is reported as such, never as "does
|
|
13
|
+
* not exist", so a network hiccup cannot turn into a duplicate asset.
|
|
14
|
+
*/
|
|
15
|
+
const api_1 = require("./api");
|
|
16
|
+
const creatorUtilities_1 = require("./creatorUtilities");
|
|
17
|
+
/**
|
|
18
|
+
* Breathecode stores "en" as "us"; both name the same language.
|
|
19
|
+
* @param lang - Language code as found in learn.json or on an asset.
|
|
20
|
+
* @returns The canonical code, or null when there is none.
|
|
21
|
+
*/
|
|
22
|
+
const canonicalLang = (lang) => {
|
|
23
|
+
if (typeof lang !== "string")
|
|
24
|
+
return null;
|
|
25
|
+
const lower = lang.trim().toLowerCase();
|
|
26
|
+
if (!lower)
|
|
27
|
+
return null;
|
|
28
|
+
return lower === "en" ? "us" : lower;
|
|
29
|
+
};
|
|
30
|
+
exports.canonicalLang = canonicalLang;
|
|
31
|
+
/**
|
|
32
|
+
* Language keys of learn.json.title, in declaration order.
|
|
33
|
+
* @param learnJson - Parsed learn.json.
|
|
34
|
+
* @returns Non-empty string keys of `title`.
|
|
35
|
+
*/
|
|
36
|
+
const getAvailableLangs = (learnJson) => {
|
|
37
|
+
const langs = Object.keys((learnJson === null || learnJson === void 0 ? void 0 : learnJson.title) || {});
|
|
38
|
+
return langs.filter(l => typeof l === "string" && l.length > 0);
|
|
39
|
+
};
|
|
40
|
+
exports.getAvailableLangs = getAvailableLangs;
|
|
41
|
+
/**
|
|
42
|
+
* Drops keys that name the same language ("en" and "us"), keeping "en" when
|
|
43
|
+
* both are present and the first one declared otherwise.
|
|
44
|
+
* @param langs - Language keys as declared in learn.json.
|
|
45
|
+
* @returns One key per canonical language.
|
|
46
|
+
*/
|
|
47
|
+
const dedupeLangs = (langs) => {
|
|
48
|
+
const byCanonical = new Map();
|
|
49
|
+
for (const lang of langs) {
|
|
50
|
+
const canonical = (0, exports.canonicalLang)(lang);
|
|
51
|
+
if (canonical === null)
|
|
52
|
+
continue;
|
|
53
|
+
const current = byCanonical.get(canonical);
|
|
54
|
+
if (current === undefined || (lang === "en" && current === "us")) {
|
|
55
|
+
byCanonical.set(canonical, lang);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return [...byCanonical.values()];
|
|
59
|
+
};
|
|
60
|
+
exports.dedupeLangs = dedupeLangs;
|
|
61
|
+
/**
|
|
62
|
+
* Slugs an asset for this language may have been created under.
|
|
63
|
+
*
|
|
64
|
+
* One per learn.json title naming this language (title.en and title.us can
|
|
65
|
+
* differ, and a historical asset came from whichever existed at the time),
|
|
66
|
+
* each with both suffixes when the language is "us"/"en".
|
|
67
|
+
* @param learnJson - Parsed learn.json.
|
|
68
|
+
* @param canonical - Canonical language.
|
|
69
|
+
* @returns Distinct candidate slugs, most likely first.
|
|
70
|
+
*/
|
|
71
|
+
const slugCandidatesForLang = (learnJson, canonical) => {
|
|
72
|
+
const suffixes = canonical === "us" ? ["en", "us"] : [canonical];
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const key of (0, exports.getAvailableLangs)(learnJson)) {
|
|
76
|
+
if ((0, exports.canonicalLang)(key) !== canonical)
|
|
77
|
+
continue;
|
|
78
|
+
const title = learnJson.title[key];
|
|
79
|
+
if (typeof title !== "string" || !title.trim())
|
|
80
|
+
continue;
|
|
81
|
+
for (const suffix of suffixes) {
|
|
82
|
+
const slug = (0, creatorUtilities_1.buildAssetSlug)(title, suffix);
|
|
83
|
+
if (!seen.has(slug)) {
|
|
84
|
+
seen.add(slug);
|
|
85
|
+
out.push(slug);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
};
|
|
91
|
+
exports.slugCandidatesForLang = slugCandidatesForLang;
|
|
92
|
+
const describeAsset = (asset) => {
|
|
93
|
+
var _a;
|
|
94
|
+
return `#${asset.id} ${asset.slug} (${(_a = asset.status) !== null && _a !== void 0 ? _a : "?"}${asset.academyId === null ? "" : `, academy ${asset.academyId}`})`;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Text for the error a language gets when more than one asset claims it.
|
|
98
|
+
* @param lang - Language key from learn.json.
|
|
99
|
+
* @param candidates - The competing assets.
|
|
100
|
+
* @returns An actionable message naming every candidate.
|
|
101
|
+
*/
|
|
102
|
+
const describeAmbiguity = (lang, candidates) => `Found ${candidates.length} Breathecode assets for language "${lang}": ${candidates
|
|
103
|
+
.map(describeAsset)
|
|
104
|
+
.join("; ")}. Nothing was created or updated for this language. Keep only the definitive ids in the Rigobot package (asset_ids) and publish again.`;
|
|
105
|
+
exports.describeAmbiguity = describeAmbiguity;
|
|
106
|
+
/**
|
|
107
|
+
* Resolves the existing Breathecode asset for every language of a package.
|
|
108
|
+
* @param input - Tokens, package slug and parsed learn.json.
|
|
109
|
+
* @returns Per-language resolution plus the package-level facts publish needs.
|
|
110
|
+
*/
|
|
111
|
+
async function resolvePackageAssets(input) {
|
|
112
|
+
var _a, _b, _c, _d;
|
|
113
|
+
const deps = (_a = input.deps) !== null && _a !== void 0 ? _a : api_1.default;
|
|
114
|
+
const warnings = [];
|
|
115
|
+
const langs = (0, exports.dedupeLangs)((0, exports.getAvailableLangs)(input.learnJson));
|
|
116
|
+
const byLang = new Map();
|
|
117
|
+
const unknownEverywhere = (error, learnpackId) => {
|
|
118
|
+
for (const lang of langs) {
|
|
119
|
+
byLang.set((0, exports.canonicalLang)(lang), { status: "unknown", error });
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
packageSlug: input.packageSlug,
|
|
123
|
+
learnpackId,
|
|
124
|
+
packageState: "unknown",
|
|
125
|
+
packageError: error,
|
|
126
|
+
knownAssetIds: [],
|
|
127
|
+
orphanIds: [],
|
|
128
|
+
byLang,
|
|
129
|
+
warnings,
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
// Step 0: the package in Rigobot, and the asset ids it already knows.
|
|
133
|
+
let learnpackId = null;
|
|
134
|
+
let previousIds = [];
|
|
135
|
+
if ((_b = input.rigoToken) === null || _b === void 0 ? void 0 : _b.trim()) {
|
|
136
|
+
const pkg = await deps.getRigoPackage(input.rigoToken, input.packageSlug);
|
|
137
|
+
if (pkg.kind === "error") {
|
|
138
|
+
return unknownEverywhere(`Could not read the Rigobot package "${input.packageSlug}": ${pkg.error}`, null);
|
|
139
|
+
}
|
|
140
|
+
if (pkg.kind === "found") {
|
|
141
|
+
learnpackId = pkg.id;
|
|
142
|
+
previousIds = [...new Set(pkg.assetIds)];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
warnings.push("No Rigobot token: existing assets are looked up by slug only.");
|
|
147
|
+
}
|
|
148
|
+
// Step 1: read every known id back from Breathecode.
|
|
149
|
+
const knownAssetIds = [];
|
|
150
|
+
const orphanIds = [];
|
|
151
|
+
const candidatesByLang = new Map();
|
|
152
|
+
for (const id of previousIds) {
|
|
153
|
+
// eslint-disable-next-line no-await-in-loop
|
|
154
|
+
const lookup = await deps.getAssetById(input.bcToken, id);
|
|
155
|
+
if (lookup.kind === "error") {
|
|
156
|
+
return unknownEverywhere(`Could not read Breathecode asset #${id}: ${lookup.error}`, learnpackId);
|
|
157
|
+
}
|
|
158
|
+
if (lookup.kind === "missing") {
|
|
159
|
+
warnings.push(`Asset #${id} listed in the Rigobot package no longer exists in Breathecode; dropped.`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const asset = lookup.asset;
|
|
163
|
+
if (asset.learnpackId !== null &&
|
|
164
|
+
learnpackId !== null &&
|
|
165
|
+
asset.learnpackId !== learnpackId) {
|
|
166
|
+
warnings.push(`Asset ${describeAsset(asset)} belongs to Learnpack package ${asset.learnpackId}, not ${learnpackId}; ignored.`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const canonical = (0, exports.canonicalLang)(asset.lang);
|
|
170
|
+
if (canonical === null) {
|
|
171
|
+
orphanIds.push(asset.id);
|
|
172
|
+
warnings.push(`Asset ${describeAsset(asset)} has no language; kept in the package but not used.`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
knownAssetIds.push(asset.id);
|
|
176
|
+
const group = (_c = candidatesByLang.get(canonical)) !== null && _c !== void 0 ? _c : [];
|
|
177
|
+
group.push(asset);
|
|
178
|
+
candidatesByLang.set(canonical, group);
|
|
179
|
+
}
|
|
180
|
+
// Step 2: languages with no known id fall back to the title-derived slugs.
|
|
181
|
+
for (const lang of langs) {
|
|
182
|
+
const canonical = (0, exports.canonicalLang)(lang);
|
|
183
|
+
const known = (_d = candidatesByLang.get(canonical)) !== null && _d !== void 0 ? _d : [];
|
|
184
|
+
if (known.length === 1) {
|
|
185
|
+
byLang.set(canonical, { status: "found", asset: known[0], via: "id" });
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (known.length > 1) {
|
|
189
|
+
byLang.set(canonical, { status: "ambiguous", candidates: known });
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const found = new Map();
|
|
193
|
+
let error = null;
|
|
194
|
+
for (const slug of (0, exports.slugCandidatesForLang)(input.learnJson, canonical)) {
|
|
195
|
+
// eslint-disable-next-line no-await-in-loop
|
|
196
|
+
const lookup = await deps.getAssetBySlug(input.bcToken, slug);
|
|
197
|
+
if (lookup.kind === "error") {
|
|
198
|
+
error = `Could not check Breathecode asset "${slug}": ${lookup.error}`;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
if (lookup.kind === "missing")
|
|
202
|
+
continue;
|
|
203
|
+
const asset = lookup.asset;
|
|
204
|
+
if (asset.learnpackId !== null &&
|
|
205
|
+
learnpackId !== null &&
|
|
206
|
+
asset.learnpackId !== learnpackId) {
|
|
207
|
+
warnings.push(`Asset ${describeAsset(asset)} matches the slug for "${lang}" but belongs to Learnpack package ${asset.learnpackId}; ignored.`);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
found.set(asset.id, asset);
|
|
211
|
+
}
|
|
212
|
+
if (error !== null) {
|
|
213
|
+
byLang.set(canonical, { status: "unknown", error });
|
|
214
|
+
}
|
|
215
|
+
else if (found.size === 0) {
|
|
216
|
+
byLang.set(canonical, { status: "missing" });
|
|
217
|
+
}
|
|
218
|
+
else if (found.size === 1) {
|
|
219
|
+
const asset = [...found.values()][0];
|
|
220
|
+
if (previousIds.length > 0) {
|
|
221
|
+
warnings.push(`Asset ${describeAsset(asset)} for "${lang}" was found by slug, not through the Rigobot package; it will be added to asset_ids.`);
|
|
222
|
+
}
|
|
223
|
+
byLang.set(canonical, { status: "found", asset, via: "slug" });
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
byLang.set(canonical, {
|
|
227
|
+
status: "ambiguous",
|
|
228
|
+
candidates: [...found.values()],
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
packageSlug: input.packageSlug,
|
|
234
|
+
learnpackId,
|
|
235
|
+
packageState: "ok",
|
|
236
|
+
knownAssetIds,
|
|
237
|
+
orphanIds,
|
|
238
|
+
byLang,
|
|
239
|
+
warnings,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Academy handling for the publish prompt, from the assets that were found.
|
|
244
|
+
* @param resolution - Output of resolvePackageAssets.
|
|
245
|
+
* @returns `select` when no found asset has an academy, `locked` when they all share one, `conflict` otherwise.
|
|
246
|
+
*/
|
|
247
|
+
const determineAcademyMode = (resolution) => {
|
|
248
|
+
const academyIds = [];
|
|
249
|
+
for (const entry of resolution.byLang.values()) {
|
|
250
|
+
if (entry.status === "found" && entry.asset.academyId !== null) {
|
|
251
|
+
academyIds.push(entry.asset.academyId);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const unique = [...new Set(academyIds)];
|
|
255
|
+
if (unique.length === 0)
|
|
256
|
+
return { type: "select" };
|
|
257
|
+
if (unique.length === 1)
|
|
258
|
+
return { type: "locked", academyId: unique[0] };
|
|
259
|
+
return { type: "conflict", academies: unique };
|
|
260
|
+
};
|
|
261
|
+
exports.determineAcademyMode = determineAcademyMode;
|
|
262
|
+
/**
|
|
263
|
+
* Whether at least one language already has an asset.
|
|
264
|
+
* @param resolution - Output of resolvePackageAssets.
|
|
265
|
+
* @returns True when any language resolved to `found`.
|
|
266
|
+
*/
|
|
267
|
+
const hasPublishedAsset = (resolution) => [...resolution.byLang.values()].some(entry => entry.status === "found");
|
|
268
|
+
exports.hasPublishedAsset = hasPublishedAsset;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates or updates the Breathecode assets of a published package, one per
|
|
3
|
+
* language, and writes the resulting ids back to the Rigobot package.
|
|
4
|
+
*
|
|
5
|
+
* Kept free of fs, oclif and express so the whole decision table can be
|
|
6
|
+
* exercised with HTTP mocks alone; `publish` and `serve` only supply the
|
|
7
|
+
* readme of each language and the tokens.
|
|
8
|
+
*/
|
|
9
|
+
import { AssetSyncError } from "./api";
|
|
10
|
+
import { PackageAssetsResolution } from "./assetResolver";
|
|
11
|
+
export type AssetSyncInput = {
|
|
12
|
+
bcToken: string;
|
|
13
|
+
rigoToken: string;
|
|
14
|
+
packageSlug: string;
|
|
15
|
+
learnJson: any;
|
|
16
|
+
deployUrl: string;
|
|
17
|
+
academyId?: number;
|
|
18
|
+
/** Raw readme for a language ("" when there is none). Base64 encoding happens here. */
|
|
19
|
+
readReadme: (lang: string) => Promise<string>;
|
|
20
|
+
/** A resolution computed earlier (e.g. before the upload) to reuse instead of resolving again. */
|
|
21
|
+
resolution?: PackageAssetsResolution;
|
|
22
|
+
};
|
|
23
|
+
export type SyncedAsset = {
|
|
24
|
+
lang: string;
|
|
25
|
+
action: "created" | "updated";
|
|
26
|
+
asset: {
|
|
27
|
+
id: number;
|
|
28
|
+
slug: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
export type AssetSyncResult = {
|
|
32
|
+
errors: AssetSyncError[];
|
|
33
|
+
warnings: string[];
|
|
34
|
+
synced: SyncedAsset[];
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Localized value with the same fallbacks the publish flow always used.
|
|
38
|
+
* @param translations - A learn.json map such as `title` or `description`.
|
|
39
|
+
* @param lang - Preferred language key.
|
|
40
|
+
* @returns The value for `lang`, else "en"/"us", else the first one, else "".
|
|
41
|
+
*/
|
|
42
|
+
export declare const getLocalizedValue: (translations: Record<string, any> | undefined, lang: string) => string;
|
|
43
|
+
/**
|
|
44
|
+
* Synchronises every language of a package with Breathecode.
|
|
45
|
+
* @param input - Tokens, package data, deploy url and readme provider.
|
|
46
|
+
* @returns Per-language errors (the publish itself is never blocked), warnings worth logging, and what was written.
|
|
47
|
+
*/
|
|
48
|
+
export declare function syncPackageAssets(input: AssetSyncInput): Promise<AssetSyncResult>;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getLocalizedValue = void 0;
|
|
4
|
+
exports.syncPackageAssets = syncPackageAssets;
|
|
5
|
+
/**
|
|
6
|
+
* Creates or updates the Breathecode assets of a published package, one per
|
|
7
|
+
* language, and writes the resulting ids back to the Rigobot package.
|
|
8
|
+
*
|
|
9
|
+
* Kept free of fs, oclif and express so the whole decision table can be
|
|
10
|
+
* exercised with HTTP mocks alone; `publish` and `serve` only supply the
|
|
11
|
+
* readme of each language and the tokens.
|
|
12
|
+
*/
|
|
13
|
+
const api_1 = require("./api");
|
|
14
|
+
const console_1 = require("./console");
|
|
15
|
+
const creatorUtilities_1 = require("./creatorUtilities");
|
|
16
|
+
const assetResolver_1 = require("./assetResolver");
|
|
17
|
+
/**
|
|
18
|
+
* Localized value with the same fallbacks the publish flow always used.
|
|
19
|
+
* @param translations - A learn.json map such as `title` or `description`.
|
|
20
|
+
* @param lang - Preferred language key.
|
|
21
|
+
* @returns The value for `lang`, else "en"/"us", else the first one, else "".
|
|
22
|
+
*/
|
|
23
|
+
const getLocalizedValue = (translations, lang) => {
|
|
24
|
+
if (!translations || typeof translations !== "object")
|
|
25
|
+
return "";
|
|
26
|
+
const direct = translations[lang];
|
|
27
|
+
if (typeof direct === "string" && direct.trim().length > 0)
|
|
28
|
+
return direct;
|
|
29
|
+
for (const fb of ["en", "us"]) {
|
|
30
|
+
const v = translations[fb];
|
|
31
|
+
if (typeof v === "string" && v.trim().length > 0)
|
|
32
|
+
return v;
|
|
33
|
+
}
|
|
34
|
+
const firstKey = Object.keys(translations)[0];
|
|
35
|
+
const first = firstKey ? translations[firstKey] : "";
|
|
36
|
+
return typeof first === "string" ? first : "";
|
|
37
|
+
};
|
|
38
|
+
exports.getLocalizedValue = getLocalizedValue;
|
|
39
|
+
const packageError = (detail) => ({
|
|
40
|
+
kind: "package_error",
|
|
41
|
+
error: { detail },
|
|
42
|
+
});
|
|
43
|
+
const langError = (lang, detail) => ({
|
|
44
|
+
kind: "lang_error",
|
|
45
|
+
lang,
|
|
46
|
+
error: { detail },
|
|
47
|
+
});
|
|
48
|
+
const errorDetail = (error) => {
|
|
49
|
+
const err = error;
|
|
50
|
+
if (typeof (err === null || err === void 0 ? void 0 : err.detail) === "string")
|
|
51
|
+
return err.detail;
|
|
52
|
+
if (typeof (err === null || err === void 0 ? void 0 : err.message) === "string")
|
|
53
|
+
return err.message;
|
|
54
|
+
try {
|
|
55
|
+
return JSON.stringify(err);
|
|
56
|
+
}
|
|
57
|
+
catch (_a) {
|
|
58
|
+
return String(err);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Synchronises every language of a package with Breathecode.
|
|
63
|
+
* @param input - Tokens, package data, deploy url and readme provider.
|
|
64
|
+
* @returns Per-language errors (the publish itself is never blocked), warnings worth logging, and what was written.
|
|
65
|
+
*/
|
|
66
|
+
async function syncPackageAssets(input) {
|
|
67
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
68
|
+
const warnings = [];
|
|
69
|
+
const errors = [];
|
|
70
|
+
const synced = [];
|
|
71
|
+
const user = await api_1.default.validateToken(input.bcToken);
|
|
72
|
+
if (!user || typeof user.id !== "number") {
|
|
73
|
+
return {
|
|
74
|
+
errors: [packageError("Invalid Breathecode token; assets not synced.")],
|
|
75
|
+
warnings,
|
|
76
|
+
synced,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const resolution = (_a = input.resolution) !== null && _a !== void 0 ? _a : (await (0, assetResolver_1.resolvePackageAssets)({
|
|
80
|
+
bcToken: input.bcToken,
|
|
81
|
+
rigoToken: input.rigoToken,
|
|
82
|
+
packageSlug: input.packageSlug,
|
|
83
|
+
learnJson: input.learnJson,
|
|
84
|
+
}));
|
|
85
|
+
warnings.push(...resolution.warnings);
|
|
86
|
+
if (resolution.packageState === "unknown") {
|
|
87
|
+
return {
|
|
88
|
+
errors: [
|
|
89
|
+
packageError(`${(_b = resolution.packageError) !== null && _b !== void 0 ? _b : "Existing assets could not be checked"}; nothing was created or updated to avoid duplicates.`),
|
|
90
|
+
],
|
|
91
|
+
warnings,
|
|
92
|
+
synced,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const learnpackId = resolution.learnpackId;
|
|
96
|
+
if (learnpackId === null) {
|
|
97
|
+
return {
|
|
98
|
+
errors: [
|
|
99
|
+
packageError("Could not resolve Learnpack package id; assets not synced to Breathecode."),
|
|
100
|
+
],
|
|
101
|
+
warnings,
|
|
102
|
+
synced,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Decide every language before touching anything, so all_translations can
|
|
106
|
+
// name the full set up front (the PUT replaces the relation, it does not add).
|
|
107
|
+
const langs = (0, assetResolver_1.dedupeLangs)((0, assetResolver_1.getAvailableLangs)(input.learnJson));
|
|
108
|
+
const plans = [];
|
|
109
|
+
for (const lang of langs) {
|
|
110
|
+
const title = (0, exports.getLocalizedValue)((_c = input.learnJson) === null || _c === void 0 ? void 0 : _c.title, lang);
|
|
111
|
+
if (!title) {
|
|
112
|
+
plans.push({
|
|
113
|
+
lang,
|
|
114
|
+
kind: "skip",
|
|
115
|
+
detail: `Missing learn.json title for language "${lang}"`,
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const entry = resolution.byLang.get((0, assetResolver_1.canonicalLang)(lang));
|
|
120
|
+
if (!entry || entry.status === "unknown") {
|
|
121
|
+
plans.push({
|
|
122
|
+
lang,
|
|
123
|
+
kind: "skip",
|
|
124
|
+
detail: `${(entry === null || entry === void 0 ? void 0 : entry.status) === "unknown" ?
|
|
125
|
+
entry.error :
|
|
126
|
+
"Existing asset could not be checked"}; nothing was created or updated for this language to avoid duplicates.`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
else if (entry.status === "ambiguous") {
|
|
130
|
+
plans.push({
|
|
131
|
+
lang,
|
|
132
|
+
kind: "skip",
|
|
133
|
+
detail: (0, assetResolver_1.describeAmbiguity)(lang, entry.candidates),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
else if (entry.status === "found") {
|
|
137
|
+
if (entry.asset.ownerId !== null && entry.asset.ownerId !== user.id) {
|
|
138
|
+
plans.push({
|
|
139
|
+
lang,
|
|
140
|
+
kind: "skip",
|
|
141
|
+
detail: `Asset #${entry.asset.id} ${entry.asset.slug} belongs to another user (owner ${entry.asset.ownerId}); it was not updated and no duplicate was created.`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
plans.push({
|
|
146
|
+
lang,
|
|
147
|
+
kind: "update",
|
|
148
|
+
asset: entry.asset,
|
|
149
|
+
slug: entry.asset.slug,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
plans.push({ lang, kind: "create", slug: (0, creatorUtilities_1.buildAssetSlug)(title, lang) });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const allTranslations = plans
|
|
158
|
+
.filter(p => p.kind !== "skip")
|
|
159
|
+
.map(p => p.slug);
|
|
160
|
+
const rigoToken = input.rigoToken.trim();
|
|
161
|
+
const createdOrUpdatedIds = [];
|
|
162
|
+
for (const plan of plans) {
|
|
163
|
+
if (plan.kind === "skip") {
|
|
164
|
+
errors.push(langError(plan.lang, plan.detail));
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const title = (0, exports.getLocalizedValue)((_d = input.learnJson) === null || _d === void 0 ? void 0 : _d.title, plan.lang);
|
|
168
|
+
const description = (0, exports.getLocalizedValue)((_e = input.learnJson) === null || _e === void 0 ? void 0 : _e.description, plan.lang);
|
|
169
|
+
try {
|
|
170
|
+
if (plan.kind === "update") {
|
|
171
|
+
// Never send lang or slug: the language of an existing asset is not
|
|
172
|
+
// ours to change, and a slug would rename it.
|
|
173
|
+
const updatePayload = {
|
|
174
|
+
graded: true,
|
|
175
|
+
learnpack_deploy_url: input.deployUrl,
|
|
176
|
+
title,
|
|
177
|
+
category: "uncategorized",
|
|
178
|
+
description,
|
|
179
|
+
all_translations: allTranslations,
|
|
180
|
+
learnpack_id: learnpackId,
|
|
181
|
+
};
|
|
182
|
+
if (plan.asset.academyId === null && input.academyId !== undefined) {
|
|
183
|
+
updatePayload.academy_id = input.academyId;
|
|
184
|
+
}
|
|
185
|
+
console_1.default.info(`Asset exists for "${plan.lang}" (#${plan.asset.id} ${plan.slug}), updating it`);
|
|
186
|
+
// eslint-disable-next-line no-await-in-loop
|
|
187
|
+
const asset = await api_1.default.updateAsset(input.bcToken, plan.asset.id, updatePayload);
|
|
188
|
+
const id = typeof (asset === null || asset === void 0 ? void 0 : asset.id) === "number" ? asset.id : plan.asset.id;
|
|
189
|
+
createdOrUpdatedIds.push(id);
|
|
190
|
+
synced.push({
|
|
191
|
+
lang: plan.lang,
|
|
192
|
+
action: "updated",
|
|
193
|
+
asset: { id, slug: (_f = asset === null || asset === void 0 ? void 0 : asset.slug) !== null && _f !== void 0 ? _f : plan.slug },
|
|
194
|
+
});
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
// eslint-disable-next-line no-await-in-loop
|
|
198
|
+
const readme = await input.readReadme(plan.lang);
|
|
199
|
+
const assetPayload = {
|
|
200
|
+
slug: plan.slug,
|
|
201
|
+
title,
|
|
202
|
+
lang: plan.lang,
|
|
203
|
+
graded: true,
|
|
204
|
+
description,
|
|
205
|
+
learnpack_deploy_url: input.deployUrl,
|
|
206
|
+
technologies: [],
|
|
207
|
+
url: input.deployUrl,
|
|
208
|
+
category: "uncategorized",
|
|
209
|
+
owner: user.id,
|
|
210
|
+
author: user.id,
|
|
211
|
+
preview: (_g = input.learnJson) === null || _g === void 0 ? void 0 : _g.preview,
|
|
212
|
+
readme_raw: Buffer.from(readme).toString("base64"),
|
|
213
|
+
all_translations: allTranslations,
|
|
214
|
+
learnpack_id: learnpackId,
|
|
215
|
+
};
|
|
216
|
+
if (input.academyId !== undefined) {
|
|
217
|
+
assetPayload.academy_id = input.academyId;
|
|
218
|
+
}
|
|
219
|
+
console_1.default.info(`No asset for "${plan.lang}", creating ${plan.slug}`);
|
|
220
|
+
// eslint-disable-next-line no-await-in-loop
|
|
221
|
+
const asset = await api_1.default.createAsset(input.bcToken, assetPayload);
|
|
222
|
+
createdOrUpdatedIds.push(asset.id);
|
|
223
|
+
synced.push({
|
|
224
|
+
lang: plan.lang,
|
|
225
|
+
action: "created",
|
|
226
|
+
asset: { id: asset.id, slug: (_h = asset.slug) !== null && _h !== void 0 ? _h : plan.slug },
|
|
227
|
+
});
|
|
228
|
+
// Record the id right away: a stale Breathecode cache could hide the
|
|
229
|
+
// new slug from the next publish, but the id never lies.
|
|
230
|
+
try {
|
|
231
|
+
// eslint-disable-next-line no-await-in-loop
|
|
232
|
+
await api_1.default.updateRigoPackage(rigoToken, input.packageSlug, {
|
|
233
|
+
asset_id: asset.id,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
warnings.push(`Could not record asset #${asset.id} in the Rigobot package: ${errorDetail(error)}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
const errorData = error && typeof error === "object" && "response" in error ?
|
|
242
|
+
((_j = error.response) === null || _j === void 0 ? void 0 : _j.data) || error :
|
|
243
|
+
error;
|
|
244
|
+
console_1.default.error(`Error syncing asset for language ${plan.lang}:`, error);
|
|
245
|
+
errors.push(langError(plan.lang, errorDetail(errorData)));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Final list: everything Rigobot knew that still exists and is ours, plus
|
|
249
|
+
// whatever this run touched. Ids that 404 or belong to another package are
|
|
250
|
+
// the only ones that fall off; duplicates awaiting a manual decision stay.
|
|
251
|
+
const finalIds = [
|
|
252
|
+
...new Set([
|
|
253
|
+
...resolution.knownAssetIds,
|
|
254
|
+
...resolution.orphanIds,
|
|
255
|
+
...createdOrUpdatedIds,
|
|
256
|
+
]),
|
|
257
|
+
];
|
|
258
|
+
try {
|
|
259
|
+
await api_1.default.updateRigoPackage(rigoToken, input.packageSlug, {
|
|
260
|
+
asset_ids: finalIds,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
warnings.push(`Could not write asset_ids ${JSON.stringify(finalIds)} to the Rigobot package: ${errorDetail(error)}`);
|
|
265
|
+
}
|
|
266
|
+
return { errors, warnings, synced };
|
|
267
|
+
}
|
|
@@ -27,6 +27,7 @@ export declare function checkReadability(markdown: string, wordsPerMinute?: numb
|
|
|
27
27
|
fkglResult: TFKGLResult;
|
|
28
28
|
};
|
|
29
29
|
export declare const slugify: (text: string) => string;
|
|
30
|
+
export declare const buildAssetSlug: (title: string, lang: string) => string;
|
|
30
31
|
export declare const getExInfo: (title: string) => {
|
|
31
32
|
exNumber: string;
|
|
32
33
|
kind: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getReadmeExtension = exports.appendAIRules = exports.appendContentIndex = exports.saveTranslatedReadme = exports.makePackageInfo = exports.getExInfo = exports.slugify = exports.estimateReadingTime = void 0;
|
|
3
|
+
exports.getReadmeExtension = exports.appendAIRules = exports.appendContentIndex = exports.saveTranslatedReadme = exports.makePackageInfo = exports.getExInfo = exports.buildAssetSlug = exports.slugify = exports.estimateReadingTime = void 0;
|
|
4
4
|
exports.checkReadability = checkReadability;
|
|
5
5
|
exports.extractImagesFromMarkdown = extractImagesFromMarkdown;
|
|
6
6
|
exports.getFilenameFromUrl = getFilenameFromUrl;
|
|
@@ -98,6 +98,22 @@ const slugify = (text) => {
|
|
|
98
98
|
.replace(/^-+|-+$/g, ""); // Trim hyphens from start/end
|
|
99
99
|
};
|
|
100
100
|
exports.slugify = slugify;
|
|
101
|
+
// Builds the slug of the Breathecode asset for a given title and language.
|
|
102
|
+
// The backend only accepts letters, numbers, underscores and hyphens, so the
|
|
103
|
+
// periods that slugify() keeps (e.g. "Next.js") are turned into hyphens here.
|
|
104
|
+
// Hyphens are collapsed and trimmed BEFORE the 47-char cut on purpose: for a
|
|
105
|
+
// title without periods this must yield exactly slugify(title).slice(0, 47),
|
|
106
|
+
// which is the slug every existing asset was created with (a trailing hyphen
|
|
107
|
+
// left by the cut is part of that historical slug and is kept).
|
|
108
|
+
const buildAssetSlug = (title, lang) => {
|
|
109
|
+
const slug = (0, exports.slugify)(title)
|
|
110
|
+
.replace(/\./g, "-")
|
|
111
|
+
.replace(/-+/g, "-")
|
|
112
|
+
.replace(/^-+|-+$/g, "")
|
|
113
|
+
.slice(0, 47);
|
|
114
|
+
return `${slug}-${lang}`;
|
|
115
|
+
};
|
|
116
|
+
exports.buildAssetSlug = buildAssetSlug;
|
|
101
117
|
const getExInfo = (title) => {
|
|
102
118
|
// Example title: '1.0 - Introduction to AI [READ: Small introduction to important concepts such as AI, machine learning, and their applications]'
|
|
103
119
|
let [exNumber, exTitle] = title.split(" - ");
|
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.360",
|
|
5
5
|
"author": "Alejandro Sanchez @alesanchezr",
|
|
6
6
|
"contributors": [
|
|
7
7
|
{
|