@epic-web/workshop-utils 6.72.0 → 6.72.1

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.
@@ -2101,7 +2101,7 @@ export declare function getWorkshopInstructions({ request, }?: {
2101
2101
  readonly status: "success";
2102
2102
  readonly code: string;
2103
2103
  readonly title: string | null;
2104
- readonly epicVideoEmbeds: Array<string>;
2104
+ readonly epicVideoEmbeds: string[];
2105
2105
  } | {
2106
2106
  readonly status: "error";
2107
2107
  readonly error: string;
@@ -2116,7 +2116,7 @@ export declare function getExtrasInstructions({ request, }?: {
2116
2116
  readonly status: "success";
2117
2117
  readonly code: string;
2118
2118
  readonly title: string | null;
2119
- readonly epicVideoEmbeds: Array<string>;
2119
+ readonly epicVideoEmbeds: string[];
2120
2120
  } | {
2121
2121
  readonly status: "error";
2122
2122
  readonly error: string;
@@ -2131,7 +2131,7 @@ export declare function getWorkshopFinished({ request, }?: {
2131
2131
  readonly status: "success";
2132
2132
  readonly code: string;
2133
2133
  readonly title: string | null;
2134
- readonly epicVideoEmbeds: Array<string>;
2134
+ readonly epicVideoEmbeds: string[];
2135
2135
  } | {
2136
2136
  readonly status: "error";
2137
2137
  readonly error: string;
@@ -217,6 +217,7 @@ async function isDirectoryEmpty(dirPath) {
217
217
  cache: directoryEmptyCache,
218
218
  ttl: 1000 * 60 * 5,
219
219
  swr: 1000 * 60 * 20,
220
+ checkValue: z.boolean(),
220
221
  forceFresh: await getForceFreshForDir(directoryEmptyCache.get(dirPath), dirPath),
221
222
  getFreshValue: async () => {
222
223
  try {
@@ -767,6 +768,7 @@ export async function getPlaygroundApp({ timings, request, } = {}) {
767
768
  cache: playgroundAppCache,
768
769
  ttl: 1000 * 60 * 5,
769
770
  swr: 1000 * 60 * 60 * 24 * 30,
771
+ checkValue: PlaygroundAppSchema.nullable(),
770
772
  timings,
771
773
  timingKey: playgroundDir.replace(`${playgroundDir}${path.sep}`, ''),
772
774
  request,
@@ -892,6 +894,7 @@ async function getExtraApps({ timings, request, } = {}) {
892
894
  cache: extraAppCache,
893
895
  ttl: 1000 * 60 * 5,
894
896
  swr: 1000 * 60 * 60 * 24 * 30,
897
+ checkValue: ExtraAppSchema.nullable(),
895
898
  timings,
896
899
  timingKey: extraDir.replace(`${extraDirInfo.fullPath}${path.sep}`, ''),
897
900
  request,
@@ -964,6 +967,7 @@ async function getSolutionApps({ timings, request, } = {}) {
964
967
  request,
965
968
  ttl: 1000 * 60 * 5,
966
969
  swr: 1000 * 60 * 60 * 24 * 30,
970
+ checkValue: SolutionAppSchema.nullable(),
967
971
  forceFresh: await getForceFreshForDir(solutionAppCache.get(solutionDir), solutionDir),
968
972
  getFreshValue: async () => {
969
973
  return getSolutionAppFromPath(solutionDir, request).catch((error) => {
@@ -1034,6 +1038,7 @@ async function getProblemApps({ timings, request, } = {}) {
1034
1038
  request,
1035
1039
  ttl: 1000 * 60 * 5,
1036
1040
  swr: 1000 * 60 * 60 * 24 * 30,
1041
+ checkValue: ProblemAppSchema.nullable(),
1037
1042
  forceFresh: await getForceFreshForDir(problemAppCache.get(problemDir), problemDir, solutionDir),
1038
1043
  getFreshValue: async () => {
1039
1044
  return getProblemAppFromPath(problemDir).catch((error) => {
@@ -2,6 +2,18 @@ import * as C from '@epic-web/cachified';
2
2
  import { type CreateReporter } from '@epic-web/cachified';
3
3
  import z from 'zod';
4
4
  import { type Timings } from "./timing.server.js";
5
+ type DiffStatus = 'renamed' | 'modified' | 'deleted' | 'added' | 'unknown';
6
+ type DiffFile = {
7
+ status: DiffStatus;
8
+ path: string;
9
+ line: number;
10
+ };
11
+ type CompiledCodeResult = {
12
+ outputFiles?: Array<unknown>;
13
+ errors: Array<unknown>;
14
+ warnings: Array<unknown>;
15
+ };
16
+ type OgCacheValue = string | Uint8Array;
5
17
  /**
6
18
  * Creates a cachified reporter that integrates with the Epic Workshop logger system.
7
19
  * Uses the pattern `epic:cache:{name-of-cache}` for logger namespaces.
@@ -156,24 +168,24 @@ export declare const playgroundAppCache: C.Cache<{
156
168
  epicVideoEmbeds?: string[] | undefined;
157
169
  }>;
158
170
  export declare const diffCodeCache: C.Cache<string>;
159
- export declare const diffFilesCache: C.Cache<string>;
171
+ export declare const diffFilesCache: C.Cache<DiffFile[]>;
160
172
  export declare const copyUnignoredFilesCache: {
161
173
  name: string;
162
- set: (key: string, value: C.CacheEntry<string>) => C.CacheEntry<string>;
163
- get: (key: string) => C.CacheEntry<string> | undefined;
174
+ set: (key: string, value: C.CacheEntry<boolean>) => C.CacheEntry<boolean>;
175
+ get: (key: string) => C.CacheEntry<boolean> | undefined;
164
176
  delete: (key: string) => boolean;
165
177
  };
166
178
  export declare const compiledMarkdownCache: C.Cache<string>;
167
179
  export declare const compiledCodeCache: {
168
180
  name: string;
169
- set: (key: string, value: C.CacheEntry<string>) => C.CacheEntry<string>;
170
- get: (key: string) => C.CacheEntry<string> | undefined;
181
+ set: (key: string, value: C.CacheEntry<CompiledCodeResult>) => C.CacheEntry<CompiledCodeResult>;
182
+ get: (key: string) => C.CacheEntry<CompiledCodeResult> | undefined;
171
183
  delete: (key: string) => boolean;
172
184
  };
173
185
  export declare const ogCache: {
174
186
  name: string;
175
- set: (key: string, value: C.CacheEntry<string>) => C.CacheEntry<string>;
176
- get: (key: string) => C.CacheEntry<string> | undefined;
187
+ set: (key: string, value: C.CacheEntry<OgCacheValue>) => C.CacheEntry<OgCacheValue>;
188
+ get: (key: string) => C.CacheEntry<OgCacheValue> | undefined;
177
189
  delete: (key: string) => boolean;
178
190
  };
179
191
  export declare const compiledInstructionMarkdownCache: C.Cache<{
@@ -430,3 +442,4 @@ export declare function shouldForceFresh({ forceFresh, request, key, }: {
430
442
  request?: Request;
431
443
  key?: string;
432
444
  }): Promise<boolean>;
445
+ export {};
@@ -7,6 +7,6 @@ export declare function compileMdx(file: string, { request, timings, forceFresh,
7
7
  }): Promise<{
8
8
  code: string;
9
9
  title: string | null;
10
- epicVideoEmbeds: Array<string>;
10
+ epicVideoEmbeds: string[];
11
11
  }>;
12
12
  export declare function compileMarkdownString(markdownString: string): Promise<string>;
@@ -11,10 +11,16 @@ import rehypeAutolinkHeadings from 'rehype-autolink-headings';
11
11
  import emoji from 'remark-emoji';
12
12
  import gfm from 'remark-gfm';
13
13
  import { visit } from 'unist-util-visit';
14
+ import { z } from 'zod';
14
15
  import { cachified, compiledInstructionMarkdownCache, compiledMarkdownCache, shouldForceFresh, } from "./cache.server.js";
15
16
  import { checkConnection } from "./utils.server.js";
16
17
  const themeCookieName = 'EpicShop_theme';
17
18
  const themeHintCookieName = 'EpicShop_CH-prefers-color-scheme';
19
+ const CompiledInstructionMarkdownSchema = z.object({
20
+ code: z.string(),
21
+ title: z.string().nullable(),
22
+ epicVideoEmbeds: z.array(z.string()),
23
+ });
18
24
  function getMermaidTheme(request) {
19
25
  if (!request)
20
26
  return 'default';
@@ -182,6 +188,7 @@ export async function compileMdx(file, { request, timings, forceFresh, } = {}) {
182
188
  request,
183
189
  timings,
184
190
  forceFresh,
191
+ checkValue: CompiledInstructionMarkdownSchema,
185
192
  getFreshValue: () => compileMdxImpl(file, { mermaidTheme }),
186
193
  });
187
194
  }
@@ -277,6 +284,7 @@ export async function compileMarkdownString(markdownString) {
277
284
  key: md5(markdownString),
278
285
  cache: compiledMarkdownCache,
279
286
  ttl: 1000 * 60 * 60 * 24,
287
+ checkValue: z.string(),
280
288
  getFreshValue: async () => {
281
289
  try {
282
290
  verboseLog(`Compiling string`, markdownString);
@@ -5,7 +5,7 @@ export declare function getDiffFiles(app1: App, app2: App, { forceFresh, timings
5
5
  timings?: Timings;
6
6
  request?: Request;
7
7
  }): Promise<{
8
- status: "renamed" | "modified" | "deleted" | "added" | "unknown";
8
+ status: "unknown" | "renamed" | "modified" | "deleted" | "added";
9
9
  path: string;
10
10
  line: number;
11
11
  }[]>;
@@ -7,6 +7,7 @@ import fsExtra from 'fs-extra';
7
7
  import ignore from 'ignore';
8
8
  import parseGitDiff from 'parse-git-diff';
9
9
  import { bundledLanguagesInfo } from 'shiki/langs';
10
+ import { z } from 'zod';
10
11
  import { getForceFreshForDir, getRelativePath, getWorkshopRoot, modifiedTimes, } from "./apps.server.js";
11
12
  import { cachified, copyUnignoredFilesCache, diffCodeCache, diffFilesCache, } from "./cache.server.js";
12
13
  import { compileMarkdownString } from "./compile-mdx.server.js";
@@ -14,6 +15,37 @@ import { modifiedMoreRecentlyThan } from "./modified-time.server.js";
14
15
  const epicshopTempDir = path.join(os.tmpdir(), 'epicshop');
15
16
  const isDeployed = getEnv().EPICSHOP_DEPLOYED;
16
17
  const diffTmpDir = path.join(epicshopTempDir, 'diff');
18
+ const DiffStatusSchema = z.enum([
19
+ 'renamed',
20
+ 'modified',
21
+ 'deleted',
22
+ 'added',
23
+ 'unknown',
24
+ ]);
25
+ const DiffFileSchema = z.object({
26
+ status: DiffStatusSchema,
27
+ path: z.string(),
28
+ line: z.number(),
29
+ });
30
+ function getDiffStatus(fileType) {
31
+ switch (fileType) {
32
+ case 'ChangedFile': {
33
+ return 'modified';
34
+ }
35
+ case 'AddedFile': {
36
+ return 'added';
37
+ }
38
+ case 'DeletedFile': {
39
+ return 'deleted';
40
+ }
41
+ case 'RenamedFile': {
42
+ return 'renamed';
43
+ }
44
+ default: {
45
+ return 'unknown';
46
+ }
47
+ }
48
+ }
17
49
  /**
18
50
  * Converts a diff file path to a relative path for display and lookup.
19
51
  * - Removes leading/trailing quotes.
@@ -165,6 +197,7 @@ async function copyUnignoredFiles(srcDir, destDir, ignoreList) {
165
197
  await cachified({
166
198
  key,
167
199
  cache: copyUnignoredFilesCache,
200
+ checkValue: z.boolean(),
168
201
  forceFresh: await getForceFreshForDir(copyUnignoredFilesCache.get(key), srcDir),
169
202
  async getFreshValue() {
170
203
  // @ts-ignore 🤷‍♂️ weird module stuff
@@ -177,6 +210,7 @@ async function copyUnignoredFiles(srcDir, destDir, ignoreList) {
177
210
  return !ig.ignores(path.relative(srcDir, file));
178
211
  },
179
212
  });
213
+ return true;
180
214
  },
181
215
  });
182
216
  }
@@ -266,6 +300,7 @@ export async function getDiffFiles(app1, app2, { forceFresh, timings, request, }
266
300
  forceFresh: forceFresh || (await getForceFreshForDiff(app1, app2, cacheEntry)),
267
301
  timings,
268
302
  request,
303
+ checkValue: DiffFileSchema.array(),
269
304
  getFreshValue: () => getDiffFilesImpl(app1, app2),
270
305
  });
271
306
  return result;
@@ -288,12 +323,6 @@ async function getDiffFilesImpl(app1, app2) {
288
323
  ], { cwd: diffTmpDir }).catch((e) => e);
289
324
  void fsExtra.remove(app1CopyPath).catch(() => { });
290
325
  void fsExtra.remove(app2CopyPath).catch(() => { });
291
- const typesMap = {
292
- ChangedFile: 'modified',
293
- AddedFile: 'added',
294
- DeletedFile: 'deleted',
295
- RenamedFile: 'renamed',
296
- };
297
326
  const parsed = parseGitDiff(diffOutput, { noPrefix: true });
298
327
  const testFiles = Array.from(new Set([...getAppTestFiles(app1), ...getAppTestFiles(app2)]));
299
328
  const startLine = (file) => {
@@ -309,8 +338,7 @@ async function getDiffFilesImpl(app1, app2) {
309
338
  };
310
339
  return parsed.files
311
340
  .map((file) => ({
312
- // prettier-ignore
313
- status: (typesMap[file.type] ?? 'unknown'),
341
+ status: getDiffStatus(file.type),
314
342
  path: diffPathToRelative(file.type === 'RenamedFile' ? file.pathBefore : file.path),
315
343
  line: startLine(file),
316
344
  }))
@@ -325,6 +353,7 @@ export async function getDiffCode(app1, app2, { forceFresh, timings, request, }
325
353
  forceFresh: forceFresh || (await getForceFreshForDiff(app1, app2, cacheEntry)),
326
354
  timings,
327
355
  request,
356
+ checkValue: z.string(),
328
357
  getFreshValue: () => getDiffCodeImpl(app1, app2),
329
358
  });
330
359
  return result;
@@ -67,71 +67,27 @@ export declare function checkForUpdates(): Promise<{
67
67
  readonly message: null;
68
68
  }>;
69
69
  export declare function checkForUpdatesCached(): Promise<{
70
+ updatesAvailable: boolean;
71
+ repoUpdatesAvailable: boolean;
72
+ dependenciesNeedInstall: boolean;
73
+ updateNotificationId: string | null;
74
+ commitsAhead: number | null;
75
+ commitsBehind: number | null;
76
+ localCommit: string | null;
77
+ remoteCommit: string | null;
78
+ diffLink: string | null;
79
+ message: string | null;
80
+ } | {
70
81
  readonly updatesAvailable: false;
82
+ readonly repoUpdatesAvailable: false;
71
83
  readonly dependenciesNeedInstall: false;
72
84
  readonly updateNotificationId: null;
73
- readonly message: "The app is deployed";
74
- readonly repoUpdatesAvailable: boolean;
75
- readonly commitsAhead: null;
76
- readonly commitsBehind: null;
77
- readonly localCommit: null;
78
- readonly remoteCommit: null;
79
- readonly diffLink: null;
80
- } | {
81
- readonly message: "You are offline";
82
- readonly updatesAvailable: boolean;
83
- readonly repoUpdatesAvailable: boolean;
84
- readonly dependenciesNeedInstall: boolean;
85
- readonly updateNotificationId: string | null;
86
- readonly commitsAhead: null;
87
- readonly commitsBehind: null;
88
- readonly localCommit: null;
89
- readonly remoteCommit: null;
90
- readonly diffLink: null;
91
- } | {
92
- readonly message: "Not in a git repo";
93
- readonly updatesAvailable: boolean;
94
- readonly repoUpdatesAvailable: boolean;
95
- readonly dependenciesNeedInstall: boolean;
96
- readonly updateNotificationId: string | null;
97
85
  readonly commitsAhead: null;
98
86
  readonly commitsBehind: null;
99
87
  readonly localCommit: null;
100
88
  readonly remoteCommit: null;
101
89
  readonly diffLink: null;
102
- } | {
103
- readonly message: "Cannot find remote";
104
- readonly updatesAvailable: boolean;
105
- readonly repoUpdatesAvailable: boolean;
106
- readonly dependenciesNeedInstall: boolean;
107
- readonly updateNotificationId: string | null;
108
- readonly commitsAhead: null;
109
- readonly commitsBehind: null;
110
- readonly localCommit: null;
111
- readonly remoteCommit: null;
112
- readonly diffLink: null;
113
- } | {
114
- readonly updatesAvailable: boolean;
115
- readonly repoUpdatesAvailable: boolean;
116
- readonly dependenciesNeedInstall: boolean;
117
- readonly updateNotificationId: string | null;
118
- readonly commitsAhead: number;
119
- readonly commitsBehind: number;
120
- readonly localCommit: string;
121
- readonly remoteCommit: string;
122
- readonly diffLink: string | null;
123
- readonly message: null;
124
- } | {
125
- readonly localCommit: string | null;
126
- readonly remoteCommit: string | null;
127
- readonly diffLink: string | null;
128
- readonly updatesAvailable: boolean;
129
- readonly repoUpdatesAvailable: boolean;
130
- readonly dependenciesNeedInstall: boolean;
131
- readonly updateNotificationId: string | null;
132
- readonly commitsAhead: null;
133
- readonly commitsBehind: null;
134
- readonly message: null;
90
+ readonly message: "The app is deployed";
135
91
  }>;
136
92
  export declare function updateLocalRepo(): Promise<{
137
93
  readonly status: "success";
@@ -2,6 +2,7 @@ import "./init-env.js";
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { execa, execaCommand } from 'execa';
5
+ import { z } from 'zod';
5
6
  import { getWorkshopRoot } from "./apps.server.js";
6
7
  import { cachified, checkForUpdatesCache } from "./cache.server.js";
7
8
  import { getWorkshopConfig } from "./config.server.js";
@@ -11,6 +12,18 @@ import { getInstallCommand, getWorkspaceInstallStatus, } from "./package-install
11
12
  import { checkConnection } from "./utils.server.js";
12
13
  import { getErrorMessage } from "./utils.js";
13
14
  const gitLog = logger('epic:git');
15
+ const CheckForUpdatesSchema = z.object({
16
+ updatesAvailable: z.boolean(),
17
+ repoUpdatesAvailable: z.boolean(),
18
+ dependenciesNeedInstall: z.boolean(),
19
+ updateNotificationId: z.string().nullable(),
20
+ commitsAhead: z.number().nullable(),
21
+ commitsBehind: z.number().nullable(),
22
+ localCommit: z.string().nullable(),
23
+ remoteCommit: z.string().nullable(),
24
+ diffLink: z.string().nullable(),
25
+ message: z.string().nullable(),
26
+ });
14
27
  function dirHasTrackedFiles(cwd, dirPath) {
15
28
  return execa('git', ['ls-files', dirPath], { cwd }).then((s) => s.stdout.trim().length > 0, () => true);
16
29
  }
@@ -170,6 +183,7 @@ export async function checkForUpdatesCached() {
170
183
  ttl: 1000 * 60,
171
184
  swr: 1000 * 60 * 60 * 24,
172
185
  key,
186
+ checkValue: CheckForUpdatesSchema,
173
187
  getFreshValue: checkForUpdates,
174
188
  cache: checkForUpdatesCache,
175
189
  });
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { isGitIgnored } from 'globby';
5
5
  import PQueue from 'p-queue';
6
+ import { z } from 'zod';
6
7
  import { cachified, dirModifiedTimeCache } from "./cache.server.js";
7
8
  async function getDirModifiedTime(dir, { forceFresh = false } = {}) {
8
9
  const result = await cachified({
@@ -10,6 +11,7 @@ async function getDirModifiedTime(dir, { forceFresh = false } = {}) {
10
11
  cache: dirModifiedTimeCache,
11
12
  ttl: 200,
12
13
  forceFresh,
14
+ checkValue: z.number(),
13
15
  getFreshValue: () => getDirModifiedTimeImpl(dir),
14
16
  });
15
17
  return result;
@@ -22,6 +22,21 @@ const NotificationSchema = z.object({
22
22
  .nullable()
23
23
  .transform((val) => (val ? new Date(val) : null)),
24
24
  });
25
+ // Schema for validating cached notifications (post-transform, with Date objects)
26
+ const CachedNotificationSchema = z.object({
27
+ id: z.string(),
28
+ title: z.string(),
29
+ message: z.string(),
30
+ link: z.string().optional(),
31
+ type: z.enum(['info', 'warning', 'danger']),
32
+ products: z
33
+ .array(z.object({
34
+ host: z.string(),
35
+ slug: z.string().optional(),
36
+ }))
37
+ .optional(),
38
+ expiresAt: z.instanceof(Date).nullable(),
39
+ });
25
40
  async function getRemoteNotifications() {
26
41
  return cachified({
27
42
  key: 'notifications',
@@ -29,6 +44,7 @@ async function getRemoteNotifications() {
29
44
  ttl: 1000 * 60 * 60 * 6,
30
45
  swr: 1000 * 60 * 60 * 24,
31
46
  offlineFallbackValue: [],
47
+ checkValue: CachedNotificationSchema.array(),
32
48
  async getFreshValue() {
33
49
  const URL = 'https://gist.github.com/kentcdodds/c3aaa5141f591cdbb0e6bfcacd361f39';
34
50
  const filename = 'notifications.json5';
@@ -1,4 +1,5 @@
1
1
  import "./init-env.js";
2
+ import { z } from 'zod';
2
3
  import { cachified, connectionCache } from "./cache.server.js";
3
4
  import { logger } from "./logger.js";
4
5
  export { dayjs } from "./utils.js";
@@ -42,6 +43,7 @@ export async function checkConnection({ request, timings, } = {}) {
42
43
  timings,
43
44
  key: 'connected',
44
45
  ttl: 1000 * 10,
46
+ checkValue: z.boolean(),
45
47
  async getFreshValue(context) {
46
48
  connectionLog('getting fresh connection value');
47
49
  const isOnline = await raceConnectivity();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epic-web/workshop-utils",
3
- "version": "6.72.0",
3
+ "version": "6.72.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -191,7 +191,7 @@
191
191
  "@mdx-js/mdx": "^3.1.1",
192
192
  "@playwright/test": "^1.57.0",
193
193
  "@react-router/node": "^7.12.0",
194
- "@sentry/react-router": "^10.35.0",
194
+ "@sentry/react-router": "^10.36.0",
195
195
  "@testing-library/dom": "^10.4.1",
196
196
  "@testing-library/jest-dom": "^6.9.1",
197
197
  "@total-typescript/ts-reset": "^0.6.1",
@@ -221,7 +221,7 @@
221
221
  "mdx-bundler": "^10.1.1",
222
222
  "p-queue": "^9.1.0",
223
223
  "parse-git-diff": "^0.0.19",
224
- "pkgmgr": "^1.0.0",
224
+ "pkgmgr": "^1.1.1",
225
225
  "react": "^19.2.3",
226
226
  "react-dom": "^19.2.3",
227
227
  "react-router": "^7.12.0",
@@ -241,7 +241,7 @@
241
241
  "@types/hast": "^3.0.4",
242
242
  "@types/mdast": "^4.0.4",
243
243
  "@types/node": "^25.0.9",
244
- "@types/react": "^19.2.8",
244
+ "@types/react": "^19.2.9",
245
245
  "@types/react-dom": "^19.2.3",
246
246
  "@types/shell-quote": "^1.7.5",
247
247
  "vitest": "^4.0.17",