@learnpack/learnpack 5.0.357 → 5.0.359
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 +54 -170
- package/lib/commands/serve.js +145 -134
- package/lib/ui/static/og-image.png +0 -0
- 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/branding.d.ts +43 -0
- package/lib/utils/branding.js +75 -0
- package/lib/utils/creatorUtilities.d.ts +1 -0
- package/lib/utils/creatorUtilities.js +17 -1
- package/package.json +2 -2
- package/src/commands/publish.ts +63 -258
- package/src/commands/serve.ts +200 -162
- package/src/ui/_app/app.js +2072 -2072
- package/src/ui/app.tar.gz +0 -0
- package/src/ui/static/og-image.png +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/branding.ts +78 -0
- 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
package/lib/commands/serve.js
CHANGED
|
@@ -31,13 +31,15 @@ const misc_1 = require("../utils/misc");
|
|
|
31
31
|
const configBuilder_1 = require("../utils/configBuilder");
|
|
32
32
|
const creatorUtilities_2 = require("../utils/creatorUtilities");
|
|
33
33
|
const sidebarGenerator_1 = require("../utils/sidebarGenerator");
|
|
34
|
-
const
|
|
34
|
+
const assetResolver_1 = require("../utils/assetResolver");
|
|
35
|
+
const assetSync_1 = require("../utils/assetSync");
|
|
35
36
|
const export_1 = require("../utils/export");
|
|
36
37
|
const router_1 = require("../services/ingest/http/router");
|
|
37
38
|
const bucketIo_1 = require("../utils/coursePackage/bucketIo");
|
|
38
39
|
const learnJson_1 = require("../utils/coursePackage/learnJson");
|
|
39
40
|
const sidebar_1 = require("../utils/coursePackage/sidebar");
|
|
40
41
|
const packageManifest_1 = require("../utils/packageManifest");
|
|
42
|
+
const branding_1 = require("../utils/branding");
|
|
41
43
|
const gcsBucketName_1 = require("../utils/gcsBucketName");
|
|
42
44
|
const syllabusSync_1 = require("../utils/syllabusSync");
|
|
43
45
|
const publishJournal_1 = require("../utils/publishJournal");
|
|
@@ -318,94 +320,34 @@ const cleanFormState = (formState) => {
|
|
|
318
320
|
const cleanFormStateForSyllabus = (formState) => {
|
|
319
321
|
return Object.assign(Object.assign({}, formState), { description: formState.description, technologies: formState.technologies, contentIndex: formState.contentIndex, purposse: undefined, duration: undefined, hasContentIndex: undefined, variables: undefined, currentStep: undefined, language: undefined, isCompleted: undefined });
|
|
320
322
|
};
|
|
321
|
-
const getLocalizedValue = (translations, lang, fallbackLangs = ["en", "us"]) => {
|
|
322
|
-
if (!translations || typeof translations !== "object")
|
|
323
|
-
return "";
|
|
324
|
-
const direct = translations[lang];
|
|
325
|
-
if (typeof direct === "string" && direct.trim().length > 0)
|
|
326
|
-
return direct;
|
|
327
|
-
for (const fb of fallbackLangs) {
|
|
328
|
-
const v = translations[fb];
|
|
329
|
-
if (typeof v === "string" && v.trim().length > 0)
|
|
330
|
-
return v;
|
|
331
|
-
}
|
|
332
|
-
const firstKey = Object.keys(translations)[0];
|
|
333
|
-
const first = firstKey ? translations[firstKey] : "";
|
|
334
|
-
return typeof first === "string" ? first : "";
|
|
335
|
-
};
|
|
336
|
-
function assetSyncErrorDetail(err) {
|
|
337
|
-
if (typeof (err === null || err === void 0 ? void 0 : err.detail) === "string")
|
|
338
|
-
return err.detail;
|
|
339
|
-
if (typeof (err === null || err === void 0 ? void 0 : err.message) === "string")
|
|
340
|
-
return err.message;
|
|
341
|
-
try {
|
|
342
|
-
return JSON.stringify(err);
|
|
343
|
-
}
|
|
344
|
-
catch (_a) {
|
|
345
|
-
return String(err);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
323
|
const createMultiLangAsset = async (bucket, rigoToken, bcToken, courseSlug, courseJson, deployUrl, academyId) => {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (learnpackId === null) {
|
|
352
|
-
return {
|
|
353
|
-
errors: [
|
|
354
|
-
{
|
|
355
|
-
kind: "package_error",
|
|
356
|
-
error: {
|
|
357
|
-
detail: "Could not resolve Learnpack package id; assets not synced to Breathecode.",
|
|
358
|
-
},
|
|
359
|
-
},
|
|
360
|
-
],
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
const availableLangs = Object.keys(courseJson.title);
|
|
364
|
-
console.log("AVAILABLE LANGUAGES to upload asset", availableLangs);
|
|
365
|
-
const all_translations = [];
|
|
366
|
-
const errors = [];
|
|
367
|
-
for (const lang of availableLangs) {
|
|
368
|
-
// eslint-disable-next-line no-await-in-loop
|
|
369
|
-
const indexReadme = await bucket.file(`courses/${courseSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(lang)}`);
|
|
370
|
-
let indexReadmeString = "";
|
|
324
|
+
const readReadme = async (lang) => {
|
|
325
|
+
const indexReadme = bucket.file(`courses/${courseSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(lang)}`);
|
|
371
326
|
try {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
indexReadmeString = indexReadmeContent.toString();
|
|
327
|
+
const [content] = await indexReadme.download();
|
|
328
|
+
return content.toString();
|
|
375
329
|
}
|
|
376
330
|
catch (error) {
|
|
377
331
|
console.error("Error downloading index readme", error);
|
|
378
332
|
// TODO: Trigger generation of the index readme
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
const b64IndexReadme = buffer_1.Buffer.from(indexReadmeString).toString("base64");
|
|
382
|
-
try {
|
|
383
|
-
// eslint-disable-next-line no-await-in-loop
|
|
384
|
-
const asset = await (0, publish_1.handleAssetCreation)({ token: bcToken, rigobotToken: rigoToken.trim() }, courseJson, lang, deployUrl, b64IndexReadme, academyId, learnpackId, undefined, all_translations);
|
|
385
|
-
if (!asset) {
|
|
386
|
-
errors.push({
|
|
387
|
-
kind: "lang_error",
|
|
388
|
-
lang,
|
|
389
|
-
error: { detail: "Failed to create asset" },
|
|
390
|
-
});
|
|
391
|
-
console.log("No se pudo crear el asset, saltando idioma", lang);
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
|
-
all_translations.push(asset.slug);
|
|
395
|
-
}
|
|
396
|
-
catch (error) {
|
|
397
|
-
const errorData = error && typeof error === "object" && "response" in error ?
|
|
398
|
-
((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) || error :
|
|
399
|
-
error;
|
|
400
|
-
errors.push({
|
|
401
|
-
kind: "lang_error",
|
|
402
|
-
lang,
|
|
403
|
-
error: { detail: assetSyncErrorDetail(errorData) },
|
|
404
|
-
});
|
|
405
|
-
console.error(`Error creating asset for language ${lang}:`, error);
|
|
333
|
+
return "";
|
|
406
334
|
}
|
|
335
|
+
};
|
|
336
|
+
const result = await (0, assetSync_1.syncPackageAssets)({
|
|
337
|
+
bcToken,
|
|
338
|
+
rigoToken,
|
|
339
|
+
packageSlug: courseSlug,
|
|
340
|
+
learnJson: courseJson,
|
|
341
|
+
deployUrl,
|
|
342
|
+
academyId,
|
|
343
|
+
readReadme,
|
|
344
|
+
});
|
|
345
|
+
for (const warning of result.warnings)
|
|
346
|
+
console.warn("[assets]", warning);
|
|
347
|
+
for (const synced of result.synced) {
|
|
348
|
+
console.log(`[assets] ${synced.action} "${synced.lang}": #${synced.asset.id} ${synced.asset.slug}`);
|
|
407
349
|
}
|
|
408
|
-
return { errors };
|
|
350
|
+
return { errors: result.errors };
|
|
409
351
|
};
|
|
410
352
|
const lessonCleaner = (lesson) => {
|
|
411
353
|
return Object.assign(Object.assign({}, lesson), { duration: undefined, generated: undefined, status: undefined, translations: undefined, uid: undefined, initialContent: undefined, locked: undefined });
|
|
@@ -733,21 +675,88 @@ async function continueWithNextLesson(courseSlug, currentExerciseIndex, rigoToke
|
|
|
733
675
|
}) });
|
|
734
676
|
await saveSyllabus(courseSlug, newSyllabus, bucket);
|
|
735
677
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
678
|
+
/**
|
|
679
|
+
* The `og:image` of a published course.
|
|
680
|
+
*
|
|
681
|
+
* A published package is served from `{slug}.learn-pack.com`, so the course's
|
|
682
|
+
* own `preview.png` is the right card whenever it exists. This used to go
|
|
683
|
+
* through a helper that returned null for an empty preview, and both call sites
|
|
684
|
+
* passed an empty string — so every published course, preview or not, fell back
|
|
685
|
+
* to a shared SVG that no unfurl crawler can render.
|
|
686
|
+
*/
|
|
687
|
+
const resolveCoursePreviewUrl = async (bucket, slug, configuredPreview) => {
|
|
688
|
+
// `config` is parsed from a config.json in the bucket, so its declared types
|
|
689
|
+
// are an assertion rather than a guarantee: an old or hand-edited course can
|
|
690
|
+
// carry anything here, and a throw would fail the whole publication.
|
|
691
|
+
if (typeof configuredPreview === "string" &&
|
|
692
|
+
configuredPreview.startsWith("http")) {
|
|
693
|
+
return configuredPreview;
|
|
694
|
+
}
|
|
695
|
+
try {
|
|
696
|
+
const [exists] = await bucket.file(`courses/${slug}/preview.png`).exists();
|
|
697
|
+
if (exists) {
|
|
698
|
+
return (0, packageManifest_1.resolvePreviewUrl)(slug);
|
|
699
|
+
}
|
|
739
700
|
}
|
|
740
|
-
|
|
741
|
-
|
|
701
|
+
catch (error) {
|
|
702
|
+
console.error(`WARNING: could not check preview.png for ${slug}, using the brand card`, error);
|
|
742
703
|
}
|
|
743
|
-
|
|
744
|
-
return expectedUrl;
|
|
704
|
+
return (0, branding_1.getFallbackPreviewUrl)();
|
|
745
705
|
};
|
|
746
706
|
const getTitleFromHTML = (html) => {
|
|
747
707
|
const titleRegex = /<title>(.*?)<\/title>/;
|
|
748
708
|
const titleMatch = html.match(titleRegex);
|
|
749
709
|
return titleMatch ? titleMatch[1] : null;
|
|
750
710
|
};
|
|
711
|
+
const escapeHtmlAttribute = (value) => value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
712
|
+
/**
|
|
713
|
+
* The IDE shell served to draft courses, with its meta tags filled in.
|
|
714
|
+
*
|
|
715
|
+
* The shell ships with `{{title}}`, `{{description}}` and `{{preview}}`
|
|
716
|
+
* placeholders that only get substituted when a course is published. Drafts were
|
|
717
|
+
* served the raw template, so a link shared in Slack or WhatsApp unfurled to a
|
|
718
|
+
* literal `{{title}}` / `{{description}}`.
|
|
719
|
+
*/
|
|
720
|
+
let draftShellTemplate = null;
|
|
721
|
+
const loadDraftShellTemplate = () => {
|
|
722
|
+
const file = path.resolve(__dirname, "../ui/_app/index.html");
|
|
723
|
+
draftShellTemplate = fs
|
|
724
|
+
.readFileSync(file, "utf-8")
|
|
725
|
+
// A draft is not a real Course, so it must not emit Course structured data.
|
|
726
|
+
// The match is anchored to the typed opening tag and non-greedy on purpose:
|
|
727
|
+
// the shell also carries an inline <script> that loads the app bundle from
|
|
728
|
+
// CloudFront, and a looser pattern would swallow it and blank the page.
|
|
729
|
+
.replace(/[^\S\n]*<script type="application\/ld\+json">[\S\s]*?<\/script>\n?/, "");
|
|
730
|
+
return draftShellTemplate;
|
|
731
|
+
};
|
|
732
|
+
const renderDraftShell = (req) => {
|
|
733
|
+
const template = draftShellTemplate || loadDraftShellTemplate();
|
|
734
|
+
// Only the path: a preview URL can carry tokens in its query string, and
|
|
735
|
+
// og:url ends up in whatever crawler cache unfurls the link.
|
|
736
|
+
const canonicalUrl = escapeHtmlAttribute(`${(0, branding_1.getPublicHost)()}${req.path}`);
|
|
737
|
+
return (template
|
|
738
|
+
.replace(/<title>.*<\/title>/, `<title>${branding_1.DRAFT_PREVIEW_TITLE}</title>`)
|
|
739
|
+
.replace(/{{title}}/g, branding_1.DRAFT_PREVIEW_TITLE)
|
|
740
|
+
.replace(/{{description}}/g, branding_1.DRAFT_PREVIEW_DESCRIPTION)
|
|
741
|
+
.replace(/{{preview}}/g, (0, branding_1.getFallbackPreviewUrl)())
|
|
742
|
+
.replace("</head>", ` <meta property="og:url" content="${canonicalUrl}" />\n` +
|
|
743
|
+
` <meta name="robots" content="noindex, nofollow, noarchive" />\n </head>`)
|
|
744
|
+
// Safety net for placeholders a future shell may add: shipping an empty
|
|
745
|
+
// value to a crawler beats shipping "{{whatever}}".
|
|
746
|
+
.replace(/{{\w+}}/g, ""));
|
|
747
|
+
};
|
|
748
|
+
/**
|
|
749
|
+
* Drafts are unlisted work in progress. `noindex` goes out as both a meta tag
|
|
750
|
+
* and a header so it reaches crawlers that skip the markup; a `robots.txt`
|
|
751
|
+
* `Disallow` would be the wrong tool, since it blocks the fetch itself and
|
|
752
|
+
* would stop Slack and X from unfurling the link at all.
|
|
753
|
+
*/
|
|
754
|
+
const sendDraftShell = (req, res) => {
|
|
755
|
+
res
|
|
756
|
+
.type("html")
|
|
757
|
+
.set("X-Robots-Tag", "noindex, nofollow")
|
|
758
|
+
.send(renderDraftShell(req));
|
|
759
|
+
};
|
|
751
760
|
/** How often the notification is touched while a translation is running. */
|
|
752
761
|
const SYNC_HEARTBEAT_INTERVAL_MS = 60 * 1000;
|
|
753
762
|
/**
|
|
@@ -1045,6 +1054,10 @@ class ServeCommand extends SessionCommand_1.default {
|
|
|
1045
1054
|
await (0, file_1.decompress)(`${__dirname}/../ui/app.tar.gz`, `${__dirname}/../ui/_app/`);
|
|
1046
1055
|
const localAppPath = path.resolve(__dirname, "../ui/_app");
|
|
1047
1056
|
// app.use(express.static(localAppPath))
|
|
1057
|
+
// Warm the draft shell now that _app is on disk. Doing it here rather than
|
|
1058
|
+
// on first request means a corrupt tarball fails at boot, with a stack
|
|
1059
|
+
// trace, instead of on some user's preview link.
|
|
1060
|
+
loadDraftShellTemplate();
|
|
1048
1061
|
app.post("/upload", async (req, res) => {
|
|
1049
1062
|
const { content, destination } = req.body;
|
|
1050
1063
|
// console.log("UPLOAD", content, destination)
|
|
@@ -2160,8 +2173,7 @@ class ServeCommand extends SessionCommand_1.default {
|
|
|
2160
2173
|
console.log("GET /");
|
|
2161
2174
|
const slug = req.query.slug;
|
|
2162
2175
|
if (slug) {
|
|
2163
|
-
|
|
2164
|
-
res.sendFile(file);
|
|
2176
|
+
sendDraftShell(req, res);
|
|
2165
2177
|
}
|
|
2166
2178
|
else {
|
|
2167
2179
|
res.redirect("https://learnpack.co");
|
|
@@ -2169,12 +2181,16 @@ class ServeCommand extends SessionCommand_1.default {
|
|
|
2169
2181
|
});
|
|
2170
2182
|
app.get("/preview/:slug", async (req, res) => {
|
|
2171
2183
|
console.log("GET COURSE PREVIEW /");
|
|
2172
|
-
|
|
2173
|
-
res.sendFile(file);
|
|
2184
|
+
sendDraftShell(req, res);
|
|
2174
2185
|
});
|
|
2175
2186
|
app.get("/preview/:slug/webview", async (req, res) => {
|
|
2176
|
-
|
|
2177
|
-
|
|
2187
|
+
sendDraftShell(req, res);
|
|
2188
|
+
});
|
|
2189
|
+
// The brand card used as og:image by drafts, and as the last-resort
|
|
2190
|
+
// fallback by published courses without a preview of their own.
|
|
2191
|
+
app.get("/og-image.png", (req, res) => {
|
|
2192
|
+
res.set("Cache-Control", "public, max-age=86400");
|
|
2193
|
+
res.sendFile(path.resolve(__dirname, "../ui/static/og-image.png"));
|
|
2178
2194
|
});
|
|
2179
2195
|
app.get("/config", async (req, res) => {
|
|
2180
2196
|
const courseSlug = req.query.slug;
|
|
@@ -3638,52 +3654,47 @@ class ServeCommand extends SessionCommand_1.default {
|
|
|
3638
3654
|
error: "Authentication failed, missing breathecode token",
|
|
3639
3655
|
});
|
|
3640
3656
|
}
|
|
3657
|
+
// Older IDEs only send the Breathecode token; without the Rigobot
|
|
3658
|
+
// token the lookup falls back to the title-derived slugs.
|
|
3659
|
+
const rigoToken = req.header("x-rigo-token");
|
|
3641
3660
|
const configFile = await bucket.file(`courses/${slug}/.learn/config.json`);
|
|
3642
3661
|
const [configContent] = await configFile.download();
|
|
3643
3662
|
const configJson = JSON.parse(configContent.toString());
|
|
3644
3663
|
const { config } = configJson;
|
|
3645
|
-
const
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3664
|
+
const resolution = await (0, assetResolver_1.resolvePackageAssets)({
|
|
3665
|
+
bcToken,
|
|
3666
|
+
rigoToken,
|
|
3667
|
+
packageSlug: slug,
|
|
3668
|
+
learnJson: config,
|
|
3669
|
+
});
|
|
3670
|
+
for (const warning of resolution.warnings) {
|
|
3671
|
+
console.warn("[package-academy]", warning);
|
|
3672
|
+
}
|
|
3673
|
+
const isPublished = (0, assetResolver_1.hasPublishedAsset)(resolution);
|
|
3674
|
+
const academyMode = (0, assetResolver_1.determineAcademyMode)(resolution);
|
|
3675
|
+
const warnings = [...resolution.warnings];
|
|
3676
|
+
if (resolution.packageState === "unknown") {
|
|
3677
|
+
warnings.push(`${resolution.packageError}; existing assets could not be checked.`);
|
|
3678
|
+
}
|
|
3679
|
+
for (const [lang, entry] of resolution.byLang) {
|
|
3680
|
+
if (entry.status === "unknown") {
|
|
3681
|
+
warnings.push(`"${lang}": ${entry.error}`);
|
|
3682
|
+
}
|
|
3683
|
+
else if (entry.status === "ambiguous") {
|
|
3684
|
+
warnings.push(`"${lang}": ${entry.candidates.length} assets found (${entry.candidates
|
|
3685
|
+
.map(c => `#${c.id} ${c.slug}`)
|
|
3686
|
+
.join(", ")})`);
|
|
3665
3687
|
}
|
|
3666
3688
|
}
|
|
3667
|
-
const
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
let conflictAcademies;
|
|
3671
|
-
if (uniqueAcademies.length === 0) {
|
|
3672
|
-
mode = "select";
|
|
3673
|
-
}
|
|
3674
|
-
else if (uniqueAcademies.length === 1) {
|
|
3675
|
-
mode = "locked";
|
|
3676
|
-
lockedAcademyId = uniqueAcademies[0];
|
|
3677
|
-
}
|
|
3678
|
-
else {
|
|
3679
|
-
mode = "conflict";
|
|
3680
|
-
conflictAcademies = uniqueAcademies;
|
|
3681
|
-
}
|
|
3689
|
+
const mode = academyMode.type;
|
|
3690
|
+
const lockedAcademyId = academyMode.type === "locked" ? academyMode.academyId : undefined;
|
|
3691
|
+
const conflictAcademies = academyMode.type === "conflict" ? academyMode.academies : undefined;
|
|
3682
3692
|
return res.json({
|
|
3683
3693
|
isPublished,
|
|
3684
3694
|
mode,
|
|
3685
3695
|
lockedAcademyId,
|
|
3686
3696
|
conflictAcademies,
|
|
3697
|
+
warnings,
|
|
3687
3698
|
});
|
|
3688
3699
|
}
|
|
3689
3700
|
catch (error) {
|
|
@@ -3742,13 +3753,13 @@ class ServeCommand extends SessionCommand_1.default {
|
|
|
3742
3753
|
title = config.title[availableLangs[0]];
|
|
3743
3754
|
selectedLang = availableLangs[0];
|
|
3744
3755
|
}
|
|
3756
|
+
const previewUrl = await resolveCoursePreviewUrl(bucket, slug, config.preview);
|
|
3745
3757
|
const idxTpl = fs.readFileSync(path.join(uiSrc, "index.html"), "utf-8");
|
|
3746
3758
|
const idxHtml = idxTpl
|
|
3747
3759
|
.replace(/{{title}}/g, title)
|
|
3748
3760
|
.replace(/<title>.*<\/title>/, `<title>${title}</title>`)
|
|
3749
3761
|
.replace(/{{description}}/g, config.description[selectedLang])
|
|
3750
|
-
.replace(/{{preview}}/g,
|
|
3751
|
-
"https://raw.githubusercontent.com/learnpack/ide/master/public/learnpack.svg")
|
|
3762
|
+
.replace(/{{preview}}/g, previewUrl)
|
|
3752
3763
|
.replace(/{{slug}}/g, slug)
|
|
3753
3764
|
.replace(/{{duration}}/g, (0, misc_1.minutesToISO8601Duration)(config.duration));
|
|
3754
3765
|
fs.writeFileSync(path.join(buildRoot, "index.html"), idxHtml);
|
|
@@ -3778,9 +3789,9 @@ class ServeCommand extends SessionCommand_1.default {
|
|
|
3778
3789
|
// 8) Crear el config.json en buildRoot con lo que retorna buildConfig
|
|
3779
3790
|
const fullConfig = { config, exercises };
|
|
3780
3791
|
fullConfig.config.slug = slug;
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3792
|
+
// Same URL the meta tags got: this used to overwrite the real preview
|
|
3793
|
+
// of every published package with the unusable SVG fallback.
|
|
3794
|
+
fullConfig.config.preview = previewUrl;
|
|
3784
3795
|
fs.writeFileSync(path.join(buildRoot, "config.json"), JSON.stringify(fullConfig, null, 2), "utf-8");
|
|
3785
3796
|
// 9) Empaquetar en ZIP (solo contenido de buildRoot)
|
|
3786
3797
|
const zipName = `${slug}.zip`;
|
|
Binary file
|
package/lib/utils/api.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare const BREATHECODE_HOST = "https://breathecode.herokuapp.com";
|
|
1
2
|
/**
|
|
2
3
|
* Student telemetry, and publication events. Both share the endpoint: a body
|
|
3
4
|
* carrying a top-level `event` key is dispatched as an event, anything else
|
|
@@ -31,13 +32,30 @@ export type AssetSyncError = {
|
|
|
31
32
|
detail: string;
|
|
32
33
|
};
|
|
33
34
|
};
|
|
35
|
+
export type RigoPackageLookup = {
|
|
36
|
+
kind: "found";
|
|
37
|
+
id: number;
|
|
38
|
+
assetIds: number[];
|
|
39
|
+
} | {
|
|
40
|
+
kind: "missing";
|
|
41
|
+
} | {
|
|
42
|
+
kind: "error";
|
|
43
|
+
error: string;
|
|
44
|
+
};
|
|
34
45
|
/**
|
|
35
|
-
*
|
|
36
|
-
* @param
|
|
46
|
+
* Human-readable summary of a failed axios call, for warnings and error payloads.
|
|
47
|
+
* @param error - Whatever the request rejected with.
|
|
48
|
+
* @returns "HTTP <status>" when the server answered, the error message otherwise.
|
|
49
|
+
*/
|
|
50
|
+
export declare function describeRequestError(error: unknown): string;
|
|
51
|
+
/**
|
|
52
|
+
* GET a Rigobot package by slug. Distinguishes "no such package" from
|
|
53
|
+
* "could not check", so callers can fail closed on the latter.
|
|
54
|
+
* @param rigoToken Rigobot API token (`Token` header value).
|
|
37
55
|
* @param courseSlug Learnpack package slug used in the Rigobot URL path.
|
|
38
|
-
* @returns
|
|
56
|
+
* @returns The package id and its asset_ids, `missing` on 404, `error` otherwise (403 included: a package owned by somebody else is not ours to read).
|
|
39
57
|
*/
|
|
40
|
-
export declare function
|
|
58
|
+
export declare function getRigoPackage(rigoToken: string, courseSlug: string): Promise<RigoPackageLookup>;
|
|
41
59
|
type TAssetMissing = {
|
|
42
60
|
slug: string;
|
|
43
61
|
title: string;
|
|
@@ -57,10 +75,41 @@ type TAssetMissing = {
|
|
|
57
75
|
learnpack_id: number;
|
|
58
76
|
};
|
|
59
77
|
export declare const createAsset: (token: string, asset: TAssetMissing) => Promise<any>;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
78
|
+
/** The subset of a Breathecode asset the publish flow reads. */
|
|
79
|
+
export type BcAsset = {
|
|
80
|
+
id: number;
|
|
81
|
+
slug: string;
|
|
82
|
+
lang: string | null;
|
|
83
|
+
status: string | null;
|
|
84
|
+
learnpackId: number | null;
|
|
85
|
+
learnpackDeployUrl: string | null;
|
|
86
|
+
ownerId: number | null;
|
|
87
|
+
academyId: number | null;
|
|
88
|
+
};
|
|
89
|
+
export type AssetLookup = {
|
|
90
|
+
kind: "found";
|
|
91
|
+
asset: BcAsset;
|
|
92
|
+
} | {
|
|
93
|
+
kind: "missing";
|
|
94
|
+
} | {
|
|
95
|
+
kind: "error";
|
|
96
|
+
error: string;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* GET a Breathecode asset by numeric id. The endpoint applies no status or
|
|
100
|
+
* visibility filter for a single asset, so this is the stable anchor.
|
|
101
|
+
* @param token Breathecode token.
|
|
102
|
+
* @param assetId Asset primary key.
|
|
103
|
+
* @returns `found` with the asset, `missing` on 404, `error` when it could not be checked.
|
|
104
|
+
*/
|
|
105
|
+
export declare const getAssetById: (token: string, assetId: number) => Promise<AssetLookup>;
|
|
106
|
+
/**
|
|
107
|
+
* GET a Breathecode asset by slug.
|
|
108
|
+
* @param token Breathecode token.
|
|
109
|
+
* @param assetSlug Asset slug.
|
|
110
|
+
* @returns `found` with the asset, `missing` on 404, `error` when it could not be checked.
|
|
111
|
+
*/
|
|
112
|
+
export declare const getAssetBySlug: (token: string, assetSlug: string) => Promise<AssetLookup>;
|
|
64
113
|
type TTechnology = {
|
|
65
114
|
slug: string;
|
|
66
115
|
lang: string;
|
|
@@ -83,18 +132,16 @@ declare const _default: {
|
|
|
83
132
|
listUserAcademies: (breathecodeToken: string) => Promise<TAcademy[]>;
|
|
84
133
|
validateToken: (token: string) => Promise<any>;
|
|
85
134
|
createAsset: (token: string, asset: TAssetMissing) => Promise<any>;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}>;
|
|
90
|
-
updateAsset: (token: string, assetSlug: string, asset: Partial<TAssetMissing> & {
|
|
135
|
+
getAssetById: (token: string, assetId: number) => Promise<AssetLookup>;
|
|
136
|
+
getAssetBySlug: (token: string, assetSlug: string) => Promise<AssetLookup>;
|
|
137
|
+
updateAsset: (token: string, assetId: number, asset: Partial<TAssetMissing> & {
|
|
91
138
|
learnpack_id: number;
|
|
92
139
|
}) => Promise<any>;
|
|
93
|
-
|
|
140
|
+
getRigoPackage: typeof getRigoPackage;
|
|
94
141
|
getCategories: (token: string) => Promise<any>;
|
|
95
142
|
updateRigoPackage: (token: string, slug: string, updates: {
|
|
96
143
|
asset_id?: number;
|
|
97
|
-
|
|
144
|
+
asset_ids?: number[];
|
|
98
145
|
}) => Promise<any>;
|
|
99
146
|
createRigoPackage: (token: string, slug: string, config: any) => Promise<any>;
|
|
100
147
|
getCurrentTechnologies: () => TTechnology[];
|