@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/publish.ts
CHANGED
|
@@ -19,281 +19,78 @@ import api, { getConsumable, RIGOBOT_HOST, TAcademy } from "../utils/api"
|
|
|
19
19
|
import * as prompts from "prompts"
|
|
20
20
|
import { isValidRigoToken } from "../utils/rigoActions"
|
|
21
21
|
import { minutesToISO8601Duration } from "../utils/misc"
|
|
22
|
-
import { getReadmeExtension
|
|
22
|
+
import { getReadmeExtension } from "../utils/creatorUtilities"
|
|
23
|
+
import { getFallbackPreviewUrl } from "../utils/branding"
|
|
24
|
+
import {
|
|
25
|
+
determineAcademyMode,
|
|
26
|
+
getAvailableLangs,
|
|
27
|
+
PackageAssetsResolution,
|
|
28
|
+
resolvePackageAssets,
|
|
29
|
+
} from "../utils/assetResolver"
|
|
30
|
+
import { getLocalizedValue, syncPackageAssets } from "../utils/assetSync"
|
|
23
31
|
|
|
24
32
|
const uploadZipEndpont = RIGOBOT_HOST + "/v1/learnpack/upload"
|
|
25
33
|
|
|
26
|
-
const getAvailableLangs = (learnJson: any): string[] => {
|
|
27
|
-
const langs = Object.keys(learnJson?.title || {})
|
|
28
|
-
return langs.filter((l) => typeof l === "string" && l.length > 0)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
34
|
const getDefaultLang = (learnJson: any): string => {
|
|
32
35
|
const availableLangs = getAvailableLangs(learnJson)
|
|
33
36
|
if (availableLangs.length === 0) return "en"
|
|
34
37
|
return availableLangs.includes("en") ? "en" : availableLangs[0]
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
)
|
|
42
|
-
if (!translations || typeof translations !== "object") return ""
|
|
43
|
-
|
|
44
|
-
const direct = translations[lang]
|
|
45
|
-
if (typeof direct === "string" && direct.trim().length > 0) return direct
|
|
46
|
-
|
|
47
|
-
for (const fb of fallbackLangs) {
|
|
48
|
-
const v = translations[fb]
|
|
49
|
-
if (typeof v === "string" && v.trim().length > 0) return v
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const firstKey = Object.keys(translations)[0]
|
|
53
|
-
const first = firstKey ? translations[firstKey] : ""
|
|
54
|
-
return typeof first === "string" ? first : ""
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
type ExistingAssetInfo = {
|
|
58
|
-
lang: string;
|
|
59
|
-
slug: string;
|
|
60
|
-
exists: boolean;
|
|
61
|
-
academyId?: number;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
type AcademyMode =
|
|
65
|
-
| { type: "select" }
|
|
66
|
-
| { type: "locked"; academyId: number }
|
|
67
|
-
| { type: "conflict"; academies: number[] };
|
|
68
|
-
|
|
69
|
-
const getExistingAssetsInfo = async (
|
|
70
|
-
token: string,
|
|
71
|
-
learnJson: any
|
|
72
|
-
): Promise<ExistingAssetInfo[]> => {
|
|
73
|
-
const availableLangs = getAvailableLangs(learnJson)
|
|
74
|
-
const results: ExistingAssetInfo[] = []
|
|
75
|
-
|
|
76
|
-
for (const lang of availableLangs) {
|
|
77
|
-
const assetTitle = getLocalizedValue(learnJson?.title, lang)
|
|
78
|
-
if (!assetTitle) continue
|
|
79
|
-
|
|
80
|
-
let slug = slugify(assetTitle).slice(0, 47)
|
|
81
|
-
slug = `${slug}-${lang}`
|
|
82
|
-
|
|
83
|
-
// eslint-disable-next-line no-await-in-loop
|
|
84
|
-
const { exists, academyId } = await api.doesAssetExists(token, slug)
|
|
85
|
-
results.push({ lang, slug, exists, academyId })
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return results
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const determinePublishAcademyMode = (
|
|
92
|
-
existingAssets: ExistingAssetInfo[]
|
|
93
|
-
): AcademyMode => {
|
|
94
|
-
const academyIds = existingAssets
|
|
95
|
-
.filter((a) => a.exists && a.academyId !== undefined)
|
|
96
|
-
.map((a) => a.academyId as number)
|
|
97
|
-
|
|
98
|
-
const unique = [...new Set(academyIds)]
|
|
99
|
-
|
|
100
|
-
if (unique.length === 0) return { type: "select" }
|
|
101
|
-
if (unique.length === 1) return { type: "locked", academyId: unique[0] }
|
|
102
|
-
return { type: "conflict", academies: unique }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export const handleAssetCreation = async (
|
|
106
|
-
sessionPayload: { token: string; rigobotToken: string },
|
|
107
|
-
learnJson: any,
|
|
108
|
-
selectedLang: string,
|
|
109
|
-
learnpackDeployUrl: string,
|
|
110
|
-
b64IndexReadme: string,
|
|
111
|
-
academyId: number | undefined,
|
|
112
|
-
learnpackId: number,
|
|
113
|
-
preflightInfo?: ExistingAssetInfo,
|
|
114
|
-
all_translations: string[] = []
|
|
115
|
-
) => {
|
|
116
|
-
const category = "uncategorized"
|
|
117
|
-
|
|
40
|
+
const readReadmeFromDisk = async (lang: string): Promise<string> => {
|
|
41
|
+
const readmePath = path.join(
|
|
42
|
+
process.cwd(),
|
|
43
|
+
`README${getReadmeExtension(lang)}`
|
|
44
|
+
)
|
|
118
45
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const assetDescription = getLocalizedValue(
|
|
123
|
-
learnJson?.description,
|
|
124
|
-
selectedLang
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
if (!assetTitle) {
|
|
128
|
-
throw new Error(
|
|
129
|
-
`Missing learn.json title for language "${selectedLang}"`
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
let slug = slugify(assetTitle).slice(0, 47)
|
|
134
|
-
slug = `${slug}-${selectedLang}`
|
|
135
|
-
|
|
136
|
-
// Use pre-flight info when available to avoid an extra GET request
|
|
137
|
-
const { exists, academyId: existingAcademyId } =
|
|
138
|
-
preflightInfo ?? (await api.doesAssetExists(sessionPayload.token, slug))
|
|
139
|
-
|
|
140
|
-
// const technologies: unknown[] = Array.isArray(learnJson?.technologies) ?
|
|
141
|
-
// learnJson.technologies :
|
|
142
|
-
// []
|
|
143
|
-
|
|
144
|
-
if (!exists) {
|
|
145
|
-
Console.info("Asset does not exist in this academy, creating it")
|
|
146
|
-
const assetPayload: any = {
|
|
147
|
-
slug: slug,
|
|
148
|
-
title: assetTitle,
|
|
149
|
-
lang: selectedLang,
|
|
150
|
-
graded: true,
|
|
151
|
-
description: assetDescription,
|
|
152
|
-
learnpack_deploy_url: learnpackDeployUrl,
|
|
153
|
-
// technologies: technologies.map((tech: unknown) =>
|
|
154
|
-
// String(tech).toLowerCase().replace(/\s+/g, "-")
|
|
155
|
-
// ),
|
|
156
|
-
technologies: [],
|
|
157
|
-
url: learnpackDeployUrl,
|
|
158
|
-
category: category,
|
|
159
|
-
owner: user.id,
|
|
160
|
-
author: user.id,
|
|
161
|
-
preview: learnJson.preview,
|
|
162
|
-
readme_raw: b64IndexReadme,
|
|
163
|
-
all_translations,
|
|
164
|
-
learnpack_id: learnpackId,
|
|
165
|
-
}
|
|
166
|
-
if (academyId !== undefined) {
|
|
167
|
-
assetPayload.academy_id = academyId
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const asset = await api.createAsset(sessionPayload.token, assetPayload)
|
|
171
|
-
try {
|
|
172
|
-
await api.updateRigoPackage(
|
|
173
|
-
sessionPayload.rigobotToken.trim(),
|
|
174
|
-
learnJson.slug,
|
|
175
|
-
{
|
|
176
|
-
asset_id: asset.id,
|
|
177
|
-
}
|
|
178
|
-
)
|
|
179
|
-
} catch (error) {
|
|
180
|
-
Console.error("Error updating Rigo package:", error)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
Console.info("Asset created with id", asset.id)
|
|
184
|
-
return asset
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
Console.info("Asset exists, updating it")
|
|
188
|
-
const updatePayload: any = {
|
|
189
|
-
graded: true,
|
|
190
|
-
learnpack_deploy_url: learnpackDeployUrl,
|
|
191
|
-
title: assetTitle,
|
|
192
|
-
category: category,
|
|
193
|
-
description: assetDescription,
|
|
194
|
-
all_translations,
|
|
195
|
-
learnpack_id: learnpackId,
|
|
196
|
-
}
|
|
197
|
-
// Only set academy when the asset has none yet and the user selected one
|
|
198
|
-
if (existingAcademyId === undefined && academyId !== undefined) {
|
|
199
|
-
updatePayload.academy_id = academyId
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const asset = await api.updateAsset(
|
|
203
|
-
sessionPayload.token,
|
|
204
|
-
slug,
|
|
205
|
-
updatePayload
|
|
206
|
-
)
|
|
207
|
-
try {
|
|
208
|
-
await api.updateRigoPackage(
|
|
209
|
-
sessionPayload.rigobotToken.trim(),
|
|
210
|
-
learnJson.slug,
|
|
211
|
-
{
|
|
212
|
-
asset_id: asset.id,
|
|
213
|
-
}
|
|
214
|
-
)
|
|
215
|
-
} catch (error) {
|
|
216
|
-
Console.error("Error updating Rigo package:", error)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
Console.info("Asset updated with id", asset.id)
|
|
220
|
-
return asset
|
|
46
|
+
return fs.existsSync(readmePath) ?
|
|
47
|
+
fs.readFileSync(readmePath, "utf-8") :
|
|
48
|
+
""
|
|
221
49
|
} catch (error) {
|
|
222
|
-
Console.error("Error
|
|
223
|
-
|
|
50
|
+
Console.error("Error reading index readme:", error)
|
|
51
|
+
return ""
|
|
224
52
|
}
|
|
225
53
|
}
|
|
226
54
|
|
|
227
|
-
const
|
|
55
|
+
const syncAssetsFromDisk = async (
|
|
228
56
|
sessionPayload: { token: string; rigobotToken: string },
|
|
229
57
|
learnJson: any,
|
|
230
58
|
deployUrl: string,
|
|
231
|
-
selectedAcademyId
|
|
232
|
-
|
|
59
|
+
selectedAcademyId: number | undefined,
|
|
60
|
+
resolution: PackageAssetsResolution
|
|
233
61
|
) => {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (availableLangs.length === 0) {
|
|
62
|
+
if (getAvailableLangs(learnJson).length === 0) {
|
|
237
63
|
Console.error(
|
|
238
64
|
"No languages found in learn.json.title. Add at least one language (e.g. title.en)."
|
|
239
65
|
)
|
|
240
66
|
return
|
|
241
67
|
}
|
|
242
68
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
69
|
+
// The pre-upload resolution is reused unless the package did not exist in
|
|
70
|
+
// Rigobot yet (first publish): the upload has just created it.
|
|
71
|
+
const result = await syncPackageAssets({
|
|
72
|
+
bcToken: sessionPayload.token,
|
|
73
|
+
rigoToken: sessionPayload.rigobotToken,
|
|
74
|
+
packageSlug: learnJson.slug,
|
|
75
|
+
learnJson,
|
|
76
|
+
deployUrl,
|
|
77
|
+
academyId: selectedAcademyId,
|
|
78
|
+
readReadme: readReadmeFromDisk,
|
|
79
|
+
resolution: resolution.learnpackId === null ? undefined : resolution,
|
|
80
|
+
})
|
|
253
81
|
|
|
254
|
-
const
|
|
255
|
-
for (const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
`README${getReadmeExtension(lang)}`
|
|
82
|
+
for (const warning of result.warnings) Console.warning(warning)
|
|
83
|
+
for (const synced of result.synced) {
|
|
84
|
+
Console.info(
|
|
85
|
+
`Asset ${synced.action} for "${synced.lang}": #${synced.asset.id} ${synced.asset.slug}`
|
|
259
86
|
)
|
|
87
|
+
}
|
|
260
88
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
} catch (error) {
|
|
267
|
-
Console.error("Error reading index readme:", error)
|
|
268
|
-
indexReadmeString = ""
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const b64IndexReadme = Buffer.from(indexReadmeString).toString("base64")
|
|
272
|
-
const preflightInfo = existingAssetsInfo.find((a) => a.lang === lang)
|
|
273
|
-
|
|
274
|
-
try {
|
|
275
|
-
// eslint-disable-next-line no-await-in-loop
|
|
276
|
-
const asset = await handleAssetCreation(
|
|
277
|
-
sessionPayload,
|
|
278
|
-
learnJson,
|
|
279
|
-
lang,
|
|
280
|
-
deployUrl,
|
|
281
|
-
b64IndexReadme,
|
|
282
|
-
selectedAcademyId,
|
|
283
|
-
learnpackId,
|
|
284
|
-
preflightInfo,
|
|
285
|
-
all_translations
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
if (!asset) {
|
|
289
|
-
Console.debug("Could not create/update asset for lang", lang)
|
|
290
|
-
continue
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
all_translations.push(asset.slug)
|
|
294
|
-
} catch (error) {
|
|
295
|
-
Console.error("Error creating asset for language", lang, error)
|
|
296
|
-
// Continue with other languages
|
|
89
|
+
for (const error of result.errors) {
|
|
90
|
+
if (error.kind === "lang_error") {
|
|
91
|
+
Console.error(`Asset for "${error.lang}": ${error.error.detail}`)
|
|
92
|
+
} else {
|
|
93
|
+
Console.error(error.error.detail)
|
|
297
94
|
}
|
|
298
95
|
}
|
|
299
96
|
}
|
|
@@ -487,11 +284,21 @@ class BuildCommand extends SessionCommand {
|
|
|
487
284
|
}
|
|
488
285
|
|
|
489
286
|
Console.info("Checking existing assets...")
|
|
490
|
-
const
|
|
491
|
-
sessionPayload.token,
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
287
|
+
const resolution = await resolvePackageAssets({
|
|
288
|
+
bcToken: sessionPayload.token,
|
|
289
|
+
rigoToken: rigoToken,
|
|
290
|
+
packageSlug: learnJson.slug,
|
|
291
|
+
learnJson,
|
|
292
|
+
})
|
|
293
|
+
for (const warning of resolution.warnings) Console.warning(warning)
|
|
294
|
+
if (resolution.packageState === "unknown") {
|
|
295
|
+
Console.error(
|
|
296
|
+
`${resolution.packageError}. Existing assets cannot be checked, so publishing now could create duplicates. Try again later.`
|
|
297
|
+
)
|
|
298
|
+
process.exit(1)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const academyMode = determineAcademyMode(resolution)
|
|
495
302
|
|
|
496
303
|
let selectedAcademyId: number | undefined
|
|
497
304
|
|
|
@@ -588,9 +395,7 @@ class BuildCommand extends SessionCommand {
|
|
|
588
395
|
getLocalizedValue(learnJson?.title, selectedLang) ||
|
|
589
396
|
"LearnPack: Interactive Learning as a Service"
|
|
590
397
|
|
|
591
|
-
const previewUrl =
|
|
592
|
-
learnJson.preview ||
|
|
593
|
-
"https://raw.githubusercontent.com/learnpack/ide/refs/heads/master/public/learnpack.svg"
|
|
398
|
+
const previewUrl = learnJson.preview || getFallbackPreviewUrl()
|
|
594
399
|
// Replace placeholders and the <title>Old title </title> tag for a new tag with the title
|
|
595
400
|
indexHtmlContent = indexHtmlContent
|
|
596
401
|
.replace(/{{description}}/g, description)
|
|
@@ -689,12 +494,12 @@ class BuildCommand extends SessionCommand {
|
|
|
689
494
|
fs.unlinkSync(zipFilePath)
|
|
690
495
|
this.removeDirectory(buildDir)
|
|
691
496
|
|
|
692
|
-
await
|
|
497
|
+
await syncAssetsFromDisk(
|
|
693
498
|
{ token: sessionPayload.token, rigobotToken: rigoToken },
|
|
694
499
|
learnJson,
|
|
695
500
|
res.data.url,
|
|
696
501
|
selectedAcademyId,
|
|
697
|
-
|
|
502
|
+
resolution
|
|
698
503
|
)
|
|
699
504
|
} catch (error) {
|
|
700
505
|
if (axios.isAxiosError(error)) {
|