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
|
|
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) => !
|
|
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
|
|
856
|
-
.sort()
|
|
857
|
-
.join(', ')}`,
|
|
874
|
+
message: `Missing videos in workshop for product lessons:\n${formatted}`,
|
|
858
875
|
});
|
|
859
876
|
}
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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.
|
|
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.
|
|
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",
|