epicshop 6.84.6 → 6.84.7

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,10 @@ function parseEpicLessonSlugFromEmbedUrl(urlString) {
38
59
  return parseSegments(segments);
39
60
  }
40
61
  }
62
+ function formatProductLessonUrl({ productHost, productSlug, lessonSlug, }) {
63
+ // The product site will typically redirect to a section-specific path when needed.
64
+ return `https://${productHost}/workshops/${productSlug}/${lessonSlug}`;
65
+ }
41
66
  function formatIssue(issue, workshopRoot) {
42
67
  const icon = issue.level === 'error' ? chalk.red('❌') : chalk.yellow('⚠️ ');
43
68
  const filePart = issue.file
@@ -789,14 +814,30 @@ export async function launchReadiness(options = {}) {
789
814
  // -------------------------------------------------------
790
815
  // 4) Remote product lesson list vs local embedded videos
791
816
  // -------------------------------------------------------
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
817
  if (!skipRemote) {
799
818
  if (productHost && productSlug) {
819
+ // Only consider embeds that belong to this workshop on the configured host.
820
+ // It's valid for content to include EpicVideo embeds from other workshops/paths.
821
+ const localProductLessonSlugs = new Set();
822
+ const normalizedConfigHost = normalizeHost(productHost);
823
+ for (const embedUrl of embedOccurrences.keys()) {
824
+ const lessonSlug = parseEpicLessonSlugFromEmbedUrl(embedUrl);
825
+ if (!lessonSlug)
826
+ continue;
827
+ const workshopSlug = parseEpicWorkshopSlugFromEmbedUrl(embedUrl);
828
+ if (!workshopSlug || workshopSlug !== productSlug)
829
+ continue;
830
+ try {
831
+ const url = new URL(embedUrl);
832
+ if (normalizeHost(url.host) !== normalizedConfigHost)
833
+ continue;
834
+ }
835
+ catch {
836
+ // Invalid URLs are reported elsewhere (host/path validation); ignore here.
837
+ continue;
838
+ }
839
+ localProductLessonSlugs.add(lessonSlug);
840
+ }
800
841
  const remote = await fetchRemoteWorkshopLessonSlugs({
801
842
  productHost,
802
843
  workshopSlug: productSlug,
@@ -817,25 +858,48 @@ export async function launchReadiness(options = {}) {
817
858
  message: 'Product API returned no lessons. Is the workshop published on the product site?',
818
859
  });
819
860
  }
820
- const missing = remoteLessonSlugs.filter((slug) => !localLessonSlugs.has(slug));
861
+ const missing = remoteLessonSlugs.filter((slug) => !localProductLessonSlugs.has(slug));
821
862
  if (missing.length) {
863
+ const formatted = missing
864
+ .sort()
865
+ .map((slug) => `- ${slug}: ${formatProductLessonUrl({
866
+ productHost,
867
+ productSlug,
868
+ lessonSlug: slug,
869
+ })}`)
870
+ .join('\n');
822
871
  issues.push({
823
872
  level: 'error',
824
873
  code: 'missing-product-videos-in-workshop',
825
- message: `Missing videos in workshop for product lessons: ${missing
826
- .sort()
827
- .join(', ')}`,
874
+ message: `Missing videos in workshop for product lessons:\n${formatted}`,
828
875
  });
829
876
  }
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
- });
877
+ const remoteLessonSlugSet = new Set(remoteLessonSlugs);
878
+ for (const [embedUrl, usedBy] of embedOccurrences.entries()) {
879
+ const lessonSlug = parseEpicLessonSlugFromEmbedUrl(embedUrl);
880
+ if (!lessonSlug)
881
+ continue;
882
+ const workshopSlug = parseEpicWorkshopSlugFromEmbedUrl(embedUrl);
883
+ if (!workshopSlug || workshopSlug !== productSlug)
884
+ continue;
885
+ try {
886
+ const url = new URL(embedUrl);
887
+ if (normalizeHost(url.host) !== normalizedConfigHost)
888
+ continue;
889
+ }
890
+ catch {
891
+ continue;
892
+ }
893
+ if (remoteLessonSlugSet.has(lessonSlug))
894
+ continue;
895
+ for (const file of usedBy) {
896
+ issues.push({
897
+ level: 'warning',
898
+ code: 'extra-local-videos',
899
+ message: `EpicVideo embed not present in the product lesson list: ${embedUrl}`,
900
+ file,
901
+ });
902
+ }
839
903
  }
840
904
  }
841
905
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epicshop",
3
- "version": "6.84.6",
3
+ "version": "6.84.7",
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.7",
109
109
  "@inquirer/prompts": "^8.2.0",
110
110
  "@sentry/node": "^10.38.0",
111
111
  "chalk": "^5.6.2",