epicshop 6.84.6 → 6.84.8
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.
|
@@ -12,6 +12,27 @@ function stripEpicAiSlugSuffix(value) {
|
|
|
12
12
|
function normalizeHost(host) {
|
|
13
13
|
return host.toLowerCase().replace(/^www\./, '');
|
|
14
14
|
}
|
|
15
|
+
function parseEpicWorkshopSlugFromEmbedUrl(urlString) {
|
|
16
|
+
const parseSegments = (segments) => {
|
|
17
|
+
// Expected: /workshops/<workshopSlug>/...
|
|
18
|
+
if (segments[0] !== 'workshops')
|
|
19
|
+
return null;
|
|
20
|
+
const workshopSlug = segments[1] ?? null;
|
|
21
|
+
return workshopSlug ? stripEpicAiSlugSuffix(workshopSlug) : null;
|
|
22
|
+
};
|
|
23
|
+
try {
|
|
24
|
+
const url = new URL(urlString);
|
|
25
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
26
|
+
return parseSegments(segments);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Fall back to naive parsing (best-effort).
|
|
30
|
+
const withoutHash = urlString.split('#')[0] ?? urlString;
|
|
31
|
+
const withoutQuery = withoutHash.split('?')[0] ?? withoutHash;
|
|
32
|
+
const segments = withoutQuery.split('/').filter(Boolean);
|
|
33
|
+
return parseSegments(segments);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
15
36
|
function parseEpicLessonSlugFromEmbedUrl(urlString) {
|
|
16
37
|
const parseSegments = (segments) => {
|
|
17
38
|
if (segments.length === 0)
|
|
@@ -38,6 +59,12 @@ function parseEpicLessonSlugFromEmbedUrl(urlString) {
|
|
|
38
59
|
return parseSegments(segments);
|
|
39
60
|
}
|
|
40
61
|
}
|
|
62
|
+
function formatProductLessonUrl({ productHost, productSlug, lessonSlug, sectionSlug, }) {
|
|
63
|
+
// The product site will typically redirect to a section-specific path when needed.
|
|
64
|
+
return sectionSlug
|
|
65
|
+
? `https://${productHost}/workshops/${productSlug}/${sectionSlug}/${lessonSlug}`
|
|
66
|
+
: `https://${productHost}/workshops/${productSlug}/${lessonSlug}`;
|
|
67
|
+
}
|
|
41
68
|
function formatIssue(issue, workshopRoot) {
|
|
42
69
|
const icon = issue.level === 'error' ? chalk.red('❌') : chalk.yellow('⚠️ ');
|
|
43
70
|
const filePart = issue.file
|
|
@@ -291,7 +318,7 @@ async function fetchRemoteWorkshopLessonSlugs({ productHost, workshopSlug, }) {
|
|
|
291
318
|
message: `Product API response did not include an array "resources" field`,
|
|
292
319
|
};
|
|
293
320
|
}
|
|
294
|
-
const
|
|
321
|
+
const lessons = [];
|
|
295
322
|
for (const resource of resources) {
|
|
296
323
|
if (!resource || typeof resource !== 'object')
|
|
297
324
|
continue;
|
|
@@ -299,24 +326,28 @@ async function fetchRemoteWorkshopLessonSlugs({ productHost, workshopSlug, }) {
|
|
|
299
326
|
if (r._type === 'lesson') {
|
|
300
327
|
const slug = r.slug;
|
|
301
328
|
if (typeof slug === 'string')
|
|
302
|
-
|
|
329
|
+
lessons.push({ slug, sectionSlug: null });
|
|
303
330
|
continue;
|
|
304
331
|
}
|
|
305
332
|
if (r._type === 'section') {
|
|
306
|
-
const
|
|
307
|
-
|
|
333
|
+
const sectionSlug = typeof r.slug === 'string' && r.slug.trim().length > 0
|
|
334
|
+
? r.slug.trim()
|
|
335
|
+
: null;
|
|
336
|
+
const sectionLessons = r.lessons;
|
|
337
|
+
if (!Array.isArray(sectionLessons))
|
|
308
338
|
continue;
|
|
309
|
-
for (const lesson of
|
|
339
|
+
for (const lesson of sectionLessons) {
|
|
310
340
|
if (!lesson || typeof lesson !== 'object')
|
|
311
341
|
continue;
|
|
312
342
|
const l = lesson;
|
|
313
343
|
const slug = l.slug;
|
|
314
|
-
if (typeof slug === 'string')
|
|
315
|
-
|
|
344
|
+
if (typeof slug === 'string') {
|
|
345
|
+
lessons.push({ slug, sectionSlug });
|
|
346
|
+
}
|
|
316
347
|
}
|
|
317
348
|
}
|
|
318
349
|
}
|
|
319
|
-
return { status: 'success',
|
|
350
|
+
return { status: 'success', lessons };
|
|
320
351
|
}
|
|
321
352
|
async function checkMinContentLength({ fullPath, minChars, }) {
|
|
322
353
|
try {
|
|
@@ -789,14 +820,30 @@ export async function launchReadiness(options = {}) {
|
|
|
789
820
|
// -------------------------------------------------------
|
|
790
821
|
// 4) Remote product lesson list vs local embedded videos
|
|
791
822
|
// -------------------------------------------------------
|
|
792
|
-
const localLessonSlugs = new Set();
|
|
793
|
-
for (const embedUrl of embedOccurrences.keys()) {
|
|
794
|
-
const slug = parseEpicLessonSlugFromEmbedUrl(embedUrl);
|
|
795
|
-
if (slug)
|
|
796
|
-
localLessonSlugs.add(slug);
|
|
797
|
-
}
|
|
798
823
|
if (!skipRemote) {
|
|
799
824
|
if (productHost && productSlug) {
|
|
825
|
+
// Only consider embeds that belong to this workshop on the configured host.
|
|
826
|
+
// It's valid for content to include EpicVideo embeds from other workshops/paths.
|
|
827
|
+
const localProductLessonSlugs = new Set();
|
|
828
|
+
const normalizedConfigHost = normalizeHost(productHost);
|
|
829
|
+
for (const embedUrl of embedOccurrences.keys()) {
|
|
830
|
+
const lessonSlug = parseEpicLessonSlugFromEmbedUrl(embedUrl);
|
|
831
|
+
if (!lessonSlug)
|
|
832
|
+
continue;
|
|
833
|
+
const workshopSlug = parseEpicWorkshopSlugFromEmbedUrl(embedUrl);
|
|
834
|
+
if (!workshopSlug || workshopSlug !== productSlug)
|
|
835
|
+
continue;
|
|
836
|
+
try {
|
|
837
|
+
const url = new URL(embedUrl);
|
|
838
|
+
if (normalizeHost(url.host) !== normalizedConfigHost)
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
catch {
|
|
842
|
+
// Invalid URLs are reported elsewhere (host/path validation); ignore here.
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
localProductLessonSlugs.add(lessonSlug);
|
|
846
|
+
}
|
|
800
847
|
const remote = await fetchRemoteWorkshopLessonSlugs({
|
|
801
848
|
productHost,
|
|
802
849
|
workshopSlug: productSlug,
|
|
@@ -809,7 +856,21 @@ export async function launchReadiness(options = {}) {
|
|
|
809
856
|
});
|
|
810
857
|
}
|
|
811
858
|
else {
|
|
812
|
-
const
|
|
859
|
+
const remoteLessons = remote.lessons
|
|
860
|
+
.map((l) => ({
|
|
861
|
+
slug: stripEpicAiSlugSuffix(l.slug),
|
|
862
|
+
sectionSlug: l.sectionSlug
|
|
863
|
+
? stripEpicAiSlugSuffix(l.sectionSlug)
|
|
864
|
+
: null,
|
|
865
|
+
}))
|
|
866
|
+
.filter((l) => l.slug.trim().length > 0);
|
|
867
|
+
// Preserve the first sectionSlug seen for a given lesson slug.
|
|
868
|
+
const remoteLessonBySlug = new Map();
|
|
869
|
+
for (const l of remoteLessons) {
|
|
870
|
+
if (!remoteLessonBySlug.has(l.slug))
|
|
871
|
+
remoteLessonBySlug.set(l.slug, l);
|
|
872
|
+
}
|
|
873
|
+
const remoteLessonSlugs = [...remoteLessonBySlug.keys()];
|
|
813
874
|
if (remoteLessonSlugs.length === 0) {
|
|
814
875
|
issues.push({
|
|
815
876
|
level: 'error',
|
|
@@ -817,25 +878,52 @@ export async function launchReadiness(options = {}) {
|
|
|
817
878
|
message: 'Product API returned no lessons. Is the workshop published on the product site?',
|
|
818
879
|
});
|
|
819
880
|
}
|
|
820
|
-
const missing = remoteLessonSlugs.filter((slug) => !
|
|
881
|
+
const missing = remoteLessonSlugs.filter((slug) => !localProductLessonSlugs.has(slug));
|
|
821
882
|
if (missing.length) {
|
|
883
|
+
const formatted = missing
|
|
884
|
+
.sort()
|
|
885
|
+
.map((slug) => {
|
|
886
|
+
const remoteLesson = remoteLessonBySlug.get(slug);
|
|
887
|
+
return `- ${slug}: ${formatProductLessonUrl({
|
|
888
|
+
productHost,
|
|
889
|
+
productSlug,
|
|
890
|
+
lessonSlug: slug,
|
|
891
|
+
sectionSlug: remoteLesson?.sectionSlug ?? null,
|
|
892
|
+
})}`;
|
|
893
|
+
})
|
|
894
|
+
.join('\n');
|
|
822
895
|
issues.push({
|
|
823
896
|
level: 'error',
|
|
824
897
|
code: 'missing-product-videos-in-workshop',
|
|
825
|
-
message: `Missing videos in workshop for product lessons
|
|
826
|
-
.sort()
|
|
827
|
-
.join(', ')}`,
|
|
898
|
+
message: `Missing videos in workshop for product lessons:\n${formatted}`,
|
|
828
899
|
});
|
|
829
900
|
}
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
901
|
+
const remoteLessonSlugSet = new Set(remoteLessonSlugs);
|
|
902
|
+
for (const [embedUrl, usedBy] of embedOccurrences.entries()) {
|
|
903
|
+
const lessonSlug = parseEpicLessonSlugFromEmbedUrl(embedUrl);
|
|
904
|
+
if (!lessonSlug)
|
|
905
|
+
continue;
|
|
906
|
+
const workshopSlug = parseEpicWorkshopSlugFromEmbedUrl(embedUrl);
|
|
907
|
+
if (!workshopSlug || workshopSlug !== productSlug)
|
|
908
|
+
continue;
|
|
909
|
+
try {
|
|
910
|
+
const url = new URL(embedUrl);
|
|
911
|
+
if (normalizeHost(url.host) !== normalizedConfigHost)
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
catch {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
if (remoteLessonSlugSet.has(lessonSlug))
|
|
918
|
+
continue;
|
|
919
|
+
for (const file of usedBy) {
|
|
920
|
+
issues.push({
|
|
921
|
+
level: 'warning',
|
|
922
|
+
code: 'extra-local-videos',
|
|
923
|
+
message: `EpicVideo embed not present in the product lesson list: ${embedUrl}`,
|
|
924
|
+
file,
|
|
925
|
+
});
|
|
926
|
+
}
|
|
839
927
|
}
|
|
840
928
|
}
|
|
841
929
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "epicshop",
|
|
3
|
-
"version": "6.84.
|
|
3
|
+
"version": "6.84.8",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"build:watch": "nx watch --projects=epicshop -- nx run \\$NX_PROJECT_NAME:build"
|
|
106
106
|
},
|
|
107
107
|
"dependencies": {
|
|
108
|
-
"@epic-web/workshop-utils": "6.84.
|
|
108
|
+
"@epic-web/workshop-utils": "6.84.8",
|
|
109
109
|
"@inquirer/prompts": "^8.2.0",
|
|
110
110
|
"@sentry/node": "^10.38.0",
|
|
111
111
|
"chalk": "^5.6.2",
|