@epic-web/workshop-utils 6.71.4 → 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.
- package/dist/apps.server.d.ts +3 -3
- package/dist/apps.server.js +5 -0
- package/dist/cache.server.d.ts +44 -13
- package/dist/compile-mdx.server.d.ts +1 -1
- package/dist/compile-mdx.server.js +8 -0
- package/dist/diff.server.d.ts +1 -1
- package/dist/diff.server.js +37 -8
- package/dist/git.server.d.ts +67 -77
- package/dist/git.server.js +134 -45
- package/dist/modified-time.server.js +2 -0
- package/dist/notifications.server.js +16 -0
- package/dist/package-install/package-install-check.server.d.ts +23 -0
- package/dist/package-install/package-install-check.server.js +229 -0
- package/dist/utils.server.js +2 -0
- package/package.json +4 -4
package/dist/apps.server.d.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
2134
|
+
readonly epicVideoEmbeds: string[];
|
|
2135
2135
|
} | {
|
|
2136
2136
|
readonly status: "error";
|
|
2137
2137
|
readonly error: string;
|
package/dist/apps.server.js
CHANGED
|
@@ -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) => {
|
package/dist/cache.server.d.ts
CHANGED
|
@@ -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<
|
|
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<
|
|
163
|
-
get: (key: string) => C.CacheEntry<
|
|
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<
|
|
170
|
-
get: (key: string) => C.CacheEntry<
|
|
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<
|
|
176
|
-
get: (key: string) => C.CacheEntry<
|
|
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<{
|
|
@@ -197,20 +209,38 @@ export declare const checkForUpdatesCache: {
|
|
|
197
209
|
name: string;
|
|
198
210
|
set: (key: string, value: C.CacheEntry<{
|
|
199
211
|
updatesAvailable: boolean;
|
|
200
|
-
|
|
201
|
-
|
|
212
|
+
repoUpdatesAvailable: boolean;
|
|
213
|
+
dependenciesNeedInstall: boolean;
|
|
214
|
+
updateNotificationId: string | null;
|
|
215
|
+
commitsAhead: number | null;
|
|
216
|
+
commitsBehind: number | null;
|
|
217
|
+
localCommit: string | null;
|
|
218
|
+
remoteCommit: string | null;
|
|
202
219
|
diffLink: string | null;
|
|
220
|
+
message: string | null;
|
|
203
221
|
}>) => C.CacheEntry<{
|
|
204
222
|
updatesAvailable: boolean;
|
|
205
|
-
|
|
206
|
-
|
|
223
|
+
repoUpdatesAvailable: boolean;
|
|
224
|
+
dependenciesNeedInstall: boolean;
|
|
225
|
+
updateNotificationId: string | null;
|
|
226
|
+
commitsAhead: number | null;
|
|
227
|
+
commitsBehind: number | null;
|
|
228
|
+
localCommit: string | null;
|
|
229
|
+
remoteCommit: string | null;
|
|
207
230
|
diffLink: string | null;
|
|
231
|
+
message: string | null;
|
|
208
232
|
}>;
|
|
209
233
|
get: (key: string) => C.CacheEntry<{
|
|
210
234
|
updatesAvailable: boolean;
|
|
211
|
-
|
|
212
|
-
|
|
235
|
+
repoUpdatesAvailable: boolean;
|
|
236
|
+
dependenciesNeedInstall: boolean;
|
|
237
|
+
updateNotificationId: string | null;
|
|
238
|
+
commitsAhead: number | null;
|
|
239
|
+
commitsBehind: number | null;
|
|
240
|
+
localCommit: string | null;
|
|
241
|
+
remoteCommit: string | null;
|
|
213
242
|
diffLink: string | null;
|
|
243
|
+
message: string | null;
|
|
214
244
|
}> | undefined;
|
|
215
245
|
delete: (key: string) => boolean;
|
|
216
246
|
};
|
|
@@ -412,3 +442,4 @@ export declare function shouldForceFresh({ forceFresh, request, key, }: {
|
|
|
412
442
|
request?: Request;
|
|
413
443
|
key?: string;
|
|
414
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:
|
|
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);
|
package/dist/diff.server.d.ts
CHANGED
|
@@ -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: "
|
|
8
|
+
status: "unknown" | "renamed" | "modified" | "deleted" | "added";
|
|
9
9
|
path: string;
|
|
10
10
|
line: number;
|
|
11
11
|
}[]>;
|
package/dist/diff.server.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/git.server.d.ts
CHANGED
|
@@ -1,38 +1,53 @@
|
|
|
1
1
|
import "./init-env.js";
|
|
2
2
|
export declare function checkForUpdates(): Promise<{
|
|
3
3
|
readonly updatesAvailable: false;
|
|
4
|
+
readonly dependenciesNeedInstall: false;
|
|
5
|
+
readonly updateNotificationId: null;
|
|
4
6
|
readonly message: "The app is deployed";
|
|
5
|
-
readonly
|
|
6
|
-
readonly
|
|
7
|
-
readonly
|
|
8
|
-
readonly
|
|
9
|
-
readonly
|
|
7
|
+
readonly repoUpdatesAvailable: boolean;
|
|
8
|
+
readonly commitsAhead: null;
|
|
9
|
+
readonly commitsBehind: null;
|
|
10
|
+
readonly localCommit: null;
|
|
11
|
+
readonly remoteCommit: null;
|
|
12
|
+
readonly diffLink: null;
|
|
10
13
|
} | {
|
|
11
|
-
readonly updatesAvailable: false;
|
|
12
14
|
readonly message: "You are offline";
|
|
13
|
-
readonly
|
|
14
|
-
readonly
|
|
15
|
-
readonly
|
|
16
|
-
readonly
|
|
17
|
-
readonly
|
|
15
|
+
readonly updatesAvailable: boolean;
|
|
16
|
+
readonly repoUpdatesAvailable: boolean;
|
|
17
|
+
readonly dependenciesNeedInstall: boolean;
|
|
18
|
+
readonly updateNotificationId: string | null;
|
|
19
|
+
readonly commitsAhead: null;
|
|
20
|
+
readonly commitsBehind: null;
|
|
21
|
+
readonly localCommit: null;
|
|
22
|
+
readonly remoteCommit: null;
|
|
23
|
+
readonly diffLink: null;
|
|
18
24
|
} | {
|
|
19
|
-
readonly updatesAvailable: false;
|
|
20
25
|
readonly message: "Not in a git repo";
|
|
21
|
-
readonly
|
|
22
|
-
readonly
|
|
23
|
-
readonly
|
|
24
|
-
readonly
|
|
25
|
-
readonly
|
|
26
|
+
readonly updatesAvailable: boolean;
|
|
27
|
+
readonly repoUpdatesAvailable: boolean;
|
|
28
|
+
readonly dependenciesNeedInstall: boolean;
|
|
29
|
+
readonly updateNotificationId: string | null;
|
|
30
|
+
readonly commitsAhead: null;
|
|
31
|
+
readonly commitsBehind: null;
|
|
32
|
+
readonly localCommit: null;
|
|
33
|
+
readonly remoteCommit: null;
|
|
34
|
+
readonly diffLink: null;
|
|
26
35
|
} | {
|
|
27
|
-
readonly updatesAvailable: false;
|
|
28
36
|
readonly message: "Cannot find remote";
|
|
29
|
-
readonly
|
|
30
|
-
readonly
|
|
31
|
-
readonly
|
|
32
|
-
readonly
|
|
33
|
-
readonly
|
|
37
|
+
readonly updatesAvailable: boolean;
|
|
38
|
+
readonly repoUpdatesAvailable: boolean;
|
|
39
|
+
readonly dependenciesNeedInstall: boolean;
|
|
40
|
+
readonly updateNotificationId: string | null;
|
|
41
|
+
readonly commitsAhead: null;
|
|
42
|
+
readonly commitsBehind: null;
|
|
43
|
+
readonly localCommit: null;
|
|
44
|
+
readonly remoteCommit: null;
|
|
45
|
+
readonly diffLink: null;
|
|
34
46
|
} | {
|
|
35
47
|
readonly updatesAvailable: boolean;
|
|
48
|
+
readonly repoUpdatesAvailable: boolean;
|
|
49
|
+
readonly dependenciesNeedInstall: boolean;
|
|
50
|
+
readonly updateNotificationId: string | null;
|
|
36
51
|
readonly commitsAhead: number;
|
|
37
52
|
readonly commitsBehind: number;
|
|
38
53
|
readonly localCommit: string;
|
|
@@ -40,64 +55,39 @@ export declare function checkForUpdates(): Promise<{
|
|
|
40
55
|
readonly diffLink: string | null;
|
|
41
56
|
readonly message: null;
|
|
42
57
|
} | {
|
|
43
|
-
readonly
|
|
44
|
-
readonly
|
|
45
|
-
readonly remoteCommit: string | undefined;
|
|
58
|
+
readonly localCommit: string | null;
|
|
59
|
+
readonly remoteCommit: string | null;
|
|
46
60
|
readonly diffLink: string | null;
|
|
47
|
-
readonly message?: undefined;
|
|
48
|
-
readonly commitsAhead?: undefined;
|
|
49
|
-
readonly commitsBehind?: undefined;
|
|
50
|
-
}>;
|
|
51
|
-
export declare function checkForUpdatesCached(): Promise<{
|
|
52
|
-
readonly updatesAvailable: false;
|
|
53
|
-
readonly message: "The app is deployed";
|
|
54
|
-
readonly commitsAhead?: undefined;
|
|
55
|
-
readonly commitsBehind?: undefined;
|
|
56
|
-
readonly localCommit?: undefined;
|
|
57
|
-
readonly remoteCommit?: undefined;
|
|
58
|
-
readonly diffLink?: undefined;
|
|
59
|
-
} | {
|
|
60
|
-
readonly updatesAvailable: false;
|
|
61
|
-
readonly message: "You are offline";
|
|
62
|
-
readonly commitsAhead?: undefined;
|
|
63
|
-
readonly commitsBehind?: undefined;
|
|
64
|
-
readonly localCommit?: undefined;
|
|
65
|
-
readonly remoteCommit?: undefined;
|
|
66
|
-
readonly diffLink?: undefined;
|
|
67
|
-
} | {
|
|
68
|
-
readonly updatesAvailable: false;
|
|
69
|
-
readonly message: "Not in a git repo";
|
|
70
|
-
readonly commitsAhead?: undefined;
|
|
71
|
-
readonly commitsBehind?: undefined;
|
|
72
|
-
readonly localCommit?: undefined;
|
|
73
|
-
readonly remoteCommit?: undefined;
|
|
74
|
-
readonly diffLink?: undefined;
|
|
75
|
-
} | {
|
|
76
|
-
readonly updatesAvailable: false;
|
|
77
|
-
readonly message: "Cannot find remote";
|
|
78
|
-
readonly commitsAhead?: undefined;
|
|
79
|
-
readonly commitsBehind?: undefined;
|
|
80
|
-
readonly localCommit?: undefined;
|
|
81
|
-
readonly remoteCommit?: undefined;
|
|
82
|
-
readonly diffLink?: undefined;
|
|
83
|
-
} | {
|
|
84
61
|
readonly updatesAvailable: boolean;
|
|
85
|
-
readonly
|
|
86
|
-
readonly
|
|
87
|
-
readonly
|
|
88
|
-
readonly
|
|
89
|
-
readonly
|
|
62
|
+
readonly repoUpdatesAvailable: boolean;
|
|
63
|
+
readonly dependenciesNeedInstall: boolean;
|
|
64
|
+
readonly updateNotificationId: string | null;
|
|
65
|
+
readonly commitsAhead: null;
|
|
66
|
+
readonly commitsBehind: null;
|
|
90
67
|
readonly message: null;
|
|
68
|
+
}>;
|
|
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;
|
|
91
80
|
} | {
|
|
92
81
|
readonly updatesAvailable: false;
|
|
93
|
-
readonly
|
|
94
|
-
readonly
|
|
95
|
-
readonly
|
|
96
|
-
readonly
|
|
97
|
-
readonly
|
|
98
|
-
readonly
|
|
99
|
-
|
|
100
|
-
readonly
|
|
82
|
+
readonly repoUpdatesAvailable: false;
|
|
83
|
+
readonly dependenciesNeedInstall: false;
|
|
84
|
+
readonly updateNotificationId: null;
|
|
85
|
+
readonly commitsAhead: null;
|
|
86
|
+
readonly commitsBehind: null;
|
|
87
|
+
readonly localCommit: null;
|
|
88
|
+
readonly remoteCommit: null;
|
|
89
|
+
readonly diffLink: null;
|
|
90
|
+
readonly message: "The app is deployed";
|
|
101
91
|
}>;
|
|
102
92
|
export declare function updateLocalRepo(): Promise<{
|
|
103
93
|
readonly status: "success";
|
|
@@ -107,7 +97,7 @@ export declare function updateLocalRepo(): Promise<{
|
|
|
107
97
|
readonly message: string;
|
|
108
98
|
} | {
|
|
109
99
|
readonly status: "success";
|
|
110
|
-
readonly message: "Updated successfully.";
|
|
100
|
+
readonly message: "Updated successfully." | "Dependencies updated successfully.";
|
|
111
101
|
}>;
|
|
112
102
|
export declare function getCommitInfo(): Promise<{
|
|
113
103
|
hash: string;
|
package/dist/git.server.js
CHANGED
|
@@ -2,20 +2,39 @@ 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";
|
|
8
9
|
import { getEnv } from "./env.server.js";
|
|
9
10
|
import { logger } from "./logger.js";
|
|
11
|
+
import { getInstallCommand, getWorkspaceInstallStatus, } from "./package-install/package-install-check.server.js";
|
|
10
12
|
import { checkConnection } from "./utils.server.js";
|
|
11
13
|
import { getErrorMessage } from "./utils.js";
|
|
12
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
|
+
});
|
|
13
27
|
function dirHasTrackedFiles(cwd, dirPath) {
|
|
14
28
|
return execa('git', ['ls-files', dirPath], { cwd }).then((s) => s.stdout.trim().length > 0, () => true);
|
|
15
29
|
}
|
|
16
30
|
function isDirectory(dirPath) {
|
|
17
31
|
return fs.stat(dirPath).then((s) => s.isDirectory(), () => false);
|
|
18
32
|
}
|
|
33
|
+
function getDependencyNotificationId(dependencyHash) {
|
|
34
|
+
if (!dependencyHash)
|
|
35
|
+
return null;
|
|
36
|
+
return `update-deps-${dependencyHash}`;
|
|
37
|
+
}
|
|
19
38
|
async function cleanupEmptyExerciseDirectories(cwd) {
|
|
20
39
|
console.log('🧹 Cleaning up empty exercise directories...');
|
|
21
40
|
try {
|
|
@@ -62,25 +81,48 @@ async function getDiffUrl(commitBefore, commitAfter) {
|
|
|
62
81
|
}
|
|
63
82
|
export async function checkForUpdates() {
|
|
64
83
|
const ENV = getEnv();
|
|
84
|
+
const cwd = getWorkshopRoot();
|
|
85
|
+
const dependencyStatus = await getWorkspaceInstallStatus(cwd);
|
|
86
|
+
const dependencyNotificationId = dependencyStatus.dependenciesNeedInstall
|
|
87
|
+
? getDependencyNotificationId(dependencyStatus.dependencyHash)
|
|
88
|
+
: null;
|
|
89
|
+
const baseResult = {
|
|
90
|
+
updatesAvailable: dependencyStatus.dependenciesNeedInstall,
|
|
91
|
+
repoUpdatesAvailable: false,
|
|
92
|
+
dependenciesNeedInstall: dependencyStatus.dependenciesNeedInstall,
|
|
93
|
+
updateNotificationId: dependencyNotificationId,
|
|
94
|
+
commitsAhead: null,
|
|
95
|
+
commitsBehind: null,
|
|
96
|
+
localCommit: null,
|
|
97
|
+
remoteCommit: null,
|
|
98
|
+
diffLink: null,
|
|
99
|
+
message: null,
|
|
100
|
+
};
|
|
65
101
|
if (ENV.EPICSHOP_DEPLOYED) {
|
|
66
|
-
return {
|
|
102
|
+
return {
|
|
103
|
+
...baseResult,
|
|
104
|
+
updatesAvailable: false,
|
|
105
|
+
dependenciesNeedInstall: false,
|
|
106
|
+
updateNotificationId: null,
|
|
107
|
+
message: 'The app is deployed',
|
|
108
|
+
};
|
|
67
109
|
}
|
|
68
|
-
const cwd = getWorkshopRoot();
|
|
69
110
|
const online = await checkConnection();
|
|
70
111
|
if (!online) {
|
|
71
|
-
return {
|
|
112
|
+
return { ...baseResult, message: 'You are offline' };
|
|
72
113
|
}
|
|
73
114
|
const isInRepo = await execaCommand('git rev-parse --is-inside-work-tree', {
|
|
74
115
|
cwd,
|
|
75
116
|
}).then(() => true, () => false);
|
|
76
117
|
if (!isInRepo) {
|
|
77
|
-
return {
|
|
118
|
+
return { ...baseResult, message: 'Not in a git repo' };
|
|
78
119
|
}
|
|
79
120
|
const { stdout: remote } = await execaCommand('git remote', { cwd });
|
|
80
121
|
if (!remote) {
|
|
81
|
-
return {
|
|
122
|
+
return { ...baseResult, message: 'Cannot find remote' };
|
|
82
123
|
}
|
|
83
|
-
let localCommit
|
|
124
|
+
let localCommit = null;
|
|
125
|
+
let remoteCommit = null;
|
|
84
126
|
try {
|
|
85
127
|
const currentBranch = (await execaCommand('git rev-parse --abbrev-ref HEAD', { cwd })).stdout.trim();
|
|
86
128
|
localCommit = (await execaCommand('git rev-parse --short HEAD', { cwd })).stdout.trim();
|
|
@@ -90,21 +132,28 @@ export async function checkForUpdates() {
|
|
|
90
132
|
})).stdout.trim();
|
|
91
133
|
const { stdout } = await execa('git', ['rev-list', '--count', '--left-right', 'HEAD...@{upstream}'], { cwd });
|
|
92
134
|
const [ahead = 0, behind = 0] = stdout.trim().split(/\s+/).map(Number);
|
|
93
|
-
const
|
|
135
|
+
const repoUpdatesAvailable = behind > 0;
|
|
136
|
+
const updatesAvailable = repoUpdatesAvailable || dependencyStatus.dependenciesNeedInstall;
|
|
137
|
+
const updateNotificationId = repoUpdatesAvailable
|
|
138
|
+
? `update-repo-${remoteCommit}`
|
|
139
|
+
: dependencyNotificationId;
|
|
94
140
|
return {
|
|
95
141
|
updatesAvailable,
|
|
142
|
+
repoUpdatesAvailable,
|
|
143
|
+
dependenciesNeedInstall: dependencyStatus.dependenciesNeedInstall,
|
|
144
|
+
updateNotificationId,
|
|
96
145
|
commitsAhead: ahead,
|
|
97
146
|
commitsBehind: behind,
|
|
98
147
|
localCommit,
|
|
99
148
|
remoteCommit,
|
|
100
149
|
diffLink: await getDiffUrl(localCommit, remoteCommit),
|
|
101
|
-
message:
|
|
150
|
+
message: baseResult.message,
|
|
102
151
|
};
|
|
103
152
|
}
|
|
104
153
|
catch (error) {
|
|
105
154
|
console.error('Unable to check for updates', getErrorMessage(error));
|
|
106
155
|
return {
|
|
107
|
-
|
|
156
|
+
...baseResult,
|
|
108
157
|
localCommit,
|
|
109
158
|
remoteCommit,
|
|
110
159
|
diffLink: localCommit && remoteCommit
|
|
@@ -116,21 +165,33 @@ export async function checkForUpdates() {
|
|
|
116
165
|
export async function checkForUpdatesCached() {
|
|
117
166
|
const ENV = getEnv();
|
|
118
167
|
if (ENV.EPICSHOP_DEPLOYED) {
|
|
119
|
-
return {
|
|
168
|
+
return {
|
|
169
|
+
updatesAvailable: false,
|
|
170
|
+
repoUpdatesAvailable: false,
|
|
171
|
+
dependenciesNeedInstall: false,
|
|
172
|
+
updateNotificationId: null,
|
|
173
|
+
commitsAhead: null,
|
|
174
|
+
commitsBehind: null,
|
|
175
|
+
localCommit: null,
|
|
176
|
+
remoteCommit: null,
|
|
177
|
+
diffLink: null,
|
|
178
|
+
message: 'The app is deployed',
|
|
179
|
+
};
|
|
120
180
|
}
|
|
121
181
|
const key = 'checkForUpdates';
|
|
122
182
|
return cachified({
|
|
123
183
|
ttl: 1000 * 60,
|
|
124
184
|
swr: 1000 * 60 * 60 * 24,
|
|
125
185
|
key,
|
|
186
|
+
checkValue: CheckForUpdatesSchema,
|
|
126
187
|
getFreshValue: checkForUpdates,
|
|
127
188
|
cache: checkForUpdatesCache,
|
|
128
189
|
});
|
|
129
190
|
}
|
|
130
|
-
async function
|
|
191
|
+
async function runInstallWithRetry(cwd, command, args, maxRetries = 3, baseDelayMs = 1000) {
|
|
131
192
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
132
193
|
try {
|
|
133
|
-
await
|
|
194
|
+
await execa(command, args, { cwd, stdio: 'inherit' });
|
|
134
195
|
return;
|
|
135
196
|
}
|
|
136
197
|
catch (error) {
|
|
@@ -160,49 +221,77 @@ export async function updateLocalRepo() {
|
|
|
160
221
|
const cwd = getWorkshopRoot();
|
|
161
222
|
try {
|
|
162
223
|
const updates = await checkForUpdates();
|
|
163
|
-
|
|
224
|
+
const repoUpdatesAvailable = updates.repoUpdatesAvailable;
|
|
225
|
+
let dependencyStatus = await getWorkspaceInstallStatus(cwd);
|
|
226
|
+
let rootsNeedingInstall = dependencyStatus.roots.filter((status) => status.dependenciesNeedInstall);
|
|
227
|
+
if (!repoUpdatesAvailable && rootsNeedingInstall.length === 0) {
|
|
164
228
|
return {
|
|
165
229
|
status: 'success',
|
|
166
230
|
message: updates.message ?? 'No updates available.',
|
|
167
231
|
};
|
|
168
232
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
233
|
+
let didPull = false;
|
|
234
|
+
let didInstall = false;
|
|
235
|
+
if (repoUpdatesAvailable) {
|
|
236
|
+
const uncommittedChanges = (await execaCommand('git status --porcelain', { cwd })).stdout.trim()
|
|
237
|
+
.length > 0;
|
|
238
|
+
if (uncommittedChanges) {
|
|
239
|
+
console.log('👜 Stashing uncommitted changes...');
|
|
240
|
+
await execaCommand('git stash --include-untracked', { cwd });
|
|
241
|
+
}
|
|
242
|
+
console.log('⬇️ Pulling latest changes...');
|
|
243
|
+
await execaCommand('git pull origin HEAD', { cwd });
|
|
244
|
+
if (uncommittedChanges) {
|
|
245
|
+
console.log('👜 re-applying stashed changes...');
|
|
246
|
+
await execaCommand('git stash pop', { cwd });
|
|
247
|
+
}
|
|
248
|
+
didPull = true;
|
|
249
|
+
dependencyStatus = await getWorkspaceInstallStatus(cwd);
|
|
250
|
+
rootsNeedingInstall = dependencyStatus.roots.filter((status) => status.dependenciesNeedInstall);
|
|
174
251
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
252
|
+
if (rootsNeedingInstall.length > 0) {
|
|
253
|
+
for (const root of rootsNeedingInstall) {
|
|
254
|
+
const rootLabel = path.relative(cwd, root.rootDir).replace(/\\/g, '/') || '.';
|
|
255
|
+
const { command, args } = getInstallCommand(root.packageManager);
|
|
256
|
+
const commandLabel = `${command} ${args.join(' ')}`.trim();
|
|
257
|
+
console.log(`📦 Installing dependencies in ${rootLabel} using ${commandLabel}...`);
|
|
258
|
+
try {
|
|
259
|
+
await runInstallWithRetry(root.rootDir, command, args);
|
|
260
|
+
didInstall = true;
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
const isEbusy = error instanceof Error &&
|
|
264
|
+
(error.message.includes('EBUSY') ||
|
|
265
|
+
error.code === 'EBUSY');
|
|
266
|
+
if (isEbusy) {
|
|
267
|
+
return {
|
|
268
|
+
status: 'error',
|
|
269
|
+
message: `${commandLabel} failed: files are locked. ` +
|
|
270
|
+
'Please close any editors or terminals using this directory, ' +
|
|
271
|
+
`then run: ${commandLabel}`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
180
277
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
await runNpmInstallWithRetry(cwd);
|
|
278
|
+
else if (repoUpdatesAvailable) {
|
|
279
|
+
console.log('📦 Dependencies already match package.json. Skipping install.');
|
|
184
280
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
status: 'error',
|
|
192
|
-
message: 'npm install failed: files are locked. ' +
|
|
193
|
-
'Please close any editors or terminals using this directory, ' +
|
|
194
|
-
'then run: npm install',
|
|
195
|
-
};
|
|
281
|
+
if (didPull || didInstall) {
|
|
282
|
+
await cleanupEmptyExerciseDirectories(cwd);
|
|
283
|
+
const postUpdateScript = getWorkshopConfig().scripts?.postupdate;
|
|
284
|
+
if (postUpdateScript) {
|
|
285
|
+
console.log('🏃 Running post update script...');
|
|
286
|
+
await execaCommand(postUpdateScript, { cwd, stdio: 'inherit' });
|
|
196
287
|
}
|
|
197
|
-
throw error;
|
|
198
288
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
return { status: 'success', message: 'Updated successfully.' };
|
|
289
|
+
return {
|
|
290
|
+
status: 'success',
|
|
291
|
+
message: repoUpdatesAvailable
|
|
292
|
+
? 'Updated successfully.'
|
|
293
|
+
: 'Dependencies updated successfully.',
|
|
294
|
+
};
|
|
206
295
|
}
|
|
207
296
|
catch (error) {
|
|
208
297
|
return { status: 'error', message: getErrorMessage(error) };
|
|
@@ -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';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type RootPackageInstallStatus = {
|
|
2
|
+
rootDir: string;
|
|
3
|
+
packageJsonPath: string;
|
|
4
|
+
packageManager: string | null;
|
|
5
|
+
dependencyHash: string | null;
|
|
6
|
+
dependenciesNeedInstall: boolean;
|
|
7
|
+
missingDependencies: Array<string>;
|
|
8
|
+
missingDevDependencies: Array<string>;
|
|
9
|
+
missingOptionalDependencies: Array<string>;
|
|
10
|
+
reason: 'missing-node-modules' | 'missing-dependencies' | 'package-json-unreadable' | 'up-to-date';
|
|
11
|
+
};
|
|
12
|
+
export type WorkspaceInstallStatus = {
|
|
13
|
+
roots: Array<RootPackageInstallStatus>;
|
|
14
|
+
dependenciesNeedInstall: boolean;
|
|
15
|
+
dependencyHash: string | null;
|
|
16
|
+
};
|
|
17
|
+
export declare function getRootPackageJsonPaths(cwd: string): Promise<string[]>;
|
|
18
|
+
export declare function getRootPackageInstallStatus(packageJsonPath: string): Promise<RootPackageInstallStatus>;
|
|
19
|
+
export declare function getWorkspaceInstallStatus(cwd: string): Promise<WorkspaceInstallStatus>;
|
|
20
|
+
export declare function getInstallCommand(packageManager: string | null): {
|
|
21
|
+
command: string;
|
|
22
|
+
args: string[];
|
|
23
|
+
};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { globby } from 'globby';
|
|
5
|
+
import { getErrorMessage } from "../utils.js";
|
|
6
|
+
const workspaceIgnorePatterns = [
|
|
7
|
+
'**/node_modules/**',
|
|
8
|
+
'**/.git/**',
|
|
9
|
+
'**/.cache/**',
|
|
10
|
+
'**/dist/**',
|
|
11
|
+
'**/build/**',
|
|
12
|
+
'**/coverage/**',
|
|
13
|
+
];
|
|
14
|
+
function hashString(value) {
|
|
15
|
+
return createHash('sha256').update(value).digest('hex').slice(0, 8);
|
|
16
|
+
}
|
|
17
|
+
function normalizeDependencyMap(dependencies) {
|
|
18
|
+
const entries = Object.entries(dependencies ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
19
|
+
return Object.fromEntries(entries);
|
|
20
|
+
}
|
|
21
|
+
function getDependencySnapshot(packageJson) {
|
|
22
|
+
return {
|
|
23
|
+
dependencies: normalizeDependencyMap(packageJson.dependencies),
|
|
24
|
+
devDependencies: normalizeDependencyMap(packageJson.devDependencies),
|
|
25
|
+
optionalDependencies: normalizeDependencyMap(packageJson.optionalDependencies),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function parsePackageManager(value) {
|
|
29
|
+
if (!value)
|
|
30
|
+
return null;
|
|
31
|
+
const [name] = value.split('@');
|
|
32
|
+
return name || null;
|
|
33
|
+
}
|
|
34
|
+
function normalizeWorkspacePattern(pattern) {
|
|
35
|
+
const trimmed = pattern.trim();
|
|
36
|
+
if (!trimmed)
|
|
37
|
+
return trimmed;
|
|
38
|
+
const isNegated = trimmed.startsWith('!');
|
|
39
|
+
const raw = isNegated ? trimmed.slice(1) : trimmed;
|
|
40
|
+
const normalized = raw.replace(/\\/g, '/');
|
|
41
|
+
const withPackageJson = normalized.endsWith('package.json')
|
|
42
|
+
? normalized
|
|
43
|
+
: path.posix.join(normalized, 'package.json');
|
|
44
|
+
return isNegated ? `!${withPackageJson}` : withPackageJson;
|
|
45
|
+
}
|
|
46
|
+
async function readPackageJson(filePath) {
|
|
47
|
+
try {
|
|
48
|
+
const contents = await fs.readFile(filePath, 'utf8');
|
|
49
|
+
return JSON.parse(contents);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
console.warn(`⚠️ Failed to read package.json at ${filePath}:`, getErrorMessage(error));
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function getWorkspacePackageJsonPaths(packageJsonPath) {
|
|
57
|
+
const packageJson = await readPackageJson(packageJsonPath);
|
|
58
|
+
if (!packageJson)
|
|
59
|
+
return [];
|
|
60
|
+
const workspaces = Array.isArray(packageJson.workspaces)
|
|
61
|
+
? packageJson.workspaces
|
|
62
|
+
: (packageJson.workspaces?.packages ?? []);
|
|
63
|
+
if (!workspaces.length)
|
|
64
|
+
return [];
|
|
65
|
+
const workspacePatterns = workspaces.map(normalizeWorkspacePattern);
|
|
66
|
+
return globby(workspacePatterns, {
|
|
67
|
+
cwd: path.dirname(packageJsonPath),
|
|
68
|
+
absolute: true,
|
|
69
|
+
ignore: workspaceIgnorePatterns,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async function listPackageJsonPaths(cwd) {
|
|
73
|
+
return globby('**/package.json', {
|
|
74
|
+
cwd,
|
|
75
|
+
absolute: true,
|
|
76
|
+
ignore: workspaceIgnorePatterns,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async function listInstalledPackages(nodeModulesPath) {
|
|
80
|
+
try {
|
|
81
|
+
const entries = await fs.readdir(nodeModulesPath, { withFileTypes: true });
|
|
82
|
+
const packages = new Set();
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
if (entry.name.startsWith('.'))
|
|
85
|
+
continue;
|
|
86
|
+
if (entry.name.startsWith('@')) {
|
|
87
|
+
if (!entry.isDirectory())
|
|
88
|
+
continue;
|
|
89
|
+
const scopePath = path.join(nodeModulesPath, entry.name);
|
|
90
|
+
const scopeEntries = await fs.readdir(scopePath, {
|
|
91
|
+
withFileTypes: true,
|
|
92
|
+
});
|
|
93
|
+
for (const scopedEntry of scopeEntries) {
|
|
94
|
+
if (scopedEntry.name.startsWith('.'))
|
|
95
|
+
continue;
|
|
96
|
+
if (scopedEntry.isDirectory() || scopedEntry.isSymbolicLink()) {
|
|
97
|
+
packages.add(`${entry.name}/${scopedEntry.name}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
103
|
+
packages.add(entry.name);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return packages;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function getExpectedDependencies(dependencies) {
|
|
113
|
+
return Object.keys(dependencies ?? {}).sort();
|
|
114
|
+
}
|
|
115
|
+
export async function getRootPackageJsonPaths(cwd) {
|
|
116
|
+
const allPackageJsonPaths = await listPackageJsonPaths(cwd);
|
|
117
|
+
const workspacePackageJsonPaths = new Set();
|
|
118
|
+
for (const packageJsonPath of allPackageJsonPaths) {
|
|
119
|
+
const workspacePaths = await getWorkspacePackageJsonPaths(packageJsonPath);
|
|
120
|
+
for (const workspacePath of workspacePaths) {
|
|
121
|
+
workspacePackageJsonPaths.add(path.resolve(workspacePath));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return allPackageJsonPaths
|
|
125
|
+
.map((packageJsonPath) => path.resolve(packageJsonPath))
|
|
126
|
+
.filter((packageJsonPath) => !workspacePackageJsonPaths.has(packageJsonPath))
|
|
127
|
+
.sort();
|
|
128
|
+
}
|
|
129
|
+
export async function getRootPackageInstallStatus(packageJsonPath) {
|
|
130
|
+
const rootDir = path.dirname(packageJsonPath);
|
|
131
|
+
const packageJson = await readPackageJson(packageJsonPath);
|
|
132
|
+
if (!packageJson) {
|
|
133
|
+
return {
|
|
134
|
+
rootDir,
|
|
135
|
+
packageJsonPath,
|
|
136
|
+
packageManager: null,
|
|
137
|
+
dependencyHash: null,
|
|
138
|
+
dependenciesNeedInstall: false,
|
|
139
|
+
missingDependencies: [],
|
|
140
|
+
missingDevDependencies: [],
|
|
141
|
+
missingOptionalDependencies: [],
|
|
142
|
+
reason: 'package-json-unreadable',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const dependencySnapshot = getDependencySnapshot(packageJson);
|
|
146
|
+
const dependencyHash = hashString(JSON.stringify(dependencySnapshot));
|
|
147
|
+
const packageManager = parsePackageManager(packageJson.packageManager);
|
|
148
|
+
const dependencies = getExpectedDependencies(packageJson.dependencies);
|
|
149
|
+
const devDependencies = getExpectedDependencies(packageJson.devDependencies);
|
|
150
|
+
const optionalDependencies = getExpectedDependencies(packageJson.optionalDependencies);
|
|
151
|
+
const expectedDependencies = [
|
|
152
|
+
...dependencies,
|
|
153
|
+
...devDependencies,
|
|
154
|
+
...optionalDependencies,
|
|
155
|
+
];
|
|
156
|
+
if (expectedDependencies.length === 0) {
|
|
157
|
+
return {
|
|
158
|
+
rootDir,
|
|
159
|
+
packageJsonPath,
|
|
160
|
+
packageManager,
|
|
161
|
+
dependencyHash,
|
|
162
|
+
dependenciesNeedInstall: false,
|
|
163
|
+
missingDependencies: [],
|
|
164
|
+
missingDevDependencies: [],
|
|
165
|
+
missingOptionalDependencies: [],
|
|
166
|
+
reason: 'up-to-date',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const installedPackages = await listInstalledPackages(path.join(rootDir, 'node_modules'));
|
|
170
|
+
if (!installedPackages) {
|
|
171
|
+
return {
|
|
172
|
+
rootDir,
|
|
173
|
+
packageJsonPath,
|
|
174
|
+
packageManager,
|
|
175
|
+
dependencyHash,
|
|
176
|
+
dependenciesNeedInstall: true,
|
|
177
|
+
missingDependencies: dependencies,
|
|
178
|
+
missingDevDependencies: devDependencies,
|
|
179
|
+
missingOptionalDependencies: optionalDependencies,
|
|
180
|
+
reason: 'missing-node-modules',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const missingDependencies = dependencies.filter((dep) => !installedPackages.has(dep));
|
|
184
|
+
const missingDevDependencies = devDependencies.filter((dep) => !installedPackages.has(dep));
|
|
185
|
+
const missingOptionalDependencies = optionalDependencies.filter((dep) => !installedPackages.has(dep));
|
|
186
|
+
const dependenciesNeedInstall = missingDependencies.length > 0 || missingDevDependencies.length > 0;
|
|
187
|
+
return {
|
|
188
|
+
rootDir,
|
|
189
|
+
packageJsonPath,
|
|
190
|
+
packageManager,
|
|
191
|
+
dependencyHash,
|
|
192
|
+
dependenciesNeedInstall,
|
|
193
|
+
missingDependencies,
|
|
194
|
+
missingDevDependencies,
|
|
195
|
+
missingOptionalDependencies,
|
|
196
|
+
reason: dependenciesNeedInstall ? 'missing-dependencies' : 'up-to-date',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
export async function getWorkspaceInstallStatus(cwd) {
|
|
200
|
+
const rootPackageJsonPaths = await getRootPackageJsonPaths(cwd);
|
|
201
|
+
const rootStatuses = await Promise.all(rootPackageJsonPaths.map(getRootPackageInstallStatus));
|
|
202
|
+
const dependenciesNeedInstall = rootStatuses.some((status) => status.dependenciesNeedInstall);
|
|
203
|
+
const dependencyHash = rootStatuses.length > 0
|
|
204
|
+
? hashString(JSON.stringify(rootStatuses
|
|
205
|
+
.map((status) => ({
|
|
206
|
+
path: path.relative(cwd, status.packageJsonPath),
|
|
207
|
+
hash: status.dependencyHash,
|
|
208
|
+
}))
|
|
209
|
+
.sort((a, b) => a.path.localeCompare(b.path))))
|
|
210
|
+
: null;
|
|
211
|
+
return {
|
|
212
|
+
roots: rootStatuses,
|
|
213
|
+
dependenciesNeedInstall,
|
|
214
|
+
dependencyHash,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
export function getInstallCommand(packageManager) {
|
|
218
|
+
switch (packageManager) {
|
|
219
|
+
case 'pnpm':
|
|
220
|
+
return { command: 'pnpm', args: ['install'] };
|
|
221
|
+
case 'yarn':
|
|
222
|
+
return { command: 'yarn', args: ['install'] };
|
|
223
|
+
case 'bun':
|
|
224
|
+
return { command: 'bun', args: ['install'] };
|
|
225
|
+
case 'npm':
|
|
226
|
+
default:
|
|
227
|
+
return { command: 'npm', args: ['install'] };
|
|
228
|
+
}
|
|
229
|
+
}
|
package/dist/utils.server.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|