@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branding used by link previews (Open Graph / Twitter cards).
|
|
3
|
+
*
|
|
4
|
+
* Both the draft server and the publish pipeline need an absolute image URL for
|
|
5
|
+
* `og:image`, and drafts additionally need copy that says nothing about the
|
|
6
|
+
* course they belong to.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Where `learnpack serve` runs in production. It is only a fallback: the server
|
|
10
|
+
* itself already has the real origin in `process.env.HOST` (it uses it to build
|
|
11
|
+
* webhook URLs). The `publish` command, however, runs on the author's machine
|
|
12
|
+
* with no such variable, and still has to write an absolute `og:image` into the
|
|
13
|
+
* package it uploads.
|
|
14
|
+
*/
|
|
15
|
+
export declare const PRODUCTION_PUBLIC_HOST = "https://app.learnpack.co";
|
|
16
|
+
/**
|
|
17
|
+
* Public origin of the app server, without a trailing slash.
|
|
18
|
+
*
|
|
19
|
+
* Only an absolute http(s) origin is accepted. `HOST` is also the conventional
|
|
20
|
+
* name for a bind address, so a deployment that sets it to `0.0.0.0` would
|
|
21
|
+
* otherwise yield a relative `og:image` — which no crawler can resolve, and
|
|
22
|
+
* which would be baked into every package published from that machine.
|
|
23
|
+
* @returns The origin to build absolute preview URLs from.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getPublicHost(): string;
|
|
26
|
+
/**
|
|
27
|
+
* Neutral brand card, used as `og:image` when a course has no preview of its own.
|
|
28
|
+
*
|
|
29
|
+
* It has to be a raster image: no unfurl crawler renders SVG. The previous
|
|
30
|
+
* fallback pointed at `learnpack.svg` on raw.githubusercontent.com, which fails
|
|
31
|
+
* twice over — SVG is unsupported, and that host serves it as `text/plain` with
|
|
32
|
+
* `X-Content-Type-Options: nosniff`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function getFallbackPreviewUrl(): string;
|
|
35
|
+
/**
|
|
36
|
+
* Copy for the link preview of a draft course.
|
|
37
|
+
*
|
|
38
|
+
* A draft is unlisted work in progress, so the card carries branding only: it
|
|
39
|
+
* must not disclose the title, the description or anything else about the
|
|
40
|
+
* course, and the pages that use it are served `noindex`.
|
|
41
|
+
*/
|
|
42
|
+
export declare const DRAFT_PREVIEW_TITLE = "LearnPack \u2014 Draft course";
|
|
43
|
+
export declare const DRAFT_PREVIEW_DESCRIPTION = "This course is still a draft and is not published yet.";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Branding used by link previews (Open Graph / Twitter cards).
|
|
4
|
+
*
|
|
5
|
+
* Both the draft server and the publish pipeline need an absolute image URL for
|
|
6
|
+
* `og:image`, and drafts additionally need copy that says nothing about the
|
|
7
|
+
* course they belong to.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.DRAFT_PREVIEW_DESCRIPTION = exports.DRAFT_PREVIEW_TITLE = exports.PRODUCTION_PUBLIC_HOST = void 0;
|
|
11
|
+
exports.getPublicHost = getPublicHost;
|
|
12
|
+
exports.getFallbackPreviewUrl = getFallbackPreviewUrl;
|
|
13
|
+
/**
|
|
14
|
+
* Where `learnpack serve` runs in production. It is only a fallback: the server
|
|
15
|
+
* itself already has the real origin in `process.env.HOST` (it uses it to build
|
|
16
|
+
* webhook URLs). The `publish` command, however, runs on the author's machine
|
|
17
|
+
* with no such variable, and still has to write an absolute `og:image` into the
|
|
18
|
+
* package it uploads.
|
|
19
|
+
*/
|
|
20
|
+
exports.PRODUCTION_PUBLIC_HOST = "https://app.learnpack.co";
|
|
21
|
+
/**
|
|
22
|
+
* Strips what `.env` files routinely smuggle in — a trailing comment, quotes,
|
|
23
|
+
* stray whitespace, a trailing slash — the same way `serve` sanitises `HOST`.
|
|
24
|
+
* @param raw - The raw environment value, if set.
|
|
25
|
+
* @returns The cleaned value, or an empty string.
|
|
26
|
+
*/
|
|
27
|
+
function normalizeOrigin(raw) {
|
|
28
|
+
if (!raw) {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
return raw
|
|
32
|
+
.trim()
|
|
33
|
+
.split(/\s+#/)[0]
|
|
34
|
+
.trim()
|
|
35
|
+
.replace(/["']/g, "")
|
|
36
|
+
.replace(/\/+$/, "");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Public origin of the app server, without a trailing slash.
|
|
40
|
+
*
|
|
41
|
+
* Only an absolute http(s) origin is accepted. `HOST` is also the conventional
|
|
42
|
+
* name for a bind address, so a deployment that sets it to `0.0.0.0` would
|
|
43
|
+
* otherwise yield a relative `og:image` — which no crawler can resolve, and
|
|
44
|
+
* which would be baked into every package published from that machine.
|
|
45
|
+
* @returns The origin to build absolute preview URLs from.
|
|
46
|
+
*/
|
|
47
|
+
function getPublicHost() {
|
|
48
|
+
for (const raw of [process.env.PUBLIC_HOST, process.env.HOST]) {
|
|
49
|
+
const cleaned = normalizeOrigin(raw);
|
|
50
|
+
if (/^https?:\/\/\S+$/.test(cleaned)) {
|
|
51
|
+
return cleaned;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return exports.PRODUCTION_PUBLIC_HOST;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Neutral brand card, used as `og:image` when a course has no preview of its own.
|
|
58
|
+
*
|
|
59
|
+
* It has to be a raster image: no unfurl crawler renders SVG. The previous
|
|
60
|
+
* fallback pointed at `learnpack.svg` on raw.githubusercontent.com, which fails
|
|
61
|
+
* twice over — SVG is unsupported, and that host serves it as `text/plain` with
|
|
62
|
+
* `X-Content-Type-Options: nosniff`.
|
|
63
|
+
*/
|
|
64
|
+
function getFallbackPreviewUrl() {
|
|
65
|
+
return `${getPublicHost()}/og-image.png`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Copy for the link preview of a draft course.
|
|
69
|
+
*
|
|
70
|
+
* A draft is unlisted work in progress, so the card carries branding only: it
|
|
71
|
+
* must not disclose the title, the description or anything else about the
|
|
72
|
+
* course, and the pages that use it are served `noindex`.
|
|
73
|
+
*/
|
|
74
|
+
exports.DRAFT_PREVIEW_TITLE = "LearnPack — Draft course";
|
|
75
|
+
exports.DRAFT_PREVIEW_DESCRIPTION = "This course is still a draft and is not published yet.";
|
|
@@ -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.359",
|
|
5
5
|
"author": "Alejandro Sanchez @alesanchezr",
|
|
6
6
|
"contributors": [
|
|
7
7
|
{
|
|
@@ -166,7 +166,7 @@
|
|
|
166
166
|
"audit:package-sources": "ts-node --project ./tsconfig.json ./scripts/audit-package-sources.ts",
|
|
167
167
|
"backfill:package-manifest": "ts-node --project ./tsconfig.json ./scripts/backfill-package-manifest-s3.ts",
|
|
168
168
|
"repair:legacy-packages": "ts-node --project ./tsconfig.json ./scripts/repair-legacy-packages.ts",
|
|
169
|
-
"copy-assets": "npx cpy src/creatorDist/**/* lib/creatorDist --parents --verbose && npx cpy src/utils/templates/**/* lib/utils/templates --parents --verbose && npx cpy src/lua/**/* lib/lua --parents --verbose",
|
|
169
|
+
"copy-assets": "npx cpy src/creatorDist/**/* lib/creatorDist --parents --verbose && npx cpy src/utils/templates/**/* lib/utils/templates --parents --verbose && npx cpy src/lua/**/* lib/lua --parents --verbose && npx cpy src/ui/static/**/* lib/ui/static --parents --verbose",
|
|
170
170
|
"tsc": "tsc -b",
|
|
171
171
|
"postpack": "rm -f oclif.manifest.json && eslint . --ext .ts --config .eslintrc",
|
|
172
172
|
"prepack": "rm -rf lib && tsc -b && npm run copy-assets",
|