epicshop 6.84.5 → 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
@@ -202,21 +227,6 @@ async function buildExpectedFiles({ workshopRoot, exerciseDirName, }) {
202
227
  relativePath: path.relative(workshopRoot, problemReadme),
203
228
  });
204
229
  }
205
- const problemFinished = await resolveMdxFile(problemDir, 'FINISHED');
206
- if (!problemFinished) {
207
- issues.push({
208
- level: 'error',
209
- code: 'missing-step-problem-finished',
210
- message: `Missing step problem FINISHED.mdx for step ${stepNumber}`,
211
- file: path.join(problemDir, 'FINISHED.mdx'),
212
- });
213
- }
214
- else {
215
- contentFiles.push({
216
- fullPath: problemFinished,
217
- relativePath: path.relative(workshopRoot, problemFinished),
218
- });
219
- }
220
230
  }
221
231
  for (const solutionDir of dirs.solutions) {
222
232
  const solutionReadme = await resolveMdxFile(solutionDir, 'README');
@@ -239,21 +249,6 @@ async function buildExpectedFiles({ workshopRoot, exerciseDirName, }) {
239
249
  relativePath: path.relative(workshopRoot, solutionReadme),
240
250
  });
241
251
  }
242
- const solutionFinished = await resolveMdxFile(solutionDir, 'FINISHED');
243
- if (!solutionFinished) {
244
- issues.push({
245
- level: 'error',
246
- code: 'missing-step-solution-finished',
247
- message: `Missing step solution FINISHED.mdx for step ${stepNumber}`,
248
- file: path.join(solutionDir, 'FINISHED.mdx'),
249
- });
250
- }
251
- else {
252
- contentFiles.push({
253
- fullPath: solutionFinished,
254
- relativePath: path.relative(workshopRoot, solutionFinished),
255
- });
256
- }
257
252
  }
258
253
  }
259
254
  return { files, contentFiles, issues };
@@ -725,7 +720,7 @@ export async function launchReadiness(options = {}) {
725
720
  });
726
721
  }
727
722
  }
728
- // Also scan the remaining required MDX files for EpicVideo embeds (e.g. step FINISHED.mdx),
723
+ // Also scan the remaining required MDX files for EpicVideo embeds,
729
724
  // but do not require that they include a video.
730
725
  {
731
726
  const videoFilePaths = new Set(filesToCheck.map((f) => f.fullPath));
@@ -819,14 +814,30 @@ export async function launchReadiness(options = {}) {
819
814
  // -------------------------------------------------------
820
815
  // 4) Remote product lesson list vs local embedded videos
821
816
  // -------------------------------------------------------
822
- const localLessonSlugs = new Set();
823
- for (const embedUrl of embedOccurrences.keys()) {
824
- const slug = parseEpicLessonSlugFromEmbedUrl(embedUrl);
825
- if (slug)
826
- localLessonSlugs.add(slug);
827
- }
828
817
  if (!skipRemote) {
829
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
+ }
830
841
  const remote = await fetchRemoteWorkshopLessonSlugs({
831
842
  productHost,
832
843
  workshopSlug: productSlug,
@@ -847,25 +858,48 @@ export async function launchReadiness(options = {}) {
847
858
  message: 'Product API returned no lessons. Is the workshop published on the product site?',
848
859
  });
849
860
  }
850
- const missing = remoteLessonSlugs.filter((slug) => !localLessonSlugs.has(slug));
861
+ const missing = remoteLessonSlugs.filter((slug) => !localProductLessonSlugs.has(slug));
851
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');
852
871
  issues.push({
853
872
  level: 'error',
854
873
  code: 'missing-product-videos-in-workshop',
855
- message: `Missing videos in workshop for product lessons: ${missing
856
- .sort()
857
- .join(', ')}`,
874
+ message: `Missing videos in workshop for product lessons:\n${formatted}`,
858
875
  });
859
876
  }
860
- const extras = [...localLessonSlugs].filter((slug) => !remoteLessonSlugs.includes(slug));
861
- if (extras.length) {
862
- issues.push({
863
- level: 'warning',
864
- code: 'extra-local-videos',
865
- message: `Found EpicVideo embeds for lesson slugs not present in the product lesson list: ${extras
866
- .sort()
867
- .join(', ')}`,
868
- });
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
+ }
869
903
  }
870
904
  }
871
905
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epicshop",
3
- "version": "6.84.5",
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.5",
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",