@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/src/commands/serve.ts
CHANGED
|
@@ -55,9 +55,7 @@ import api, {
|
|
|
55
55
|
RIGOBOT_HOST,
|
|
56
56
|
RIGOBOT_REALTIME_HOST,
|
|
57
57
|
listUserAcademies,
|
|
58
|
-
doesAssetExists,
|
|
59
58
|
type AssetSyncError,
|
|
60
|
-
resolveLearnpackPackageId,
|
|
61
59
|
} from "../utils/api"
|
|
62
60
|
import {
|
|
63
61
|
createUploadMiddleware,
|
|
@@ -66,7 +64,12 @@ import {
|
|
|
66
64
|
import { buildConfig } from "../utils/configBuilder"
|
|
67
65
|
import { checkReadability, slugify } from "../utils/creatorUtilities"
|
|
68
66
|
import { checkAndFixSidebarPure } from "../utils/sidebarGenerator"
|
|
69
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
determineAcademyMode,
|
|
69
|
+
hasPublishedAsset,
|
|
70
|
+
resolvePackageAssets,
|
|
71
|
+
} from "../utils/assetResolver"
|
|
72
|
+
import { syncPackageAssets } from "../utils/assetSync"
|
|
70
73
|
import {
|
|
71
74
|
FormState,
|
|
72
75
|
Lesson,
|
|
@@ -81,7 +84,16 @@ import {
|
|
|
81
84
|
} from "../utils/coursePackage/bucketIo"
|
|
82
85
|
import { createLearnJson } from "../utils/coursePackage/learnJson"
|
|
83
86
|
import { createInitialSidebar } from "../utils/coursePackage/sidebar"
|
|
84
|
-
import {
|
|
87
|
+
import {
|
|
88
|
+
generateAndPersistPackageManifest,
|
|
89
|
+
resolvePreviewUrl,
|
|
90
|
+
} from "../utils/packageManifest"
|
|
91
|
+
import {
|
|
92
|
+
DRAFT_PREVIEW_DESCRIPTION,
|
|
93
|
+
DRAFT_PREVIEW_TITLE,
|
|
94
|
+
getFallbackPreviewUrl,
|
|
95
|
+
getPublicHost,
|
|
96
|
+
} from "../utils/branding"
|
|
85
97
|
import { requireGcsBucketName } from "../utils/gcsBucketName"
|
|
86
98
|
import {
|
|
87
99
|
createGcsSyllabusSyncStorage,
|
|
@@ -529,36 +541,6 @@ const cleanFormStateForSyllabus = (formState: FormState) => {
|
|
|
529
541
|
}
|
|
530
542
|
}
|
|
531
543
|
|
|
532
|
-
const getLocalizedValue = (
|
|
533
|
-
translations: Record<string, any> | undefined,
|
|
534
|
-
lang: string,
|
|
535
|
-
fallbackLangs: string[] = ["en", "us"]
|
|
536
|
-
): string => {
|
|
537
|
-
if (!translations || typeof translations !== "object") return ""
|
|
538
|
-
|
|
539
|
-
const direct = translations[lang]
|
|
540
|
-
if (typeof direct === "string" && direct.trim().length > 0) return direct
|
|
541
|
-
|
|
542
|
-
for (const fb of fallbackLangs) {
|
|
543
|
-
const v = translations[fb]
|
|
544
|
-
if (typeof v === "string" && v.trim().length > 0) return v
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const firstKey = Object.keys(translations)[0]
|
|
548
|
-
const first = firstKey ? translations[firstKey] : ""
|
|
549
|
-
return typeof first === "string" ? first : ""
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
function assetSyncErrorDetail(err: any): string {
|
|
553
|
-
if (typeof err?.detail === "string") return err.detail
|
|
554
|
-
if (typeof err?.message === "string") return err.message
|
|
555
|
-
try {
|
|
556
|
-
return JSON.stringify(err)
|
|
557
|
-
} catch {
|
|
558
|
-
return String(err)
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
544
|
const createMultiLangAsset = async (
|
|
563
545
|
bucket: Bucket,
|
|
564
546
|
rigoToken: string,
|
|
@@ -568,85 +550,38 @@ const createMultiLangAsset = async (
|
|
|
568
550
|
deployUrl: string,
|
|
569
551
|
academyId?: number
|
|
570
552
|
): Promise<{ errors: AssetSyncError[] }> => {
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
return {
|
|
574
|
-
errors: [
|
|
575
|
-
{
|
|
576
|
-
kind: "package_error",
|
|
577
|
-
error: {
|
|
578
|
-
detail:
|
|
579
|
-
"Could not resolve Learnpack package id; assets not synced to Breathecode.",
|
|
580
|
-
},
|
|
581
|
-
},
|
|
582
|
-
],
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
const availableLangs = Object.keys(courseJson.title)
|
|
587
|
-
console.log("AVAILABLE LANGUAGES to upload asset", availableLangs)
|
|
588
|
-
|
|
589
|
-
const all_translations: string[] = []
|
|
590
|
-
const errors: AssetSyncError[] = []
|
|
591
|
-
|
|
592
|
-
for (const lang of availableLangs) {
|
|
593
|
-
// eslint-disable-next-line no-await-in-loop
|
|
594
|
-
const indexReadme = await bucket.file(
|
|
553
|
+
const readReadme = async (lang: string): Promise<string> => {
|
|
554
|
+
const indexReadme = bucket.file(
|
|
595
555
|
`courses/${courseSlug}/README${getReadmeExtension(lang)}`
|
|
596
556
|
)
|
|
597
|
-
let indexReadmeString = ""
|
|
598
557
|
try {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
indexReadmeString = indexReadmeContent.toString()
|
|
558
|
+
const [content] = await indexReadme.download()
|
|
559
|
+
return content.toString()
|
|
602
560
|
} catch (error) {
|
|
603
561
|
console.error("Error downloading index readme", error)
|
|
604
562
|
// TODO: Trigger generation of the index readme
|
|
605
|
-
|
|
563
|
+
return ""
|
|
606
564
|
}
|
|
565
|
+
}
|
|
607
566
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
b64IndexReadme,
|
|
618
|
-
academyId,
|
|
619
|
-
learnpackId,
|
|
620
|
-
undefined,
|
|
621
|
-
all_translations
|
|
622
|
-
)
|
|
623
|
-
|
|
624
|
-
if (!asset) {
|
|
625
|
-
errors.push({
|
|
626
|
-
kind: "lang_error",
|
|
627
|
-
lang,
|
|
628
|
-
error: { detail: "Failed to create asset" },
|
|
629
|
-
})
|
|
630
|
-
console.log("No se pudo crear el asset, saltando idioma", lang)
|
|
631
|
-
continue
|
|
632
|
-
}
|
|
567
|
+
const result = await syncPackageAssets({
|
|
568
|
+
bcToken,
|
|
569
|
+
rigoToken,
|
|
570
|
+
packageSlug: courseSlug,
|
|
571
|
+
learnJson: courseJson,
|
|
572
|
+
deployUrl,
|
|
573
|
+
academyId,
|
|
574
|
+
readReadme,
|
|
575
|
+
})
|
|
633
576
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
error
|
|
640
|
-
errors.push({
|
|
641
|
-
kind: "lang_error",
|
|
642
|
-
lang,
|
|
643
|
-
error: { detail: assetSyncErrorDetail(errorData) },
|
|
644
|
-
})
|
|
645
|
-
console.error(`Error creating asset for language ${lang}:`, error)
|
|
646
|
-
}
|
|
577
|
+
for (const warning of result.warnings) console.warn("[assets]", warning)
|
|
578
|
+
for (const synced of result.synced) {
|
|
579
|
+
console.log(
|
|
580
|
+
`[assets] ${synced.action} "${synced.lang}": #${synced.asset.id} ${synced.asset.slug}`
|
|
581
|
+
)
|
|
647
582
|
}
|
|
648
583
|
|
|
649
|
-
return { errors }
|
|
584
|
+
return { errors: result.errors }
|
|
650
585
|
}
|
|
651
586
|
|
|
652
587
|
const lessonCleaner = (lesson: Lesson) => {
|
|
@@ -1181,17 +1116,43 @@ async function continueWithNextLesson(
|
|
|
1181
1116
|
await saveSyllabus(courseSlug, newSyllabus, bucket)
|
|
1182
1117
|
}
|
|
1183
1118
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1119
|
+
/**
|
|
1120
|
+
* The `og:image` of a published course.
|
|
1121
|
+
*
|
|
1122
|
+
* A published package is served from `{slug}.learn-pack.com`, so the course's
|
|
1123
|
+
* own `preview.png` is the right card whenever it exists. This used to go
|
|
1124
|
+
* through a helper that returned null for an empty preview, and both call sites
|
|
1125
|
+
* passed an empty string — so every published course, preview or not, fell back
|
|
1126
|
+
* to a shared SVG that no unfurl crawler can render.
|
|
1127
|
+
*/
|
|
1128
|
+
const resolveCoursePreviewUrl = async (
|
|
1129
|
+
bucket: Bucket,
|
|
1130
|
+
slug: string,
|
|
1131
|
+
configuredPreview?: string
|
|
1132
|
+
): Promise<string> => {
|
|
1133
|
+
// `config` is parsed from a config.json in the bucket, so its declared types
|
|
1134
|
+
// are an assertion rather than a guarantee: an old or hand-edited course can
|
|
1135
|
+
// carry anything here, and a throw would fail the whole publication.
|
|
1136
|
+
if (
|
|
1137
|
+
typeof configuredPreview === "string" &&
|
|
1138
|
+
configuredPreview.startsWith("http")
|
|
1139
|
+
) {
|
|
1140
|
+
return configuredPreview
|
|
1187
1141
|
}
|
|
1188
1142
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1143
|
+
try {
|
|
1144
|
+
const [exists] = await bucket.file(`courses/${slug}/preview.png`).exists()
|
|
1145
|
+
if (exists) {
|
|
1146
|
+
return resolvePreviewUrl(slug)
|
|
1147
|
+
}
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
console.error(
|
|
1150
|
+
`WARNING: could not check preview.png for ${slug}, using the brand card`,
|
|
1151
|
+
error
|
|
1152
|
+
)
|
|
1191
1153
|
}
|
|
1192
1154
|
|
|
1193
|
-
|
|
1194
|
-
return expectedUrl
|
|
1155
|
+
return getFallbackPreviewUrl()
|
|
1195
1156
|
}
|
|
1196
1157
|
|
|
1197
1158
|
const getTitleFromHTML = (html: string) => {
|
|
@@ -1200,6 +1161,73 @@ const getTitleFromHTML = (html: string) => {
|
|
|
1200
1161
|
return titleMatch ? titleMatch[1] : null
|
|
1201
1162
|
}
|
|
1202
1163
|
|
|
1164
|
+
const escapeHtmlAttribute = (value: string) =>
|
|
1165
|
+
value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<")
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* The IDE shell served to draft courses, with its meta tags filled in.
|
|
1169
|
+
*
|
|
1170
|
+
* The shell ships with `{{title}}`, `{{description}}` and `{{preview}}`
|
|
1171
|
+
* placeholders that only get substituted when a course is published. Drafts were
|
|
1172
|
+
* served the raw template, so a link shared in Slack or WhatsApp unfurled to a
|
|
1173
|
+
* literal `{{title}}` / `{{description}}`.
|
|
1174
|
+
*/
|
|
1175
|
+
let draftShellTemplate: string | null = null
|
|
1176
|
+
|
|
1177
|
+
const loadDraftShellTemplate = (): string => {
|
|
1178
|
+
const file = path.resolve(__dirname, "../ui/_app/index.html")
|
|
1179
|
+
|
|
1180
|
+
draftShellTemplate = fs
|
|
1181
|
+
.readFileSync(file, "utf-8")
|
|
1182
|
+
// A draft is not a real Course, so it must not emit Course structured data.
|
|
1183
|
+
// The match is anchored to the typed opening tag and non-greedy on purpose:
|
|
1184
|
+
// the shell also carries an inline <script> that loads the app bundle from
|
|
1185
|
+
// CloudFront, and a looser pattern would swallow it and blank the page.
|
|
1186
|
+
.replace(
|
|
1187
|
+
/[^\S\n]*<script type="application\/ld\+json">[\S\s]*?<\/script>\n?/,
|
|
1188
|
+
""
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
return draftShellTemplate
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const renderDraftShell = (req: express.Request): string => {
|
|
1195
|
+
const template = draftShellTemplate || loadDraftShellTemplate()
|
|
1196
|
+
|
|
1197
|
+
// Only the path: a preview URL can carry tokens in its query string, and
|
|
1198
|
+
// og:url ends up in whatever crawler cache unfurls the link.
|
|
1199
|
+
const canonicalUrl = escapeHtmlAttribute(`${getPublicHost()}${req.path}`)
|
|
1200
|
+
|
|
1201
|
+
return (
|
|
1202
|
+
template
|
|
1203
|
+
.replace(/<title>.*<\/title>/, `<title>${DRAFT_PREVIEW_TITLE}</title>`)
|
|
1204
|
+
.replace(/{{title}}/g, DRAFT_PREVIEW_TITLE)
|
|
1205
|
+
.replace(/{{description}}/g, DRAFT_PREVIEW_DESCRIPTION)
|
|
1206
|
+
.replace(/{{preview}}/g, getFallbackPreviewUrl())
|
|
1207
|
+
.replace(
|
|
1208
|
+
"</head>",
|
|
1209
|
+
` <meta property="og:url" content="${canonicalUrl}" />\n` +
|
|
1210
|
+
` <meta name="robots" content="noindex, nofollow, noarchive" />\n </head>`
|
|
1211
|
+
)
|
|
1212
|
+
// Safety net for placeholders a future shell may add: shipping an empty
|
|
1213
|
+
// value to a crawler beats shipping "{{whatever}}".
|
|
1214
|
+
.replace(/{{\w+}}/g, "")
|
|
1215
|
+
)
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* Drafts are unlisted work in progress. `noindex` goes out as both a meta tag
|
|
1220
|
+
* and a header so it reaches crawlers that skip the markup; a `robots.txt`
|
|
1221
|
+
* `Disallow` would be the wrong tool, since it blocks the fetch itself and
|
|
1222
|
+
* would stop Slack and X from unfurling the link at all.
|
|
1223
|
+
*/
|
|
1224
|
+
const sendDraftShell = (req: express.Request, res: express.Response) => {
|
|
1225
|
+
res
|
|
1226
|
+
.type("html")
|
|
1227
|
+
.set("X-Robots-Tag", "noindex, nofollow")
|
|
1228
|
+
.send(renderDraftShell(req))
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1203
1231
|
/** How often the notification is touched while a translation is running. */
|
|
1204
1232
|
const SYNC_HEARTBEAT_INTERVAL_MS = 60 * 1000
|
|
1205
1233
|
|
|
@@ -1645,6 +1673,11 @@ class ServeCommand extends SessionCommand {
|
|
|
1645
1673
|
const localAppPath = path.resolve(__dirname, "../ui/_app")
|
|
1646
1674
|
// app.use(express.static(localAppPath))
|
|
1647
1675
|
|
|
1676
|
+
// Warm the draft shell now that _app is on disk. Doing it here rather than
|
|
1677
|
+
// on first request means a corrupt tarball fails at boot, with a stack
|
|
1678
|
+
// trace, instead of on some user's preview link.
|
|
1679
|
+
loadDraftShellTemplate()
|
|
1680
|
+
|
|
1648
1681
|
app.post("/upload", async (req, res) => {
|
|
1649
1682
|
const { content, destination } = req.body
|
|
1650
1683
|
// console.log("UPLOAD", content, destination)
|
|
@@ -3130,8 +3163,7 @@ class ServeCommand extends SessionCommand {
|
|
|
3130
3163
|
const slug = req.query.slug
|
|
3131
3164
|
|
|
3132
3165
|
if (slug) {
|
|
3133
|
-
|
|
3134
|
-
res.sendFile(file)
|
|
3166
|
+
sendDraftShell(req, res)
|
|
3135
3167
|
} else {
|
|
3136
3168
|
res.redirect("https://learnpack.co")
|
|
3137
3169
|
}
|
|
@@ -3140,13 +3172,18 @@ class ServeCommand extends SessionCommand {
|
|
|
3140
3172
|
app.get("/preview/:slug", async (req, res) => {
|
|
3141
3173
|
console.log("GET COURSE PREVIEW /")
|
|
3142
3174
|
|
|
3143
|
-
|
|
3144
|
-
res.sendFile(file)
|
|
3175
|
+
sendDraftShell(req, res)
|
|
3145
3176
|
})
|
|
3146
3177
|
|
|
3147
3178
|
app.get("/preview/:slug/webview", async (req, res) => {
|
|
3148
|
-
|
|
3149
|
-
|
|
3179
|
+
sendDraftShell(req, res)
|
|
3180
|
+
})
|
|
3181
|
+
|
|
3182
|
+
// The brand card used as og:image by drafts, and as the last-resort
|
|
3183
|
+
// fallback by published courses without a preview of their own.
|
|
3184
|
+
app.get("/og-image.png", (req, res) => {
|
|
3185
|
+
res.set("Cache-Control", "public, max-age=86400")
|
|
3186
|
+
res.sendFile(path.resolve(__dirname, "../ui/static/og-image.png"))
|
|
3150
3187
|
})
|
|
3151
3188
|
|
|
3152
3189
|
app.get("/config", async (req, res) => {
|
|
@@ -5281,6 +5318,10 @@ class ServeCommand extends SessionCommand {
|
|
|
5281
5318
|
})
|
|
5282
5319
|
}
|
|
5283
5320
|
|
|
5321
|
+
// Older IDEs only send the Breathecode token; without the Rigobot
|
|
5322
|
+
// token the lookup falls back to the title-derived slugs.
|
|
5323
|
+
const rigoToken = req.header("x-rigo-token")
|
|
5324
|
+
|
|
5284
5325
|
const configFile = await bucket.file(
|
|
5285
5326
|
`courses/${slug}/.learn/config.json`
|
|
5286
5327
|
)
|
|
@@ -5288,56 +5329,51 @@ class ServeCommand extends SessionCommand {
|
|
|
5288
5329
|
const configJson = JSON.parse(configContent.toString())
|
|
5289
5330
|
const { config } = configJson
|
|
5290
5331
|
|
|
5291
|
-
const
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
const
|
|
5298
|
-
.
|
|
5299
|
-
|
|
5300
|
-
if (!assetTitle) return null
|
|
5301
|
-
const assetSlug = slugify(assetTitle).slice(0, 47)
|
|
5302
|
-
return `${assetSlug}-${lang}`
|
|
5303
|
-
})
|
|
5304
|
-
.filter((slug): slug is string => slug !== null)
|
|
5332
|
+
const resolution = await resolvePackageAssets({
|
|
5333
|
+
bcToken,
|
|
5334
|
+
rigoToken,
|
|
5335
|
+
packageSlug: slug,
|
|
5336
|
+
learnJson: config,
|
|
5337
|
+
})
|
|
5338
|
+
for (const warning of resolution.warnings) {
|
|
5339
|
+
console.warn("[package-academy]", warning)
|
|
5340
|
+
}
|
|
5305
5341
|
|
|
5306
|
-
const
|
|
5307
|
-
|
|
5308
|
-
|
|
5342
|
+
const isPublished = hasPublishedAsset(resolution)
|
|
5343
|
+
const academyMode = determineAcademyMode(resolution)
|
|
5344
|
+
const warnings = [...resolution.warnings]
|
|
5345
|
+
if (resolution.packageState === "unknown") {
|
|
5346
|
+
warnings.push(
|
|
5347
|
+
`${resolution.packageError}; existing assets could not be checked.`
|
|
5348
|
+
)
|
|
5349
|
+
}
|
|
5309
5350
|
|
|
5310
|
-
for (const
|
|
5311
|
-
if (
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5351
|
+
for (const [lang, entry] of resolution.byLang) {
|
|
5352
|
+
if (entry.status === "unknown") {
|
|
5353
|
+
warnings.push(`"${lang}": ${entry.error}`)
|
|
5354
|
+
} else if (entry.status === "ambiguous") {
|
|
5355
|
+
warnings.push(
|
|
5356
|
+
`"${lang}": ${
|
|
5357
|
+
entry.candidates.length
|
|
5358
|
+
} assets found (${entry.candidates
|
|
5359
|
+
.map(c => `#${c.id} ${c.slug}`)
|
|
5360
|
+
.join(", ")})`
|
|
5361
|
+
)
|
|
5316
5362
|
}
|
|
5317
5363
|
}
|
|
5318
5364
|
|
|
5319
|
-
const
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
let conflictAcademies: number[] | undefined
|
|
5325
|
-
|
|
5326
|
-
if (uniqueAcademies.length === 0) {
|
|
5327
|
-
mode = "select"
|
|
5328
|
-
} else if (uniqueAcademies.length === 1) {
|
|
5329
|
-
mode = "locked"
|
|
5330
|
-
lockedAcademyId = uniqueAcademies[0]
|
|
5331
|
-
} else {
|
|
5332
|
-
mode = "conflict"
|
|
5333
|
-
conflictAcademies = uniqueAcademies
|
|
5334
|
-
}
|
|
5365
|
+
const mode = academyMode.type
|
|
5366
|
+
const lockedAcademyId =
|
|
5367
|
+
academyMode.type === "locked" ? academyMode.academyId : undefined
|
|
5368
|
+
const conflictAcademies =
|
|
5369
|
+
academyMode.type === "conflict" ? academyMode.academies : undefined
|
|
5335
5370
|
|
|
5336
5371
|
return res.json({
|
|
5337
5372
|
isPublished,
|
|
5338
5373
|
mode,
|
|
5339
5374
|
lockedAcademyId,
|
|
5340
5375
|
conflictAcademies,
|
|
5376
|
+
warnings,
|
|
5341
5377
|
})
|
|
5342
5378
|
} catch (error) {
|
|
5343
5379
|
console.error("Error fetching package academy:", error)
|
|
@@ -5406,16 +5442,18 @@ class ServeCommand extends SessionCommand {
|
|
|
5406
5442
|
selectedLang = availableLangs[0]
|
|
5407
5443
|
}
|
|
5408
5444
|
|
|
5445
|
+
const previewUrl = await resolveCoursePreviewUrl(
|
|
5446
|
+
bucket,
|
|
5447
|
+
slug,
|
|
5448
|
+
config.preview
|
|
5449
|
+
)
|
|
5450
|
+
|
|
5409
5451
|
const idxTpl = fs.readFileSync(path.join(uiSrc, "index.html"), "utf-8")
|
|
5410
5452
|
const idxHtml = idxTpl
|
|
5411
5453
|
.replace(/{{title}}/g, title)
|
|
5412
5454
|
.replace(/<title>.*<\/title>/, `<title>${title}</title>`)
|
|
5413
5455
|
.replace(/{{description}}/g, config.description[selectedLang])
|
|
5414
|
-
.replace(
|
|
5415
|
-
/{{preview}}/g,
|
|
5416
|
-
fixPreviewUrl(slug, "") ||
|
|
5417
|
-
"https://raw.githubusercontent.com/learnpack/ide/master/public/learnpack.svg"
|
|
5418
|
-
)
|
|
5456
|
+
.replace(/{{preview}}/g, previewUrl)
|
|
5419
5457
|
.replace(/{{slug}}/g, slug)
|
|
5420
5458
|
.replace(/{{duration}}/g, minutesToISO8601Duration(config.duration))
|
|
5421
5459
|
fs.writeFileSync(path.join(buildRoot, "index.html"), idxHtml)
|
|
@@ -5463,9 +5501,9 @@ class ServeCommand extends SessionCommand {
|
|
|
5463
5501
|
// 8) Crear el config.json en buildRoot con lo que retorna buildConfig
|
|
5464
5502
|
const fullConfig = { config, exercises }
|
|
5465
5503
|
fullConfig.config.slug = slug
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5504
|
+
// Same URL the meta tags got: this used to overwrite the real preview
|
|
5505
|
+
// of every published package with the unusable SVG fallback.
|
|
5506
|
+
fullConfig.config.preview = previewUrl
|
|
5469
5507
|
|
|
5470
5508
|
fs.writeFileSync(
|
|
5471
5509
|
path.join(buildRoot, "config.json"),
|