@epic-web/workshop-utils 4.0.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.
- package/README.md +3 -0
- package/dist/esm/apps.server.d.ts +3861 -0
- package/dist/esm/apps.server.d.ts.map +1 -0
- package/dist/esm/apps.server.js +1011 -0
- package/dist/esm/apps.server.js.map +1 -0
- package/dist/esm/cache.server.d.ts +798 -0
- package/dist/esm/cache.server.d.ts.map +1 -0
- package/dist/esm/cache.server.js +113 -0
- package/dist/esm/cache.server.js.map +1 -0
- package/dist/esm/change-tracker.server.d.ts +8 -0
- package/dist/esm/change-tracker.server.d.ts.map +1 -0
- package/dist/esm/change-tracker.server.js +32 -0
- package/dist/esm/change-tracker.server.js.map +1 -0
- package/dist/esm/codefile-mdx.server.d.ts +16 -0
- package/dist/esm/codefile-mdx.server.d.ts.map +1 -0
- package/dist/esm/codefile-mdx.server.js +275 -0
- package/dist/esm/codefile-mdx.server.js.map +1 -0
- package/dist/esm/compile-mdx.server.d.ts +11 -0
- package/dist/esm/compile-mdx.server.d.ts.map +1 -0
- package/dist/esm/compile-mdx.server.js +330 -0
- package/dist/esm/compile-mdx.server.js.map +1 -0
- package/dist/esm/db.server.d.ts +176 -0
- package/dist/esm/db.server.d.ts.map +1 -0
- package/dist/esm/db.server.js +203 -0
- package/dist/esm/db.server.js.map +1 -0
- package/dist/esm/git.server.d.ts +27 -0
- package/dist/esm/git.server.d.ts.map +1 -0
- package/dist/esm/git.server.js +93 -0
- package/dist/esm/git.server.js.map +1 -0
- package/dist/esm/iframe-sync.d.ts +10 -0
- package/dist/esm/iframe-sync.d.ts.map +1 -0
- package/dist/esm/iframe-sync.js +101 -0
- package/dist/esm/iframe-sync.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/playwright.server.d.ts +6 -0
- package/dist/esm/playwright.server.d.ts.map +1 -0
- package/dist/esm/playwright.server.js +94 -0
- package/dist/esm/playwright.server.js.map +1 -0
- package/dist/esm/process-manager.server.d.ts +78 -0
- package/dist/esm/process-manager.server.d.ts.map +1 -0
- package/dist/esm/process-manager.server.js +267 -0
- package/dist/esm/process-manager.server.js.map +1 -0
- package/dist/esm/test.d.ts +9 -0
- package/dist/esm/test.d.ts.map +1 -0
- package/dist/esm/test.js +45 -0
- package/dist/esm/test.js.map +1 -0
- package/dist/esm/timing.server.d.ts +20 -0
- package/dist/esm/timing.server.d.ts.map +1 -0
- package/dist/esm/timing.server.js +89 -0
- package/dist/esm/timing.server.js.map +1 -0
- package/dist/esm/utils.d.ts +2 -0
- package/dist/esm/utils.d.ts.map +1 -0
- package/dist/esm/utils.js +13 -0
- package/dist/esm/utils.js.map +1 -0
- package/dist/esm/utils.server.d.ts +3 -0
- package/dist/esm/utils.server.d.ts.map +1 -0
- package/dist/esm/utils.server.js +32 -0
- package/dist/esm/utils.server.js.map +1 -0
- package/package.json +171 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { redirect } from '@remix-run/node';
|
|
4
|
+
import fsExtra from 'fs-extra';
|
|
5
|
+
import md5 from 'md5-hex';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
export const DiscordMemberSchema = z.object({
|
|
8
|
+
avatarURL: z.string().nullable().optional(),
|
|
9
|
+
displayName: z.string(),
|
|
10
|
+
id: z.string(),
|
|
11
|
+
});
|
|
12
|
+
const TokenSetSchema = z.object({
|
|
13
|
+
access_token: z.string(),
|
|
14
|
+
token_type: z.string(),
|
|
15
|
+
scope: z.string(),
|
|
16
|
+
});
|
|
17
|
+
export const PlayerPreferencesSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
volumeRate: z.number().optional(),
|
|
20
|
+
playbackRate: z.number().optional(),
|
|
21
|
+
autoplay: z.boolean().optional(),
|
|
22
|
+
subtitle: z
|
|
23
|
+
.object({
|
|
24
|
+
id: z.string().nullable().default(null),
|
|
25
|
+
mode: z
|
|
26
|
+
.literal('disabled')
|
|
27
|
+
.or(z.literal('hidden'))
|
|
28
|
+
.or(z.literal('showing'))
|
|
29
|
+
.nullable()
|
|
30
|
+
.default('disabled'),
|
|
31
|
+
})
|
|
32
|
+
.optional()
|
|
33
|
+
.default({}),
|
|
34
|
+
muted: z.boolean().optional(),
|
|
35
|
+
theater: z.boolean().optional(),
|
|
36
|
+
defaultView: z.string().optional(),
|
|
37
|
+
activeSidebarTab: z.number().optional(),
|
|
38
|
+
})
|
|
39
|
+
.optional()
|
|
40
|
+
.default({});
|
|
41
|
+
const PresencePreferencesSchema = z
|
|
42
|
+
.object({
|
|
43
|
+
optOut: z.boolean(),
|
|
44
|
+
})
|
|
45
|
+
.optional()
|
|
46
|
+
.default({ optOut: false });
|
|
47
|
+
const AuthInfoSchema = z
|
|
48
|
+
.object({
|
|
49
|
+
tokenSet: TokenSetSchema,
|
|
50
|
+
email: z.string(),
|
|
51
|
+
name: z.string().nullable().optional(),
|
|
52
|
+
})
|
|
53
|
+
.transform(d => ({ ...d, id: md5(d.email) }));
|
|
54
|
+
const DataSchema = z.object({
|
|
55
|
+
onboarding: z
|
|
56
|
+
.object({ finishedTourVideo: z.boolean() })
|
|
57
|
+
.optional()
|
|
58
|
+
.default({ finishedTourVideo: false }),
|
|
59
|
+
preferences: z
|
|
60
|
+
.object({
|
|
61
|
+
player: PlayerPreferencesSchema,
|
|
62
|
+
presence: PresencePreferencesSchema,
|
|
63
|
+
})
|
|
64
|
+
.optional()
|
|
65
|
+
.default({}),
|
|
66
|
+
authInfo: AuthInfoSchema.optional(),
|
|
67
|
+
discordMember: DiscordMemberSchema.optional(),
|
|
68
|
+
});
|
|
69
|
+
const appDir = path.join(os.homedir(), '.epicshop');
|
|
70
|
+
const dbPath = path.join(appDir, 'data.json');
|
|
71
|
+
export async function deleteDb() {
|
|
72
|
+
if (process.env.EPICSHOP_DEPLOYED)
|
|
73
|
+
return null;
|
|
74
|
+
try {
|
|
75
|
+
if (await fsExtra.exists(dbPath)) {
|
|
76
|
+
await fsExtra.remove(dbPath);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.error(`Error deleting the database in ${dbPath}`, error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function readDb() {
|
|
84
|
+
if (process.env.EPICSHOP_DEPLOYED)
|
|
85
|
+
return null;
|
|
86
|
+
try {
|
|
87
|
+
if (await fsExtra.exists(dbPath)) {
|
|
88
|
+
const db = DataSchema.parse(await fsExtra.readJSON(dbPath));
|
|
89
|
+
return db;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error(`Error reading the database in ${dbPath}, moving it to a .bkp file to avoid parsing errors in the future`, error);
|
|
94
|
+
void fsExtra.move(dbPath, `${dbPath}.bkp`).catch(() => { });
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
export async function getAuthInfo() {
|
|
99
|
+
const data = await readDb();
|
|
100
|
+
return data?.authInfo ?? null;
|
|
101
|
+
}
|
|
102
|
+
export async function getUserInfo() {
|
|
103
|
+
const db = await readDb();
|
|
104
|
+
if (!db?.authInfo)
|
|
105
|
+
return null;
|
|
106
|
+
return {
|
|
107
|
+
id: db.authInfo.id,
|
|
108
|
+
name: db.discordMember?.displayName ?? db.authInfo.name ?? null,
|
|
109
|
+
email: db.authInfo.email,
|
|
110
|
+
avatarUrl: db.discordMember?.avatarURL ??
|
|
111
|
+
getGravatar({ email: db.authInfo.email, size: 288 }),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function getGravatar({ email, size }) {
|
|
115
|
+
const gravatarOptions = new URLSearchParams({
|
|
116
|
+
size: size.toString(),
|
|
117
|
+
default: 'identicon',
|
|
118
|
+
});
|
|
119
|
+
const gravatarUrl = `https://www.gravatar.com/avatar/${md5(email)}?${gravatarOptions.toString()}`;
|
|
120
|
+
return gravatarUrl;
|
|
121
|
+
}
|
|
122
|
+
export async function requireAuthInfo({ request, redirectTo, }) {
|
|
123
|
+
const authInfo = await getAuthInfo();
|
|
124
|
+
if (!authInfo) {
|
|
125
|
+
const requestUrl = new URL(request.url);
|
|
126
|
+
redirectTo =
|
|
127
|
+
redirectTo === null
|
|
128
|
+
? null
|
|
129
|
+
: redirectTo ?? `${requestUrl.pathname}${requestUrl.search}`;
|
|
130
|
+
const loginParams = redirectTo ? new URLSearchParams({ redirectTo }) : null;
|
|
131
|
+
const loginRedirect = ['/login', loginParams?.toString()]
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.join('?');
|
|
134
|
+
throw redirect(loginRedirect);
|
|
135
|
+
}
|
|
136
|
+
return authInfo;
|
|
137
|
+
}
|
|
138
|
+
export async function setAuthInfo({ tokenSet, email = 'unknown@example.com', name, }) {
|
|
139
|
+
const data = await readDb();
|
|
140
|
+
const authInfo = AuthInfoSchema.parse({ tokenSet, email, name });
|
|
141
|
+
await fsExtra.ensureDir(appDir);
|
|
142
|
+
await fsExtra.writeJSON(dbPath, { ...data, authInfo });
|
|
143
|
+
return authInfo;
|
|
144
|
+
}
|
|
145
|
+
export async function getPreferences() {
|
|
146
|
+
const data = await readDb();
|
|
147
|
+
return data?.preferences ?? null;
|
|
148
|
+
}
|
|
149
|
+
export async function setPlayerPreferences(playerPreferences) {
|
|
150
|
+
const data = await readDb();
|
|
151
|
+
const updatedData = {
|
|
152
|
+
...data,
|
|
153
|
+
preferences: { ...data?.preferences, player: playerPreferences },
|
|
154
|
+
};
|
|
155
|
+
await fsExtra.ensureDir(appDir);
|
|
156
|
+
await fsExtra.writeJSON(dbPath, updatedData);
|
|
157
|
+
return updatedData.preferences.player;
|
|
158
|
+
}
|
|
159
|
+
export async function setPresencePreferences(presnecePreferences) {
|
|
160
|
+
const data = await readDb();
|
|
161
|
+
const updatedData = {
|
|
162
|
+
...data,
|
|
163
|
+
preferences: { ...data?.preferences, presence: presnecePreferences },
|
|
164
|
+
};
|
|
165
|
+
await fsExtra.ensureDir(appDir);
|
|
166
|
+
await fsExtra.writeJSON(dbPath, updatedData);
|
|
167
|
+
return updatedData.preferences.presence;
|
|
168
|
+
}
|
|
169
|
+
export async function getDiscordMember() {
|
|
170
|
+
const data = await readDb();
|
|
171
|
+
return data?.discordMember ?? null;
|
|
172
|
+
}
|
|
173
|
+
export async function setDiscordMember(discordMember) {
|
|
174
|
+
const data = await readDb();
|
|
175
|
+
const updatedData = {
|
|
176
|
+
...data,
|
|
177
|
+
discordMember,
|
|
178
|
+
};
|
|
179
|
+
await fsExtra.ensureDir(appDir);
|
|
180
|
+
await fsExtra.writeJSON(dbPath, updatedData);
|
|
181
|
+
return updatedData.discordMember;
|
|
182
|
+
}
|
|
183
|
+
export async function deleteDiscordInfo() {
|
|
184
|
+
const data = await readDb();
|
|
185
|
+
delete data?.discordMember;
|
|
186
|
+
await fsExtra.ensureDir(appDir);
|
|
187
|
+
await fsExtra.writeJSON(dbPath, data);
|
|
188
|
+
}
|
|
189
|
+
export async function readOnboardingData() {
|
|
190
|
+
const data = await readDb();
|
|
191
|
+
return data?.onboarding ?? null;
|
|
192
|
+
}
|
|
193
|
+
export async function updateOnboardingData(onboardingData) {
|
|
194
|
+
const data = await readDb();
|
|
195
|
+
const updatedData = {
|
|
196
|
+
...data,
|
|
197
|
+
onboarding: { ...data?.onboarding, ...onboardingData },
|
|
198
|
+
};
|
|
199
|
+
await fsExtra.ensureDir(appDir);
|
|
200
|
+
await fsExtra.writeJSON(dbPath, updatedData);
|
|
201
|
+
return updatedData.onboarding;
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=db.server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.server.js","sourceRoot":"","sources":["../../src/db.server.ts"],"names":[],"mappings":"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","sourcesContent":["import os from 'os'\nimport path from 'path'\nimport { redirect } from '@remix-run/node'\nimport fsExtra from 'fs-extra'\nimport md5 from 'md5-hex'\nimport { z } from 'zod'\n\nexport const DiscordMemberSchema = z.object({\n\tavatarURL: z.string().nullable().optional(),\n\tdisplayName: z.string(),\n\tid: z.string(),\n})\n\nconst TokenSetSchema = z.object({\n\taccess_token: z.string(),\n\ttoken_type: z.string(),\n\tscope: z.string(),\n})\nexport const PlayerPreferencesSchema = z\n\t.object({\n\t\tvolumeRate: z.number().optional(),\n\t\tplaybackRate: z.number().optional(),\n\t\tautoplay: z.boolean().optional(),\n\t\tsubtitle: z\n\t\t\t.object({\n\t\t\t\tid: z.string().nullable().default(null),\n\t\t\t\tmode: z\n\t\t\t\t\t.literal('disabled')\n\t\t\t\t\t.or(z.literal('hidden'))\n\t\t\t\t\t.or(z.literal('showing'))\n\t\t\t\t\t.nullable()\n\t\t\t\t\t.default('disabled'),\n\t\t\t})\n\t\t\t.optional()\n\t\t\t.default({}),\n\t\tmuted: z.boolean().optional(),\n\t\ttheater: z.boolean().optional(),\n\t\tdefaultView: z.string().optional(),\n\t\tactiveSidebarTab: z.number().optional(),\n\t})\n\t.optional()\n\t.default({})\n\nconst PresencePreferencesSchema = z\n\t.object({\n\t\toptOut: z.boolean(),\n\t})\n\t.optional()\n\t.default({ optOut: false })\n\nconst AuthInfoSchema = z\n\t.object({\n\t\ttokenSet: TokenSetSchema,\n\t\temail: z.string(),\n\t\tname: z.string().nullable().optional(),\n\t})\n\t.transform(d => ({ ...d, id: md5(d.email) }))\n\nconst DataSchema = z.object({\n\tonboarding: z\n\t\t.object({ finishedTourVideo: z.boolean() })\n\t\t.optional()\n\t\t.default({ finishedTourVideo: false }),\n\tpreferences: z\n\t\t.object({\n\t\t\tplayer: PlayerPreferencesSchema,\n\t\t\tpresence: PresencePreferencesSchema,\n\t\t})\n\t\t.optional()\n\t\t.default({}),\n\tauthInfo: AuthInfoSchema.optional(),\n\tdiscordMember: DiscordMemberSchema.optional(),\n})\n\nconst appDir = path.join(os.homedir(), '.epicshop')\nconst dbPath = path.join(appDir, 'data.json')\n\nexport async function deleteDb() {\n\tif (process.env.EPICSHOP_DEPLOYED) return null\n\n\ttry {\n\t\tif (await fsExtra.exists(dbPath)) {\n\t\t\tawait fsExtra.remove(dbPath)\n\t\t}\n\t} catch (error) {\n\t\tconsole.error(`Error deleting the database in ${dbPath}`, error)\n\t}\n}\n\nasync function readDb() {\n\tif (process.env.EPICSHOP_DEPLOYED) return null\n\n\ttry {\n\t\tif (await fsExtra.exists(dbPath)) {\n\t\t\tconst db = DataSchema.parse(await fsExtra.readJSON(dbPath))\n\t\t\treturn db\n\t\t}\n\t} catch (error) {\n\t\tconsole.error(\n\t\t\t`Error reading the database in ${dbPath}, moving it to a .bkp file to avoid parsing errors in the future`,\n\t\t\terror,\n\t\t)\n\t\tvoid fsExtra.move(dbPath, `${dbPath}.bkp`).catch(() => {})\n\t}\n\treturn null\n}\n\nexport async function getAuthInfo() {\n\tconst data = await readDb()\n\treturn data?.authInfo ?? null\n}\n\nexport async function getUserInfo() {\n\tconst db = await readDb()\n\tif (!db?.authInfo) return null\n\n\treturn {\n\t\tid: db.authInfo.id,\n\t\tname: db.discordMember?.displayName ?? db.authInfo.name ?? null,\n\t\temail: db.authInfo.email,\n\t\tavatarUrl:\n\t\t\tdb.discordMember?.avatarURL ??\n\t\t\tgetGravatar({ email: db.authInfo.email, size: 288 }),\n\t}\n}\n\nfunction getGravatar({ email, size }: { email: string; size: number }) {\n\tconst gravatarOptions = new URLSearchParams({\n\t\tsize: size.toString(),\n\t\tdefault: 'identicon',\n\t})\n\tconst gravatarUrl = `https://www.gravatar.com/avatar/${md5(\n\t\temail,\n\t)}?${gravatarOptions.toString()}`\n\treturn gravatarUrl\n}\n\nexport async function requireAuthInfo({\n\trequest,\n\tredirectTo,\n}: {\n\trequest: Request\n\tredirectTo?: string | null\n}) {\n\tconst authInfo = await getAuthInfo()\n\tif (!authInfo) {\n\t\tconst requestUrl = new URL(request.url)\n\t\tredirectTo =\n\t\t\tredirectTo === null\n\t\t\t\t? null\n\t\t\t\t: redirectTo ?? `${requestUrl.pathname}${requestUrl.search}`\n\t\tconst loginParams = redirectTo ? new URLSearchParams({ redirectTo }) : null\n\t\tconst loginRedirect = ['/login', loginParams?.toString()]\n\t\t\t.filter(Boolean)\n\t\t\t.join('?')\n\t\tthrow redirect(loginRedirect)\n\t}\n\treturn authInfo\n}\n\nexport async function setAuthInfo({\n\ttokenSet,\n\temail = 'unknown@example.com',\n\tname,\n}: {\n\ttokenSet: Partial<z.infer<typeof TokenSetSchema>>\n\temail?: string\n\tname?: string\n}) {\n\tconst data = await readDb()\n\tconst authInfo = AuthInfoSchema.parse({ tokenSet, email, name })\n\tawait fsExtra.ensureDir(appDir)\n\tawait fsExtra.writeJSON(dbPath, { ...data, authInfo })\n\treturn authInfo\n}\n\nexport async function getPreferences() {\n\tconst data = await readDb()\n\treturn data?.preferences ?? null\n}\n\nexport async function setPlayerPreferences(\n\tplayerPreferences: z.input<typeof PlayerPreferencesSchema>,\n) {\n\tconst data = await readDb()\n\tconst updatedData = {\n\t\t...data,\n\t\tpreferences: { ...data?.preferences, player: playerPreferences },\n\t}\n\tawait fsExtra.ensureDir(appDir)\n\tawait fsExtra.writeJSON(dbPath, updatedData)\n\treturn updatedData.preferences.player\n}\n\nexport async function setPresencePreferences(\n\tpresnecePreferences: z.input<typeof PresencePreferencesSchema>,\n) {\n\tconst data = await readDb()\n\tconst updatedData = {\n\t\t...data,\n\t\tpreferences: { ...data?.preferences, presence: presnecePreferences },\n\t}\n\tawait fsExtra.ensureDir(appDir)\n\tawait fsExtra.writeJSON(dbPath, updatedData)\n\treturn updatedData.preferences.presence\n}\n\nexport async function getDiscordMember() {\n\tconst data = await readDb()\n\treturn data?.discordMember ?? null\n}\n\nexport async function setDiscordMember(\n\tdiscordMember: z.infer<typeof DiscordMemberSchema>,\n) {\n\tconst data = await readDb()\n\tconst updatedData = {\n\t\t...data,\n\t\tdiscordMember,\n\t}\n\tawait fsExtra.ensureDir(appDir)\n\tawait fsExtra.writeJSON(dbPath, updatedData)\n\treturn updatedData.discordMember\n}\n\nexport async function deleteDiscordInfo() {\n\tconst data = await readDb()\n\tdelete data?.discordMember\n\tawait fsExtra.ensureDir(appDir)\n\tawait fsExtra.writeJSON(dbPath, data)\n}\n\nexport async function readOnboardingData() {\n\tconst data = await readDb()\n\treturn data?.onboarding ?? null\n}\n\nexport async function updateOnboardingData(onboardingData: {\n\tfinishedTourVideo: boolean\n}) {\n\tconst data = await readDb()\n\tconst updatedData = {\n\t\t...data,\n\t\tonboarding: { ...data?.onboarding, ...onboardingData },\n\t}\n\tawait fsExtra.ensureDir(appDir)\n\tawait fsExtra.writeJSON(dbPath, updatedData)\n\treturn updatedData.onboarding\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare function checkForUpdates(): Promise<{
|
|
2
|
+
readonly updatesAvailable: false;
|
|
3
|
+
readonly localCommit?: undefined;
|
|
4
|
+
readonly remoteCommit?: undefined;
|
|
5
|
+
readonly diffLink?: undefined;
|
|
6
|
+
} | {
|
|
7
|
+
readonly updatesAvailable: boolean;
|
|
8
|
+
readonly localCommit: string;
|
|
9
|
+
readonly remoteCommit: string;
|
|
10
|
+
readonly diffLink: string | null;
|
|
11
|
+
} | {
|
|
12
|
+
readonly updatesAvailable: false;
|
|
13
|
+
readonly localCommit: string | undefined;
|
|
14
|
+
readonly remoteCommit: string | undefined;
|
|
15
|
+
readonly diffLink: string | null;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function updateLocalRepo(): Promise<{
|
|
18
|
+
readonly status: "success";
|
|
19
|
+
readonly message: "No updates available.";
|
|
20
|
+
} | {
|
|
21
|
+
readonly status: "success";
|
|
22
|
+
readonly message: "Updated successfully.";
|
|
23
|
+
} | {
|
|
24
|
+
readonly status: "error";
|
|
25
|
+
readonly message: string;
|
|
26
|
+
}>;
|
|
27
|
+
//# sourceMappingURL=git.server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.server.d.ts","sourceRoot":"","sources":["../../src/git.server.ts"],"names":[],"mappings":"AAuBA,wBAAsB,eAAe;;;;;;;;;;;;;;;GA+DpC;AAED,wBAAsB,eAAe;;;;;;;;;GAyCpC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { execa, execaCommand } from 'execa';
|
|
2
|
+
import { getWorkshopRoot } from './apps.server.js';
|
|
3
|
+
import { getErrorMessage } from './utils.js';
|
|
4
|
+
import { checkConnection, getPkgProp } from './utils.server.js';
|
|
5
|
+
const cwd = getWorkshopRoot();
|
|
6
|
+
async function getDiffUrl(commitBefore, commitAfter) {
|
|
7
|
+
try {
|
|
8
|
+
const { stdout: remoteUrl } = await execaCommand('git config --get remote.origin.url', { cwd });
|
|
9
|
+
const [, username, repoName] = remoteUrl.match(/(?:[^/]+\/|:)([^/]+)\/([^.]+)\.git/) ?? [];
|
|
10
|
+
const diffUrl = `https://github.com/${username}/${repoName}/compare/${commitBefore}...${commitAfter}`;
|
|
11
|
+
return diffUrl;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
console.error('Failed to get repository info:', getErrorMessage(error));
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function checkForUpdates() {
|
|
19
|
+
const online = await checkConnection();
|
|
20
|
+
if (!online)
|
|
21
|
+
return { updatesAvailable: false };
|
|
22
|
+
const isInRepo = await execaCommand('git rev-parse --is-inside-work-tree', {
|
|
23
|
+
cwd,
|
|
24
|
+
}).then(() => true, () => false);
|
|
25
|
+
if (!isInRepo) {
|
|
26
|
+
return { updatesAvailable: false };
|
|
27
|
+
}
|
|
28
|
+
const { stdout: remote } = await execaCommand('git remote', { cwd });
|
|
29
|
+
if (!remote) {
|
|
30
|
+
return { updatesAvailable: false };
|
|
31
|
+
}
|
|
32
|
+
let localCommit, remoteCommit;
|
|
33
|
+
try {
|
|
34
|
+
const currentBranch = (await execaCommand('git rev-parse --abbrev-ref HEAD', { cwd })).stdout.trim();
|
|
35
|
+
localCommit = (await execaCommand('git rev-parse --short HEAD', { cwd })).stdout.trim();
|
|
36
|
+
await execaCommand('git fetch --all', { cwd });
|
|
37
|
+
remoteCommit = (await execaCommand(`git rev-parse --short origin/${currentBranch}`, {
|
|
38
|
+
cwd,
|
|
39
|
+
})).stdout.trim();
|
|
40
|
+
const { stdout } = await execa('git', ['rev-list', '--count', '--left-right', 'HEAD...@{upstream}'], { cwd });
|
|
41
|
+
const [, behind = 0] = stdout.trim().split(/\s+/).map(Number);
|
|
42
|
+
const updatesAvailable = behind > 0;
|
|
43
|
+
return {
|
|
44
|
+
updatesAvailable,
|
|
45
|
+
localCommit,
|
|
46
|
+
remoteCommit,
|
|
47
|
+
diffLink: await getDiffUrl(localCommit, remoteCommit),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error('Unable to check for updates', getErrorMessage(error));
|
|
52
|
+
return {
|
|
53
|
+
updatesAvailable: false,
|
|
54
|
+
localCommit,
|
|
55
|
+
remoteCommit,
|
|
56
|
+
diffLink: localCommit && remoteCommit
|
|
57
|
+
? await getDiffUrl(localCommit, remoteCommit)
|
|
58
|
+
: null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export async function updateLocalRepo() {
|
|
63
|
+
try {
|
|
64
|
+
const updates = await checkForUpdates();
|
|
65
|
+
if (!updates.updatesAvailable) {
|
|
66
|
+
return { status: 'success', message: 'No updates available.' };
|
|
67
|
+
}
|
|
68
|
+
const uncommittedChanges = (await execaCommand('git status --porcelain', { cwd })).stdout.trim()
|
|
69
|
+
.length > 0;
|
|
70
|
+
if (uncommittedChanges) {
|
|
71
|
+
console.log('👜 Stashing uncommitted changes...');
|
|
72
|
+
await execaCommand('git stash', { cwd });
|
|
73
|
+
}
|
|
74
|
+
console.log('⬇️ Pulling latest changes...');
|
|
75
|
+
await execaCommand('git pull origin HEAD', { cwd });
|
|
76
|
+
if (uncommittedChanges) {
|
|
77
|
+
console.log('👜 re-applying stashed changes...');
|
|
78
|
+
await execaCommand('git stash pop', { cwd });
|
|
79
|
+
}
|
|
80
|
+
console.log('📦 Re-installing dependencies...');
|
|
81
|
+
await execaCommand('npm install', { cwd, stdio: 'inherit' });
|
|
82
|
+
const postUpdateScript = await getPkgProp(cwd, 'epicshop.scripts.postupdate', '');
|
|
83
|
+
if (postUpdateScript) {
|
|
84
|
+
console.log('🏃 Running post update script...');
|
|
85
|
+
await execaCommand(postUpdateScript, { cwd, stdio: 'inherit' });
|
|
86
|
+
}
|
|
87
|
+
return { status: 'success', message: 'Updated successfully.' };
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return { status: 'error', message: getErrorMessage(error) };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=git.server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.server.js","sourceRoot":"","sources":["../../src/git.server.ts"],"names":[],"mappings":"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","sourcesContent":["import { execa, execaCommand } from 'execa'\nimport { getWorkshopRoot } from './apps.server.js'\nimport { getErrorMessage } from './utils.js'\nimport { checkConnection, getPkgProp } from './utils.server.js'\n\nconst cwd = getWorkshopRoot()\n\nasync function getDiffUrl(commitBefore: string, commitAfter: string) {\n\ttry {\n\t\tconst { stdout: remoteUrl } = await execaCommand(\n\t\t\t'git config --get remote.origin.url',\n\t\t\t{ cwd },\n\t\t)\n\t\tconst [, username, repoName] =\n\t\t\tremoteUrl.match(/(?:[^/]+\\/|:)([^/]+)\\/([^.]+)\\.git/) ?? []\n\t\tconst diffUrl = `https://github.com/${username}/${repoName}/compare/${commitBefore}...${commitAfter}`\n\t\treturn diffUrl\n\t} catch (error) {\n\t\tconsole.error('Failed to get repository info:', getErrorMessage(error))\n\t\treturn null\n\t}\n}\n\nexport async function checkForUpdates() {\n\tconst online = await checkConnection()\n\tif (!online) return { updatesAvailable: false } as const\n\n\tconst isInRepo = await execaCommand('git rev-parse --is-inside-work-tree', {\n\t\tcwd,\n\t}).then(\n\t\t() => true,\n\t\t() => false,\n\t)\n\tif (!isInRepo) {\n\t\treturn { updatesAvailable: false } as const\n\t}\n\n\tconst { stdout: remote } = await execaCommand('git remote', { cwd })\n\tif (!remote) {\n\t\treturn { updatesAvailable: false } as const\n\t}\n\n\tlet localCommit, remoteCommit\n\ttry {\n\t\tconst currentBranch = (\n\t\t\tawait execaCommand('git rev-parse --abbrev-ref HEAD', { cwd })\n\t\t).stdout.trim()\n\n\t\tlocalCommit = (\n\t\t\tawait execaCommand('git rev-parse --short HEAD', { cwd })\n\t\t).stdout.trim()\n\n\t\tawait execaCommand('git fetch --all', { cwd })\n\n\t\tremoteCommit = (\n\t\t\tawait execaCommand(`git rev-parse --short origin/${currentBranch}`, {\n\t\t\t\tcwd,\n\t\t\t})\n\t\t).stdout.trim()\n\n\t\tconst { stdout } = await execa(\n\t\t\t'git',\n\t\t\t['rev-list', '--count', '--left-right', 'HEAD...@{upstream}'],\n\t\t\t{ cwd },\n\t\t)\n\t\tconst [, behind = 0] = stdout.trim().split(/\\s+/).map(Number)\n\t\tconst updatesAvailable = behind > 0\n\n\t\treturn {\n\t\t\tupdatesAvailable,\n\t\t\tlocalCommit,\n\t\t\tremoteCommit,\n\t\t\tdiffLink: await getDiffUrl(localCommit, remoteCommit),\n\t\t} as const\n\t} catch (error) {\n\t\tconsole.error('Unable to check for updates', getErrorMessage(error))\n\t\treturn {\n\t\t\tupdatesAvailable: false,\n\t\t\tlocalCommit,\n\t\t\tremoteCommit,\n\t\t\tdiffLink:\n\t\t\t\tlocalCommit && remoteCommit\n\t\t\t\t\t? await getDiffUrl(localCommit, remoteCommit)\n\t\t\t\t\t: null,\n\t\t} as const\n\t}\n}\n\nexport async function updateLocalRepo() {\n\ttry {\n\t\tconst updates = await checkForUpdates()\n\t\tif (!updates.updatesAvailable) {\n\t\t\treturn { status: 'success', message: 'No updates available.' } as const\n\t\t}\n\n\t\tconst uncommittedChanges =\n\t\t\t(await execaCommand('git status --porcelain', { cwd })).stdout.trim()\n\t\t\t\t.length > 0\n\n\t\tif (uncommittedChanges) {\n\t\t\tconsole.log('👜 Stashing uncommitted changes...')\n\t\t\tawait execaCommand('git stash', { cwd })\n\t\t}\n\n\t\tconsole.log('⬇️ Pulling latest changes...')\n\t\tawait execaCommand('git pull origin HEAD', { cwd })\n\n\t\tif (uncommittedChanges) {\n\t\t\tconsole.log('👜 re-applying stashed changes...')\n\t\t\tawait execaCommand('git stash pop', { cwd })\n\t\t}\n\n\t\tconsole.log('📦 Re-installing dependencies...')\n\t\tawait execaCommand('npm install', { cwd, stdio: 'inherit' })\n\n\t\tconst postUpdateScript = await getPkgProp(\n\t\t\tcwd,\n\t\t\t'epicshop.scripts.postupdate',\n\t\t\t'',\n\t\t)\n\t\tif (postUpdateScript) {\n\t\t\tconsole.log('🏃 Running post update script...')\n\t\t\tawait execaCommand(postUpdateScript, { cwd, stdio: 'inherit' })\n\t\t}\n\n\t\treturn { status: 'success', message: 'Updated successfully.' } as const\n\t} catch (error) {\n\t\treturn { status: 'error', message: getErrorMessage(error) } as const\n\t}\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type CustomReactType = {
|
|
2
|
+
useEffect: (cb: () => (() => void) | void, deps: Array<any>) => void;
|
|
3
|
+
createElement: (type: string, props: any, ...children: Array<any>) => any;
|
|
4
|
+
};
|
|
5
|
+
export declare function EpicShopIFrameSync<ReactType extends CustomReactType>({ React, navigate, }: {
|
|
6
|
+
React: ReactType;
|
|
7
|
+
navigate: (...args: Array<any>) => void;
|
|
8
|
+
}): any;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=iframe-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"iframe-sync.d.ts","sourceRoot":"","sources":["../../src/iframe-sync.tsx"],"names":[],"mappings":"AAwBA,KAAK,eAAe,GAAG;IACtB,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,CAAA;IACpE,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,QAAQ,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,GAAG,CAAA;CACzE,CAAA;AAgCD,wBAAgB,kBAAkB,CAAC,SAAS,SAAS,eAAe,EAAE,EACrE,KAAK,EACL,QAAQ,GACR,EAAE;IACF,KAAK,EAAE,SAAS,CAAA;IAChB,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,CAAA;CACvC,OAoDA"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This file is kinda weird. EpicShop actually bundles react and react router to
|
|
3
|
+
avoid getting version clashes, but this component is used in the "host"
|
|
4
|
+
application. Anything we use in this file will be this file's version of that
|
|
5
|
+
dependency, which in the case of bundled dependencies would be different from
|
|
6
|
+
the host app's version. We want to avoid shipping two versions of React and
|
|
7
|
+
react-router to the client. So we need to accept React and navigate as props
|
|
8
|
+
rather than just using those things directly.
|
|
9
|
+
|
|
10
|
+
To reduce the annoyance, we'll have the host applications have a file like this:
|
|
11
|
+
|
|
12
|
+
// Ignore this file please
|
|
13
|
+
import { EpicShopIFrameSync } from '@epic-web/workshop-utils/iframe-sync'
|
|
14
|
+
import { useNavigate } from '@remix-run/react'
|
|
15
|
+
import * as React from 'react'
|
|
16
|
+
|
|
17
|
+
export function EpicShop() {
|
|
18
|
+
const navigate = useNavigate()
|
|
19
|
+
return <EpicShopIFrameSync React={React} navigate={navigate} />
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
*/
|
|
23
|
+
let effectSetup = false;
|
|
24
|
+
const iframeSyncScript = /* javascript */ `
|
|
25
|
+
if (window.parent !== window) {
|
|
26
|
+
window.__epicshop__ = window.__epicshop__ || {};
|
|
27
|
+
window.parent.postMessage(
|
|
28
|
+
{ type: 'epicshop:loaded', url: window.location.href },
|
|
29
|
+
'*'
|
|
30
|
+
);
|
|
31
|
+
function handleMessage(event) {
|
|
32
|
+
const { type, params } = event.data
|
|
33
|
+
if (type === 'epicshop:navigate-call') {
|
|
34
|
+
const [distanceOrUrl, options] = params
|
|
35
|
+
if (typeof distanceOrUrl === 'number') {
|
|
36
|
+
window.history.go(distanceOrUrl)
|
|
37
|
+
} else {
|
|
38
|
+
if (options?.replace) {
|
|
39
|
+
window.location.replace(distanceOrUrl)
|
|
40
|
+
} else {
|
|
41
|
+
window.location.assign(distanceOrUrl)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
window.addEventListener('message', handleMessage)
|
|
48
|
+
window.__epicshop__.onHydrated = function() {
|
|
49
|
+
window.removeEventListener('message', handleMessage)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
export function EpicShopIFrameSync({ React, navigate, }) {
|
|
54
|
+
// communicate with parent
|
|
55
|
+
React.useEffect(() => {
|
|
56
|
+
if (effectSetup)
|
|
57
|
+
return;
|
|
58
|
+
effectSetup = true;
|
|
59
|
+
if (window.parent === window)
|
|
60
|
+
return;
|
|
61
|
+
// @ts-expect-error - this is fine 🔥
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
63
|
+
window.__epicshop__?.onHydrated?.();
|
|
64
|
+
const methods = [
|
|
65
|
+
'pushState',
|
|
66
|
+
'replaceState',
|
|
67
|
+
'go',
|
|
68
|
+
'forward',
|
|
69
|
+
'back',
|
|
70
|
+
];
|
|
71
|
+
for (const method of methods) {
|
|
72
|
+
// @ts-expect-error - this is fine 🔥
|
|
73
|
+
window.history[method] = new Proxy(window.history[method], {
|
|
74
|
+
// eslint-disable-next-line no-loop-func
|
|
75
|
+
apply(target, thisArg, argArray) {
|
|
76
|
+
window.parent.postMessage({ type: 'epicshop:history-call', method, args: argArray }, '*');
|
|
77
|
+
// @ts-expect-error - this is fine too 🙃
|
|
78
|
+
return target.apply(thisArg, argArray);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}, []);
|
|
83
|
+
// listen for messages from parent
|
|
84
|
+
React.useEffect(() => {
|
|
85
|
+
function handleMessage(event) {
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
87
|
+
const { type, params } = event.data;
|
|
88
|
+
if (type === 'epicshop:navigate-call') {
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
90
|
+
navigate(...params);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
window.addEventListener('message', handleMessage);
|
|
94
|
+
return () => window.removeEventListener('message', handleMessage);
|
|
95
|
+
}, [navigate]);
|
|
96
|
+
return React.createElement('script', {
|
|
97
|
+
type: 'module',
|
|
98
|
+
dangerouslySetInnerHTML: { __html: iframeSyncScript },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=iframe-sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"iframe-sync.js","sourceRoot":"","sources":["../../src/iframe-sync.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,IAAI,WAAW,GAAG,KAAK,CAAA;AAOvB,MAAM,gBAAgB,GAAG,gBAAgB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BzC,CAAA;AAED,MAAM,UAAU,kBAAkB,CAAoC,EACrE,KAAK,EACL,QAAQ,GAIR;IACA,0BAA0B;IAC1B,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACpB,IAAI,WAAW;YAAE,OAAM;QACvB,WAAW,GAAG,IAAI,CAAA;QAClB,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM;YAAE,OAAM;QAEpC,qCAAqC;QACrC,yGAAyG;QACzG,MAAM,CAAC,YAAY,EAAE,UAAU,EAAE,EAAE,CAAA;QAEnC,MAAM,OAAO,GAAG;YACf,WAAW;YACX,cAAc;YACd,IAAI;YACJ,SAAS;YACT,MAAM;SACG,CAAA;QACV,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC9B,qCAAqC;YACrC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;gBAC1D,wCAAwC;gBACxC,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ;oBAC9B,MAAM,CAAC,MAAM,CAAC,WAAW,CACxB,EAAE,IAAI,EAAE,uBAAuB,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,EACzD,GAAG,CACH,CAAA;oBACD,yCAAyC;oBACzC,OAAO,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;gBACvC,CAAC;aACD,CAAC,CAAA;QACH,CAAC;IACF,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,kCAAkC;IAClC,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACpB,SAAS,aAAa,CAAC,KAAmB;YACzC,mEAAmE;YACnE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,IAAI,CAAA;YACnC,IAAI,IAAI,KAAK,wBAAwB,EAAE,CAAC;gBACvC,iEAAiE;gBACjE,QAAQ,CAAC,GAAG,MAAM,CAAC,CAAA;YACpB,CAAC;QACF,CAAC;QACD,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,aAAa,CAAC,CAAA;QACjD,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,aAAa,CAAC,CAAA;IAClE,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAA;IAEd,OAAO,KAAK,CAAC,aAAa,CAAC,QAAQ,EAAE;QACpC,IAAI,EAAE,QAAQ;QACd,uBAAuB,EAAE,EAAE,MAAM,EAAE,gBAAgB,EAAE;KACrD,CAAC,CAAA;AACH,CAAC","sourcesContent":["/*\nThis file is kinda weird. EpicShop actually bundles react and react router to\navoid getting version clashes, but this component is used in the \"host\"\napplication. Anything we use in this file will be this file's version of that\ndependency, which in the case of bundled dependencies would be different from\nthe host app's version. We want to avoid shipping two versions of React and\nreact-router to the client. So we need to accept React and navigate as props\nrather than just using those things directly.\n\nTo reduce the annoyance, we'll have the host applications have a file like this:\n\n// Ignore this file please\nimport { EpicShopIFrameSync } from '@epic-web/workshop-utils/iframe-sync'\nimport { useNavigate } from '@remix-run/react'\nimport * as React from 'react'\n\nexport function EpicShop() {\n\tconst navigate = useNavigate()\n\treturn <EpicShopIFrameSync React={React} navigate={navigate} />\n}\n\n */\nlet effectSetup = false\n\ntype CustomReactType = {\n\tuseEffect: (cb: () => (() => void) | void, deps: Array<any>) => void\n\tcreateElement: (type: string, props: any, ...children: Array<any>) => any\n}\n\nconst iframeSyncScript = /* javascript */ `\nif (window.parent !== window) {\n\twindow.__epicshop__ = window.__epicshop__ || {};\n\twindow.parent.postMessage(\n\t\t{ type: 'epicshop:loaded', url: window.location.href },\n\t\t'*'\n\t);\n\tfunction handleMessage(event) {\n\t\tconst { type, params } = event.data\n\t\tif (type === 'epicshop:navigate-call') {\n\t\t\tconst [distanceOrUrl, options] = params\n\t\t\tif (typeof distanceOrUrl === 'number') {\n\t\t\t\twindow.history.go(distanceOrUrl)\n\t\t\t} else {\n\t\t\t\tif (options?.replace) {\n\t\t\t\t\twindow.location.replace(distanceOrUrl)\n\t\t\t\t} else {\n\t\t\t\t\twindow.location.assign(distanceOrUrl)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\twindow.addEventListener('message', handleMessage)\n\twindow.__epicshop__.onHydrated = function() {\n\t\twindow.removeEventListener('message', handleMessage)\n\t};\n}\n`\n\nexport function EpicShopIFrameSync<ReactType extends CustomReactType>({\n\tReact,\n\tnavigate,\n}: {\n\tReact: ReactType\n\tnavigate: (...args: Array<any>) => void\n}) {\n\t// communicate with parent\n\tReact.useEffect(() => {\n\t\tif (effectSetup) return\n\t\teffectSetup = true\n\t\tif (window.parent === window) return\n\n\t\t// @ts-expect-error - this is fine 🔥\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access\n\t\twindow.__epicshop__?.onHydrated?.()\n\n\t\tconst methods = [\n\t\t\t'pushState',\n\t\t\t'replaceState',\n\t\t\t'go',\n\t\t\t'forward',\n\t\t\t'back',\n\t\t] as const\n\t\tfor (const method of methods) {\n\t\t\t// @ts-expect-error - this is fine 🔥\n\t\t\twindow.history[method] = new Proxy(window.history[method], {\n\t\t\t\t// eslint-disable-next-line no-loop-func\n\t\t\t\tapply(target, thisArg, argArray) {\n\t\t\t\t\twindow.parent.postMessage(\n\t\t\t\t\t\t{ type: 'epicshop:history-call', method, args: argArray },\n\t\t\t\t\t\t'*',\n\t\t\t\t\t)\n\t\t\t\t\t// @ts-expect-error - this is fine too 🙃\n\t\t\t\t\treturn target.apply(thisArg, argArray)\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t}, [])\n\n\t// listen for messages from parent\n\tReact.useEffect(() => {\n\t\tfunction handleMessage(event: MessageEvent) {\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n\t\t\tconst { type, params } = event.data\n\t\t\tif (type === 'epicshop:navigate-call') {\n\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n\t\t\t\tnavigate(...params)\n\t\t\t}\n\t\t}\n\t\twindow.addEventListener('message', handleMessage)\n\t\treturn () => window.removeEventListener('message', handleMessage)\n\t}, [navigate])\n\n\treturn React.createElement('script', {\n\t\ttype: 'module',\n\t\tdangerouslySetInnerHTML: { __html: iframeSyncScript },\n\t})\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playwright.server.d.ts","sourceRoot":"","sources":["../../src/playwright.server.ts"],"names":[],"mappings":"AAKA,wBAAsB,qBAAqB;;;KAe1C;AAuBD,wBAAgB,mBAAmB,SA4DlC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import crossSpawn from 'cross-spawn';
|
|
3
|
+
import z from 'zod';
|
|
4
|
+
import { getApps, isProblemApp } from './apps.server.js';
|
|
5
|
+
export async function getInBrowserTestPages() {
|
|
6
|
+
const apps = (await getApps())
|
|
7
|
+
.filter(isProblemApp)
|
|
8
|
+
.filter(a => a.test.type === 'browser');
|
|
9
|
+
const pages = apps.map(app => {
|
|
10
|
+
if (app.test.type !== 'browser')
|
|
11
|
+
return null;
|
|
12
|
+
const { pathname } = app.test;
|
|
13
|
+
return app.test.testFiles.map(testFile => {
|
|
14
|
+
return {
|
|
15
|
+
path: `${pathname}${testFile}`,
|
|
16
|
+
testFile,
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
return pages.filter(Boolean).flat();
|
|
21
|
+
}
|
|
22
|
+
const sleep = (time) => new Promise(resolve => setTimeout(resolve, time));
|
|
23
|
+
async function waitFor(cb, { timeout = 1000, interval = 30 } = {}) {
|
|
24
|
+
const timeEnd = Date.now() + timeout;
|
|
25
|
+
let lastError = null;
|
|
26
|
+
while (Date.now() < timeEnd) {
|
|
27
|
+
try {
|
|
28
|
+
const result = await cb();
|
|
29
|
+
if (result)
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
lastError = error;
|
|
34
|
+
}
|
|
35
|
+
await sleep(interval);
|
|
36
|
+
}
|
|
37
|
+
throw lastError || new Error(`waitFor timed out after ${timeout}ms`);
|
|
38
|
+
}
|
|
39
|
+
export function setupInBrowserTests() {
|
|
40
|
+
// doing this because playwright needs the tests to be registered synchoronously
|
|
41
|
+
const code = `import('@epic-web/workshop-utils/playwright.server').then(({ getInBrowserTestPages }) => getInBrowserTestPages().then(r => console.log(JSON.stringify(r))))`;
|
|
42
|
+
const result = crossSpawn.sync('node', ['--eval', code], {
|
|
43
|
+
encoding: 'utf-8',
|
|
44
|
+
});
|
|
45
|
+
if (result.status !== 0) {
|
|
46
|
+
console.error(result.output.join('\n'));
|
|
47
|
+
throw new Error(`Failed to get in-browser test pages. Status: ${result.status}.`);
|
|
48
|
+
}
|
|
49
|
+
const testPages = z
|
|
50
|
+
.array(z.object({ path: z.string() }))
|
|
51
|
+
.parse(JSON.parse(result.stdout));
|
|
52
|
+
for (const testPage of testPages) {
|
|
53
|
+
// eslint-disable-next-line no-loop-func
|
|
54
|
+
test(testPage.path, async ({ page }) => {
|
|
55
|
+
const errors = [];
|
|
56
|
+
const logs = [];
|
|
57
|
+
const infos = [];
|
|
58
|
+
page.on('console', message => {
|
|
59
|
+
switch (message.type()) {
|
|
60
|
+
case 'error': {
|
|
61
|
+
errors.push(message.text());
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case 'log': {
|
|
65
|
+
logs.push(message.text());
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case 'info': {
|
|
69
|
+
infos.push(message.text());
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
default: {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
await page.goto(testPage.path);
|
|
78
|
+
await page.waitForLoadState();
|
|
79
|
+
await waitFor(() => infos.find(info => info.includes('status: pending')), { timeout: 10_000 });
|
|
80
|
+
const result = await Promise.race([
|
|
81
|
+
waitFor(() => logs.find(log => log.includes('status: pass')), {
|
|
82
|
+
timeout: 10_000,
|
|
83
|
+
}),
|
|
84
|
+
waitFor(() => (errors.length > 0 ? errors : null), {
|
|
85
|
+
timeout: 10_000,
|
|
86
|
+
}).then(errors => {
|
|
87
|
+
throw errors;
|
|
88
|
+
}),
|
|
89
|
+
]);
|
|
90
|
+
expect(result).toContain('status: pass');
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=playwright.server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playwright.server.js","sourceRoot":"","sources":["../../src/playwright.server.ts"],"names":[],"mappings":"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","sourcesContent":["import { test, expect } from '@playwright/test'\nimport crossSpawn from 'cross-spawn'\nimport z from 'zod'\nimport { getApps, isProblemApp } from './apps.server.js'\n\nexport async function getInBrowserTestPages() {\n\tconst apps = (await getApps())\n\t\t.filter(isProblemApp)\n\t\t.filter(a => a.test.type === 'browser')\n\tconst pages = apps.map(app => {\n\t\tif (app.test.type !== 'browser') return null\n\t\tconst { pathname } = app.test\n\t\treturn app.test.testFiles.map(testFile => {\n\t\t\treturn {\n\t\t\t\tpath: `${pathname}${testFile}`,\n\t\t\t\ttestFile,\n\t\t\t}\n\t\t})\n\t})\n\treturn pages.filter(Boolean).flat()\n}\n\nconst sleep = (time: number) =>\n\tnew Promise(resolve => setTimeout(resolve, time))\n\nasync function waitFor<ReturnValue>(\n\tcb: () => ReturnValue | Promise<ReturnValue> | undefined | null,\n\t{ timeout = 1000, interval = 30 } = {},\n) {\n\tconst timeEnd = Date.now() + timeout\n\tlet lastError: unknown | null = null\n\twhile (Date.now() < timeEnd) {\n\t\ttry {\n\t\t\tconst result = await cb()\n\t\t\tif (result) return result\n\t\t} catch (error) {\n\t\t\tlastError = error\n\t\t}\n\t\tawait sleep(interval)\n\t}\n\tthrow lastError || new Error(`waitFor timed out after ${timeout}ms`)\n}\n\nexport function setupInBrowserTests() {\n\t// doing this because playwright needs the tests to be registered synchoronously\n\tconst code = `import('@epic-web/workshop-utils/playwright.server').then(({ getInBrowserTestPages }) => getInBrowserTestPages().then(r => console.log(JSON.stringify(r))))`\n\tconst result = crossSpawn.sync('node', ['--eval', code], {\n\t\tencoding: 'utf-8',\n\t})\n\tif (result.status !== 0) {\n\t\tconsole.error(result.output.join('\\n'))\n\t\tthrow new Error(\n\t\t\t`Failed to get in-browser test pages. Status: ${result.status}.`,\n\t\t)\n\t}\n\tconst testPages = z\n\t\t.array(z.object({ path: z.string() }))\n\t\t.parse(JSON.parse(result.stdout))\n\n\tfor (const testPage of testPages) {\n\t\t// eslint-disable-next-line no-loop-func\n\t\ttest(testPage.path, async ({ page }) => {\n\t\t\tconst errors: Array<string> = []\n\t\t\tconst logs: Array<string> = []\n\t\t\tconst infos: Array<string> = []\n\t\t\tpage.on('console', message => {\n\t\t\t\tswitch (message.type()) {\n\t\t\t\t\tcase 'error': {\n\t\t\t\t\t\terrors.push(message.text())\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tcase 'log': {\n\t\t\t\t\t\tlogs.push(message.text())\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tcase 'info': {\n\t\t\t\t\t\tinfos.push(message.text())\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tdefault: {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t\t\tawait page.goto(testPage.path)\n\t\t\tawait page.waitForLoadState()\n\t\t\tawait waitFor(\n\t\t\t\t() => infos.find(info => info.includes('status: pending')),\n\t\t\t\t{ timeout: 10_000 },\n\t\t\t)\n\t\t\tconst result = await Promise.race([\n\t\t\t\twaitFor(() => logs.find(log => log.includes('status: pass')), {\n\t\t\t\t\ttimeout: 10_000,\n\t\t\t\t}),\n\t\t\t\twaitFor(() => (errors.length > 0 ? errors : null), {\n\t\t\t\t\ttimeout: 10_000,\n\t\t\t\t}).then(errors => {\n\t\t\t\t\tthrow errors\n\t\t\t\t}),\n\t\t\t])\n\t\t\texpect(result).toContain('status: pass')\n\t\t})\n\t}\n}\n"]}
|