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 lessonSlugs = [];
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
- lessonSlugs.push(slug);
329
+ lessons.push({ slug, sectionSlug: null });
303
330
  continue;
304
331
  }
305
332
  if (r._type === 'section') {
306
- const lessons = r.lessons;
307
- if (!Array.isArray(lessons))
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 lessons) {
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
- lessonSlugs.push(slug);
344
+ if (typeof slug === 'string') {
345
+ lessons.push({ slug, sectionSlug });
346
+ }
316
347
  }
317
348
  }
318
349
  }
319
- return { status: 'success', lessonSlugs };
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 remoteLessonSlugs = Array.from(new Set(remote.lessonSlugs.map(stripEpicAiSlugSuffix)));
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) => !localLessonSlugs.has(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: ${missing
826
- .sort()
827
- .join(', ')}`,
898
+ message: `Missing videos in workshop for product lessons:\n${formatted}`,
828
899
  });
829
900
  }
830
- const extras = [...localLessonSlugs].filter((slug) => !remoteLessonSlugs.includes(slug));
831
- if (extras.length) {
832
- issues.push({
833
- level: 'warning',
834
- code: 'extra-local-videos',
835
- message: `Found EpicVideo embeds for lesson slugs not present in the product lesson list: ${extras
836
- .sort()
837
- .join(', ')}`,
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.6",
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.6",
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",