epicshop 6.86.0 → 6.87.0

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.
@@ -1,10 +1,12 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { debuglog } from 'node:util';
3
4
  import { compileMdx } from '@epic-web/workshop-utils/compile-mdx.server';
4
5
  import { getErrorMessage } from '@epic-web/workshop-utils/utils';
5
6
  import chalk from 'chalk';
6
7
  import { pathExists } from "../../utils/filesystem.js";
7
8
  import { collectStepDirectories, formatProductLessonUrl, fetchRemoteWorkshopLessons, isDirectory, resolveMdxFile, } from "./workshop-content-utils.js";
9
+ const debug = debuglog('epic:launch-readiness');
8
10
  function stripEpicAiSlugSuffix(value) {
9
11
  // EpicAI embeds sometimes include a `~...` suffix in the slug segment.
10
12
  return value.replace(/~[^ ]*$/, '');
@@ -62,6 +64,27 @@ function parseEpicLessonSlugFromEmbedUrl(urlString) {
62
64
  return parseSegments(segments);
63
65
  }
64
66
  }
67
+ function parseEmbedUrlVariant(urlString) {
68
+ const parseSegments = (segments) => {
69
+ const last = segments.at(-1) ?? '';
70
+ return {
71
+ hasProblemSuffix: last === 'problem',
72
+ hasSolutionSuffix: last === 'solution',
73
+ };
74
+ };
75
+ try {
76
+ const url = new URL(urlString);
77
+ const segments = url.pathname.split('/').filter(Boolean);
78
+ return parseSegments(segments);
79
+ }
80
+ catch {
81
+ // Fall back to naive parsing (best-effort).
82
+ const withoutHash = urlString.split('#')[0] ?? urlString;
83
+ const withoutQuery = withoutHash.split('?')[0] ?? withoutHash;
84
+ const segments = withoutQuery.split('/').filter(Boolean);
85
+ return parseSegments(segments);
86
+ }
87
+ }
65
88
  function formatIssue(issue, workshopRoot) {
66
89
  const icon = issue.level === 'error' ? chalk.red('❌') : chalk.yellow('⚠️ ');
67
90
  const filePart = issue.file
@@ -569,6 +592,7 @@ export async function launchReadiness(options = {}) {
569
592
  try {
570
593
  const compiled = await compileMdx(file.fullPath);
571
594
  const embeds = compiled.epicVideoEmbeds ?? [];
595
+ debug('file=%s kind=%s embeds=%d', file.relativePath, file.kind, embeds.length);
572
596
  if (embeds.length === 0) {
573
597
  issues.push({
574
598
  level: 'error',
@@ -579,6 +603,24 @@ export async function launchReadiness(options = {}) {
579
603
  continue;
580
604
  }
581
605
  for (const embed of embeds) {
606
+ const variant = parseEmbedUrlVariant(embed);
607
+ debug('embed file=%s kind=%s url=%s problemSuffix=%s solutionSuffix=%s', file.relativePath, file.kind, embed, variant.hasProblemSuffix, variant.hasSolutionSuffix);
608
+ if (file.kind === 'step-solution' && !variant.hasSolutionSuffix) {
609
+ issues.push({
610
+ level: 'error',
611
+ code: 'step-solution-video-missing-solution-suffix',
612
+ message: `Step solution files must use a /solution video URL suffix: ${embed}`,
613
+ file: file.fullPath,
614
+ });
615
+ }
616
+ if (file.kind === 'step-problem' && variant.hasSolutionSuffix) {
617
+ issues.push({
618
+ level: 'error',
619
+ code: 'step-problem-video-uses-solution-suffix',
620
+ message: `Step problem files must not use a /solution video URL suffix: ${embed}`,
621
+ file: file.fullPath,
622
+ });
623
+ }
582
624
  const set = embedOccurrences.get(embed) ?? new Set();
583
625
  set.add(file.fullPath);
584
626
  embedOccurrences.set(embed, set);
@@ -711,6 +753,7 @@ export async function launchReadiness(options = {}) {
711
753
  }
712
754
  localProductLessonSlugs.add(lessonSlug);
713
755
  }
756
+ debug('remote-check workshop=%s localProductLessonSlugs=%d', productSlug, localProductLessonSlugs.size);
714
757
  const remote = await fetchRemoteWorkshopLessons({
715
758
  productHost,
716
759
  workshopSlug: productSlug,
@@ -739,6 +782,7 @@ export async function launchReadiness(options = {}) {
739
782
  remoteLessonBySlug.set(l.slug, l);
740
783
  }
741
784
  const remoteLessonSlugs = [...remoteLessonBySlug.keys()];
785
+ debug('remote-check workshop=%s remoteLessonSlugs=%d moduleType=%s', productSlug, remoteLessonSlugs.length, remoteModuleType);
742
786
  if (remoteLessonSlugs.length === 0) {
743
787
  issues.push({
744
788
  level: 'error',
@@ -748,6 +792,7 @@ export async function launchReadiness(options = {}) {
748
792
  }
749
793
  const missing = remoteLessonSlugs.filter((slug) => !localProductLessonSlugs.has(slug));
750
794
  if (missing.length) {
795
+ debug('remote-check missing-lesson-slugs=%o', missing.slice().sort());
751
796
  const formatted = missing
752
797
  .sort()
753
798
  .map((slug) => {
@@ -234,6 +234,12 @@ function upsertTitleEpicVideo({ content, url, }) {
234
234
  function formatNumberedList(items, { startAt = 1 } = {}) {
235
235
  return items.map((item, index) => `${startAt + index}. ${item}`).join('\n');
236
236
  }
237
+ function getTargetLessonUrl({ baseUrl, fileKind, }) {
238
+ if (fileKind === 'step-solution') {
239
+ return `${baseUrl.replace(/\/+$/, '')}/solution`;
240
+ }
241
+ return baseUrl;
242
+ }
237
243
  function buildFileLessonSlotPlans(files) {
238
244
  const plans = [];
239
245
  const stepSlotByKey = new Map();
@@ -385,13 +391,17 @@ export async function setVideos(options = {}) {
385
391
  const lesson = remoteLessons[plan.lessonSlotIndex];
386
392
  if (!lesson)
387
393
  continue;
388
- const targetUrl = formatProductLessonUrl({
394
+ const baseLessonUrl = formatProductLessonUrl({
389
395
  productHost,
390
396
  productSlug,
391
397
  moduleType: remoteModuleType,
392
398
  lessonSlug: lesson.slug,
393
399
  sectionSlug: lesson.sectionSlug,
394
400
  });
401
+ const targetUrl = getTargetLessonUrl({
402
+ baseUrl: baseLessonUrl,
403
+ fileKind: plan.file.kind,
404
+ });
395
405
  let currentContent = '';
396
406
  try {
397
407
  currentContent = await fs.readFile(plan.file.fullPath, 'utf8');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epicshop",
3
- "version": "6.86.0",
3
+ "version": "6.87.0",
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.86.0",
108
+ "@epic-web/workshop-utils": "6.87.0",
109
109
  "@inquirer/prompts": "^8.2.0",
110
110
  "@sentry/node": "^10.38.0",
111
111
  "chalk": "^5.6.2",