@epic-web/workshop-utils 0.0.0-semantically-released

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.
Files changed (79) hide show
  1. package/README.md +3 -0
  2. package/dist/esm/apps.server.d.ts +4205 -0
  3. package/dist/esm/apps.server.d.ts.map +1 -0
  4. package/dist/esm/apps.server.js +1198 -0
  5. package/dist/esm/apps.server.js.map +1 -0
  6. package/dist/esm/cache.server.d.ts +940 -0
  7. package/dist/esm/cache.server.d.ts.map +1 -0
  8. package/dist/esm/cache.server.js +161 -0
  9. package/dist/esm/cache.server.js.map +1 -0
  10. package/dist/esm/compile-mdx.server.d.ts +12 -0
  11. package/dist/esm/compile-mdx.server.d.ts.map +1 -0
  12. package/dist/esm/compile-mdx.server.js +285 -0
  13. package/dist/esm/compile-mdx.server.js.map +1 -0
  14. package/dist/esm/config.server.d.ts +348 -0
  15. package/dist/esm/config.server.d.ts.map +1 -0
  16. package/dist/esm/config.server.js +231 -0
  17. package/dist/esm/config.server.js.map +1 -0
  18. package/dist/esm/db.server.d.ts +463 -0
  19. package/dist/esm/db.server.d.ts.map +1 -0
  20. package/dist/esm/db.server.js +260 -0
  21. package/dist/esm/db.server.js.map +1 -0
  22. package/dist/esm/diff.server.d.ts +18 -0
  23. package/dist/esm/diff.server.d.ts.map +1 -0
  24. package/dist/esm/diff.server.js +437 -0
  25. package/dist/esm/diff.server.js.map +1 -0
  26. package/dist/esm/env.server.d.ts +61 -0
  27. package/dist/esm/env.server.d.ts.map +1 -0
  28. package/dist/esm/env.server.js +42 -0
  29. package/dist/esm/env.server.js.map +1 -0
  30. package/dist/esm/epic-api.server.d.ts +227 -0
  31. package/dist/esm/epic-api.server.d.ts.map +1 -0
  32. package/dist/esm/epic-api.server.js +529 -0
  33. package/dist/esm/epic-api.server.js.map +1 -0
  34. package/dist/esm/git.server.d.ts +49 -0
  35. package/dist/esm/git.server.d.ts.map +1 -0
  36. package/dist/esm/git.server.js +135 -0
  37. package/dist/esm/git.server.js.map +1 -0
  38. package/dist/esm/iframe-sync.d.ts +10 -0
  39. package/dist/esm/iframe-sync.d.ts.map +1 -0
  40. package/dist/esm/iframe-sync.js +97 -0
  41. package/dist/esm/iframe-sync.js.map +1 -0
  42. package/dist/esm/modified-time.server.d.ts +7 -0
  43. package/dist/esm/modified-time.server.d.ts.map +1 -0
  44. package/dist/esm/modified-time.server.js +80 -0
  45. package/dist/esm/modified-time.server.js.map +1 -0
  46. package/dist/esm/notifications.server.d.ts +56 -0
  47. package/dist/esm/notifications.server.d.ts.map +1 -0
  48. package/dist/esm/notifications.server.js +65 -0
  49. package/dist/esm/notifications.server.js.map +1 -0
  50. package/dist/esm/package.json +3 -0
  51. package/dist/esm/playwright.server.d.ts +6 -0
  52. package/dist/esm/playwright.server.d.ts.map +1 -0
  53. package/dist/esm/playwright.server.js +95 -0
  54. package/dist/esm/playwright.server.js.map +1 -0
  55. package/dist/esm/process-manager.server.d.ts +77 -0
  56. package/dist/esm/process-manager.server.d.ts.map +1 -0
  57. package/dist/esm/process-manager.server.js +266 -0
  58. package/dist/esm/process-manager.server.js.map +1 -0
  59. package/dist/esm/test.d.ts +16 -0
  60. package/dist/esm/test.d.ts.map +1 -0
  61. package/dist/esm/test.js +56 -0
  62. package/dist/esm/test.js.map +1 -0
  63. package/dist/esm/timing.server.d.ts +20 -0
  64. package/dist/esm/timing.server.d.ts.map +1 -0
  65. package/dist/esm/timing.server.js +88 -0
  66. package/dist/esm/timing.server.js.map +1 -0
  67. package/dist/esm/user.server.d.ts +17 -0
  68. package/dist/esm/user.server.d.ts.map +1 -0
  69. package/dist/esm/user.server.js +38 -0
  70. package/dist/esm/user.server.js.map +1 -0
  71. package/dist/esm/utils.d.ts +2 -0
  72. package/dist/esm/utils.d.ts.map +1 -0
  73. package/dist/esm/utils.js +13 -0
  74. package/dist/esm/utils.js.map +1 -0
  75. package/dist/esm/utils.server.d.ts +9 -0
  76. package/dist/esm/utils.server.d.ts.map +1 -0
  77. package/dist/esm/utils.server.js +45 -0
  78. package/dist/esm/utils.server.js.map +1 -0
  79. package/package.json +221 -0
@@ -0,0 +1,135 @@
1
+ import { execa, execaCommand } from 'execa';
2
+ import { getWorkshopRoot } from './apps.server.js';
3
+ import { cachified, checkForUpdatesCache } from './cache.server.js';
4
+ import { getWorkshopConfig } from './config.server.js';
5
+ import { getErrorMessage } from './utils.js';
6
+ import { checkConnection } from './utils.server.js';
7
+ async function getDiffUrl(commitBefore, commitAfter) {
8
+ const cwd = getWorkshopRoot();
9
+ try {
10
+ const { stdout: remoteUrl } = await execaCommand('git config --get remote.origin.url', { cwd });
11
+ const [, username, repoName] = remoteUrl.match(/(?:[^/]+\/|:)([^/]+)\/([^.]+)\.git/) ?? [];
12
+ const diffUrl = `https://github.com/${username}/${repoName}/compare/${commitBefore}...${commitAfter}`;
13
+ return diffUrl;
14
+ }
15
+ catch (error) {
16
+ console.error('Failed to get repository info:', getErrorMessage(error));
17
+ return null;
18
+ }
19
+ }
20
+ export async function checkForUpdates() {
21
+ const cwd = getWorkshopRoot();
22
+ const online = await checkConnection();
23
+ if (!online)
24
+ return { updatesAvailable: false };
25
+ const isInRepo = await execaCommand('git rev-parse --is-inside-work-tree', {
26
+ cwd,
27
+ }).then(() => true, () => false);
28
+ if (!isInRepo) {
29
+ return { updatesAvailable: false };
30
+ }
31
+ const { stdout: remote } = await execaCommand('git remote', { cwd });
32
+ if (!remote) {
33
+ return { updatesAvailable: false };
34
+ }
35
+ let localCommit, remoteCommit;
36
+ try {
37
+ const currentBranch = (await execaCommand('git rev-parse --abbrev-ref HEAD', { cwd })).stdout.trim();
38
+ localCommit = (await execaCommand('git rev-parse --short HEAD', { cwd })).stdout.trim();
39
+ await execaCommand('git fetch --all', { cwd });
40
+ remoteCommit = (await execaCommand(`git rev-parse --short origin/${currentBranch}`, {
41
+ cwd,
42
+ })).stdout.trim();
43
+ const { stdout } = await execa('git', ['rev-list', '--count', '--left-right', 'HEAD...@{upstream}'], { cwd });
44
+ const [, behind = 0] = stdout.trim().split(/\s+/).map(Number);
45
+ const updatesAvailable = behind > 0;
46
+ return {
47
+ updatesAvailable,
48
+ localCommit,
49
+ remoteCommit,
50
+ diffLink: await getDiffUrl(localCommit, remoteCommit),
51
+ };
52
+ }
53
+ catch (error) {
54
+ console.error('Unable to check for updates', getErrorMessage(error));
55
+ return {
56
+ updatesAvailable: false,
57
+ localCommit,
58
+ remoteCommit,
59
+ diffLink: localCommit && remoteCommit
60
+ ? await getDiffUrl(localCommit, remoteCommit)
61
+ : null,
62
+ };
63
+ }
64
+ }
65
+ export async function checkForUpdatesCached() {
66
+ const key = 'checkForUpdates';
67
+ return cachified({
68
+ ttl: 1000 * 60,
69
+ swr: 1000 * 60 * 60 * 24,
70
+ key,
71
+ getFreshValue: checkForUpdates,
72
+ cache: checkForUpdatesCache,
73
+ });
74
+ }
75
+ export async function updateLocalRepo() {
76
+ const cwd = getWorkshopRoot();
77
+ try {
78
+ const updates = await checkForUpdates();
79
+ if (!updates.updatesAvailable) {
80
+ return { status: 'success', message: 'No updates available.' };
81
+ }
82
+ const uncommittedChanges = (await execaCommand('git status --porcelain', { cwd })).stdout.trim()
83
+ .length > 0;
84
+ if (uncommittedChanges) {
85
+ console.log('👜 Stashing uncommitted changes...');
86
+ await execaCommand('git stash --include-untracked', { cwd });
87
+ }
88
+ console.log('⬇️ Pulling latest changes...');
89
+ await execaCommand('git pull origin HEAD', { cwd });
90
+ if (uncommittedChanges) {
91
+ console.log('👜 re-applying stashed changes...');
92
+ await execaCommand('git stash pop', { cwd });
93
+ }
94
+ console.log('📦 Re-installing dependencies...');
95
+ await execaCommand('npm install', { cwd, stdio: 'inherit' });
96
+ const postUpdateScript = getWorkshopConfig().scripts?.postupdate;
97
+ if (postUpdateScript) {
98
+ console.log('🏃 Running post update script...');
99
+ await execaCommand(postUpdateScript, { cwd, stdio: 'inherit' });
100
+ }
101
+ return { status: 'success', message: 'Updated successfully.' };
102
+ }
103
+ catch (error) {
104
+ return { status: 'error', message: getErrorMessage(error) };
105
+ }
106
+ }
107
+ export async function getCommitInfo() {
108
+ const cwd = getWorkshopRoot();
109
+ try {
110
+ const { stdout: hash } = await execaCommand('git rev-parse HEAD', { cwd });
111
+ const { stdout: message } = await execaCommand('git log -1 --pretty=%B', {
112
+ cwd,
113
+ });
114
+ const { stdout: date } = await execaCommand('git log -1 --format=%cI', {
115
+ cwd,
116
+ });
117
+ return { hash: hash.trim(), message: message.trim(), date: date.trim() };
118
+ }
119
+ catch (error) {
120
+ console.error('Failed to get commit info:', getErrorMessage(error));
121
+ return null;
122
+ }
123
+ }
124
+ export async function getLatestWorkshopAppVersion() {
125
+ const cwd = getWorkshopRoot();
126
+ try {
127
+ const { stdout } = await execaCommand('npm view @epic-web/workshop-app version', { cwd });
128
+ return stdout.trim();
129
+ }
130
+ catch (error) {
131
+ console.error('Failed to get latest workshop app version:', getErrorMessage(error));
132
+ return null;
133
+ }
134
+ }
135
+ //# 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 { cachified, checkForUpdatesCache } from './cache.server.js'\nimport { getWorkshopConfig } from './config.server.js'\nimport { getErrorMessage } from './utils.js'\nimport { checkConnection } from './utils.server.js'\n\nasync function getDiffUrl(commitBefore: string, commitAfter: string) {\n\tconst cwd = getWorkshopRoot()\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 cwd = getWorkshopRoot()\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 checkForUpdatesCached() {\n\tconst key = 'checkForUpdates'\n\treturn cachified({\n\t\tttl: 1000 * 60,\n\t\tswr: 1000 * 60 * 60 * 24,\n\t\tkey,\n\t\tgetFreshValue: checkForUpdates,\n\t\tcache: checkForUpdatesCache,\n\t})\n}\n\nexport async function updateLocalRepo() {\n\tconst cwd = getWorkshopRoot()\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 --include-untracked', { 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 = getWorkshopConfig().scripts?.postupdate\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\nexport async function getCommitInfo() {\n\tconst cwd = getWorkshopRoot()\n\ttry {\n\t\tconst { stdout: hash } = await execaCommand('git rev-parse HEAD', { cwd })\n\t\tconst { stdout: message } = await execaCommand('git log -1 --pretty=%B', {\n\t\t\tcwd,\n\t\t})\n\t\tconst { stdout: date } = await execaCommand('git log -1 --format=%cI', {\n\t\t\tcwd,\n\t\t})\n\t\treturn { hash: hash.trim(), message: message.trim(), date: date.trim() }\n\t} catch (error) {\n\t\tconsole.error('Failed to get commit info:', getErrorMessage(error))\n\t\treturn null\n\t}\n}\n\nexport async function getLatestWorkshopAppVersion() {\n\tconst cwd = getWorkshopRoot()\n\ttry {\n\t\tconst { stdout } = await execaCommand(\n\t\t\t'npm view @epic-web/workshop-app version',\n\t\t\t{ cwd },\n\t\t)\n\t\treturn stdout.trim()\n\t} catch (error) {\n\t\tconsole.error(\n\t\t\t'Failed to get latest workshop app version:',\n\t\t\tgetErrorMessage(error),\n\t\t)\n\t\treturn null\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,OAiDA"}
@@ -0,0 +1,97 @@
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
+ window.__epicshop__?.onHydrated?.();
63
+ const methods = [
64
+ 'pushState',
65
+ 'replaceState',
66
+ 'go',
67
+ 'forward',
68
+ 'back',
69
+ ];
70
+ for (const method of methods) {
71
+ // @ts-expect-error - this is fine 🔥
72
+ window.history[method] = new Proxy(window.history[method], {
73
+ apply(target, thisArg, argArray) {
74
+ window.parent.postMessage({ type: 'epicshop:history-call', method, args: argArray }, '*');
75
+ // @ts-expect-error - this is fine too 🙃
76
+ return target.apply(thisArg, argArray);
77
+ },
78
+ });
79
+ }
80
+ }, []);
81
+ // listen for messages from parent
82
+ React.useEffect(() => {
83
+ function handleMessage(event) {
84
+ const { type, params } = event.data;
85
+ if (type === 'epicshop:navigate-call') {
86
+ navigate(...params);
87
+ }
88
+ }
89
+ window.addEventListener('message', handleMessage);
90
+ return () => window.removeEventListener('message', handleMessage);
91
+ }, [navigate]);
92
+ return React.createElement('script', {
93
+ type: 'module',
94
+ dangerouslySetInnerHTML: { __html: iframeSyncScript },
95
+ });
96
+ }
97
+ //# 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;QAErC,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,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,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,IAAI,CAAA;YACnC,IAAI,IAAI,KAAK,wBAAwB,EAAE,CAAC;gBACvC,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\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\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\tconst { type, params } = event.data\n\t\t\tif (type === 'epicshop:navigate-call') {\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,7 @@
1
+ declare function getDirModifiedTime(dir: string, { forceFresh }?: {
2
+ forceFresh?: boolean;
3
+ }): Promise<number>;
4
+ export declare function modifiedMoreRecentlyThan(time: number, ...dirs: Array<string>): Promise<boolean>;
5
+ declare function queuedGetDirModifiedTime(...args: Parameters<typeof getDirModifiedTime>): Promise<number>;
6
+ export { queuedGetDirModifiedTime as getDirModifiedTime };
7
+ //# sourceMappingURL=modified-time.server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modified-time.server.d.ts","sourceRoot":"","sources":["../../src/modified-time.server.ts"],"names":[],"mappings":"AAMA,iBAAe,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,EAAE,UAAkB,EAAE,GAAE;IAAE,UAAU,CAAC,EAAE,OAAO,CAAA;CAAO,GACnD,OAAO,CAAC,MAAM,CAAC,CASjB;AA2CD,wBAAsB,wBAAwB,CAC7C,IAAI,EAAE,MAAM,EACZ,GAAG,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,oBAStB;AAgBD,iBAAe,wBAAwB,CACtC,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,mBAK9C;AAED,OAAO,EAAE,wBAAwB,IAAI,kBAAkB,EAAE,CAAA"}
@@ -0,0 +1,80 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { isGitIgnored } from 'globby';
4
+ import PQueue from 'p-queue';
5
+ import { cachified, dirModifiedTimeCache } from './cache.server.js';
6
+ async function getDirModifiedTime(dir, { forceFresh = false } = {}) {
7
+ const result = await cachified({
8
+ key: dir,
9
+ cache: dirModifiedTimeCache,
10
+ ttl: 200,
11
+ forceFresh,
12
+ getFreshValue: () => getDirModifiedTimeImpl(dir),
13
+ });
14
+ return result;
15
+ }
16
+ async function getDirModifiedTimeImpl(dir) {
17
+ const isIgnored = await isGitIgnored({ cwd: dir });
18
+ const files = await fs.promises
19
+ .readdir(dir, { withFileTypes: true })
20
+ .catch(() => []);
21
+ const modifiedTimes = [];
22
+ for (const file of files) {
23
+ // Skip ignored files
24
+ if (isIgnored(file.name))
25
+ continue;
26
+ const filePath = path.join(dir, file.name);
27
+ if (file.isDirectory()) {
28
+ modifiedTimes.push(await getDirModifiedTime(filePath));
29
+ }
30
+ else {
31
+ try {
32
+ const { mtimeMs } = await fs.promises.stat(filePath);
33
+ modifiedTimes.push(mtimeMs);
34
+ }
35
+ catch {
36
+ // ignore errors (e.g., file access permissions, file has been moved or deleted)
37
+ }
38
+ }
39
+ }
40
+ try {
41
+ const { mtimeMs } = await fs.promises.stat(dir);
42
+ modifiedTimes.push(mtimeMs);
43
+ }
44
+ catch {
45
+ // ignore errors (e.g., file access permissions, file has been moved or deleted)
46
+ }
47
+ return Math.max(-1, ...modifiedTimes);
48
+ }
49
+ // this will return true as soon as one of the directories has been found to
50
+ // have been modified more recently than the given time
51
+ // TODO: this could be improved by not waiting for entire directories to be
52
+ // scanned and instead stopping the scan as soon as we find a file that was
53
+ // modified more recently than the given time
54
+ export async function modifiedMoreRecentlyThan(time, ...dirs) {
55
+ const modifiedTimePromises = dirs.map((dir) => getDirModifiedTime(dir));
56
+ const allFinishedPromise = Promise.all(modifiedTimePromises);
57
+ const firstMoreRecentPromise = modifiedTimePromises.map((p) => p.then((t) => (t > time ? true : allFinishedPromise.then(() => false))));
58
+ const firstMoreRecent = await Promise.race(firstMoreRecentPromise);
59
+ return firstMoreRecent;
60
+ }
61
+ let _queue = null;
62
+ function getQueue() {
63
+ if (_queue)
64
+ return _queue;
65
+ _queue = new PQueue({
66
+ concurrency: 10,
67
+ throwOnTimeout: true,
68
+ timeout: 1000 * 60,
69
+ });
70
+ return _queue;
71
+ }
72
+ // We have to use a queue because we can't run more than one of these at a time
73
+ // or we'll hit an out of memory error because esbuild uses a lot of memory...
74
+ async function queuedGetDirModifiedTime(...args) {
75
+ const queue = getQueue();
76
+ const result = await queue.add(() => getDirModifiedTime(...args));
77
+ return result || -1;
78
+ }
79
+ export { queuedGetDirModifiedTime as getDirModifiedTime };
80
+ //# sourceMappingURL=modified-time.server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modified-time.server.js","sourceRoot":"","sources":["../../src/modified-time.server.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'node:fs'\nimport path from 'node:path'\nimport { isGitIgnored } from 'globby'\nimport PQueue from 'p-queue'\nimport { cachified, dirModifiedTimeCache } from './cache.server.js'\n\nasync function getDirModifiedTime(\n\tdir: string,\n\t{ forceFresh = false }: { forceFresh?: boolean } = {},\n): Promise<number> {\n\tconst result = await cachified({\n\t\tkey: dir,\n\t\tcache: dirModifiedTimeCache,\n\t\tttl: 200,\n\t\tforceFresh,\n\t\tgetFreshValue: () => getDirModifiedTimeImpl(dir),\n\t})\n\treturn result\n}\n\nasync function getDirModifiedTimeImpl(dir: string): Promise<number> {\n\tconst isIgnored = await isGitIgnored({ cwd: dir })\n\tconst files = await fs.promises\n\t\t.readdir(dir, { withFileTypes: true })\n\t\t.catch(() => [])\n\n\tconst modifiedTimes: Array<number> = []\n\n\tfor (const file of files) {\n\t\t// Skip ignored files\n\t\tif (isIgnored(file.name)) continue\n\n\t\tconst filePath = path.join(dir, file.name)\n\n\t\tif (file.isDirectory()) {\n\t\t\tmodifiedTimes.push(await getDirModifiedTime(filePath))\n\t\t} else {\n\t\t\ttry {\n\t\t\t\tconst { mtimeMs } = await fs.promises.stat(filePath)\n\t\t\t\tmodifiedTimes.push(mtimeMs)\n\t\t\t} catch {\n\t\t\t\t// ignore errors (e.g., file access permissions, file has been moved or deleted)\n\t\t\t}\n\t\t}\n\t}\n\n\ttry {\n\t\tconst { mtimeMs } = await fs.promises.stat(dir)\n\t\tmodifiedTimes.push(mtimeMs)\n\t} catch {\n\t\t// ignore errors (e.g., file access permissions, file has been moved or deleted)\n\t}\n\n\treturn Math.max(-1, ...modifiedTimes)\n}\n\n// this will return true as soon as one of the directories has been found to\n// have been modified more recently than the given time\n// TODO: this could be improved by not waiting for entire directories to be\n// scanned and instead stopping the scan as soon as we find a file that was\n// modified more recently than the given time\nexport async function modifiedMoreRecentlyThan(\n\ttime: number,\n\t...dirs: Array<string>\n) {\n\tconst modifiedTimePromises = dirs.map((dir) => getDirModifiedTime(dir))\n\tconst allFinishedPromise = Promise.all(modifiedTimePromises)\n\tconst firstMoreRecentPromise = modifiedTimePromises.map((p) =>\n\t\tp.then((t) => (t > time ? true : allFinishedPromise.then(() => false))),\n\t)\n\tconst firstMoreRecent = await Promise.race(firstMoreRecentPromise)\n\treturn firstMoreRecent\n}\n\nlet _queue: PQueue | null = null\nfunction getQueue() {\n\tif (_queue) return _queue\n\n\t_queue = new PQueue({\n\t\tconcurrency: 10,\n\t\tthrowOnTimeout: true,\n\t\ttimeout: 1000 * 60,\n\t})\n\treturn _queue\n}\n\n// We have to use a queue because we can't run more than one of these at a time\n// or we'll hit an out of memory error because esbuild uses a lot of memory...\nasync function queuedGetDirModifiedTime(\n\t...args: Parameters<typeof getDirModifiedTime>\n) {\n\tconst queue = getQueue()\n\tconst result = await queue.add(() => getDirModifiedTime(...args))\n\treturn result || -1\n}\n\nexport { queuedGetDirModifiedTime as getDirModifiedTime }\n"]}
@@ -0,0 +1,56 @@
1
+ import { z } from 'zod';
2
+ declare const NotificationSchema: z.ZodObject<{
3
+ id: z.ZodString;
4
+ title: z.ZodString;
5
+ message: z.ZodString;
6
+ link: z.ZodOptional<z.ZodString>;
7
+ type: z.ZodEnum<["info", "warning", "danger"]>;
8
+ products: z.ZodOptional<z.ZodArray<z.ZodObject<{
9
+ host: z.ZodString;
10
+ slug: z.ZodOptional<z.ZodString>;
11
+ }, "strip", z.ZodTypeAny, {
12
+ host: string;
13
+ slug?: string | undefined;
14
+ }, {
15
+ host: string;
16
+ slug?: string | undefined;
17
+ }>, "many">>;
18
+ expiresAt: z.ZodEffects<z.ZodNullable<z.ZodString>, Date | null, string | null>;
19
+ }, "strip", z.ZodTypeAny, {
20
+ title: string;
21
+ message: string;
22
+ type: "info" | "warning" | "danger";
23
+ id: string;
24
+ expiresAt: Date | null;
25
+ link?: string | undefined;
26
+ products?: {
27
+ host: string;
28
+ slug?: string | undefined;
29
+ }[] | undefined;
30
+ }, {
31
+ title: string;
32
+ message: string;
33
+ type: "info" | "warning" | "danger";
34
+ id: string;
35
+ expiresAt: string | null;
36
+ link?: string | undefined;
37
+ products?: {
38
+ host: string;
39
+ slug?: string | undefined;
40
+ }[] | undefined;
41
+ }>;
42
+ export type Notification = z.infer<typeof NotificationSchema>;
43
+ export declare function getUnmutedNotifications(): Promise<{
44
+ title: string;
45
+ message: string;
46
+ type: "info" | "warning" | "danger";
47
+ id: string;
48
+ expiresAt: Date | null;
49
+ link?: string | undefined;
50
+ products?: {
51
+ host: string;
52
+ slug?: string | undefined;
53
+ }[] | undefined;
54
+ }[]>;
55
+ export {};
56
+ //# sourceMappingURL=notifications.server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.server.d.ts","sourceRoot":"","sources":["../../src/notifications.server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAKvB,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkBtB,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAsB7D,wBAAsB,uBAAuB;;;;;;;;;;;KAgC5C"}
@@ -0,0 +1,65 @@
1
+ import json5 from 'json5';
2
+ import { z } from 'zod';
3
+ import { cachified, notificationsCache } from './cache.server.js';
4
+ import { getWorkshopConfig } from './config.server.js';
5
+ import { getMutedNotifications } from './db.server.js';
6
+ const NotificationSchema = z.object({
7
+ id: z.string(),
8
+ title: z.string(),
9
+ message: z.string(),
10
+ link: z.string().optional(),
11
+ type: z.enum(['info', 'warning', 'danger']),
12
+ products: z
13
+ .array(z.object({
14
+ host: z.string(),
15
+ slug: z.string().optional(),
16
+ }))
17
+ .optional(),
18
+ expiresAt: z
19
+ .string()
20
+ .nullable()
21
+ .transform((val) => (val ? new Date(val) : null)),
22
+ });
23
+ async function getRemoteNotifications() {
24
+ return cachified({
25
+ key: 'notifications',
26
+ cache: notificationsCache,
27
+ ttl: 1000 * 60 * 60 * 6,
28
+ swr: 1000 * 60 * 60 * 24,
29
+ offlineFallbackValue: [],
30
+ async getFreshValue() {
31
+ const URL = 'https://gist.github.com/kentcdodds/c3aaa5141f591cdbb0e6bfcacd361f39';
32
+ const filename = 'notifications.json5';
33
+ const response = await fetch(`${URL}/raw/${filename}`);
34
+ const text = await response.text();
35
+ const json = json5.parse(text);
36
+ return NotificationSchema.array().parse(json);
37
+ },
38
+ }).catch(() => []);
39
+ }
40
+ export async function getUnmutedNotifications() {
41
+ if (ENV.EPICSHOP_DEPLOYED)
42
+ return [];
43
+ const remoteNotifications = await getRemoteNotifications();
44
+ const config = getWorkshopConfig();
45
+ const notificationsToShow = remoteNotifications
46
+ .filter((n) => {
47
+ if (n.expiresAt && n.expiresAt < new Date()) {
48
+ return false;
49
+ }
50
+ return true;
51
+ })
52
+ .filter((n) => {
53
+ if (!n.products)
54
+ return true;
55
+ return n.products.some((p) => {
56
+ return (p.host === config.product.host &&
57
+ (p.slug ? p.slug === config.product.slug : true));
58
+ });
59
+ })
60
+ .concat(config.notifications);
61
+ const muted = await getMutedNotifications();
62
+ const visibleNotifications = notificationsToShow.filter((n) => !muted.includes(n.id));
63
+ return visibleNotifications;
64
+ }
65
+ //# sourceMappingURL=notifications.server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.server.js","sourceRoot":"","sources":["../../src/notifications.server.ts"],"names":[],"mappings":"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","sourcesContent":["import json5 from 'json5'\nimport { z } from 'zod'\nimport { cachified, notificationsCache } from './cache.server.js'\nimport { getWorkshopConfig } from './config.server.js'\nimport { getMutedNotifications } from './db.server.js'\n\nconst NotificationSchema = z.object({\n\tid: z.string(),\n\ttitle: z.string(),\n\tmessage: z.string(),\n\tlink: z.string().optional(),\n\ttype: z.enum(['info', 'warning', 'danger']),\n\tproducts: z\n\t\t.array(\n\t\t\tz.object({\n\t\t\t\thost: z.string(),\n\t\t\t\tslug: z.string().optional(),\n\t\t\t}),\n\t\t)\n\t\t.optional(),\n\texpiresAt: z\n\t\t.string()\n\t\t.nullable()\n\t\t.transform((val) => (val ? new Date(val) : null)),\n})\n\nexport type Notification = z.infer<typeof NotificationSchema>\n\nasync function getRemoteNotifications() {\n\treturn cachified({\n\t\tkey: 'notifications',\n\t\tcache: notificationsCache,\n\t\tttl: 1000 * 60 * 60 * 6,\n\t\tswr: 1000 * 60 * 60 * 24,\n\t\tofflineFallbackValue: [],\n\t\tasync getFreshValue() {\n\t\t\tconst URL =\n\t\t\t\t'https://gist.github.com/kentcdodds/c3aaa5141f591cdbb0e6bfcacd361f39'\n\t\t\tconst filename = 'notifications.json5'\n\t\t\tconst response = await fetch(`${URL}/raw/${filename}`)\n\t\t\tconst text = await response.text()\n\t\t\tconst json = json5.parse(text)\n\n\t\t\treturn NotificationSchema.array().parse(json)\n\t\t},\n\t}).catch(() => [])\n}\n\nexport async function getUnmutedNotifications() {\n\tif (ENV.EPICSHOP_DEPLOYED) return []\n\n\tconst remoteNotifications = await getRemoteNotifications()\n\n\tconst config = getWorkshopConfig()\n\n\tconst notificationsToShow = remoteNotifications\n\t\t.filter((n) => {\n\t\t\tif (n.expiresAt && n.expiresAt < new Date()) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn true\n\t\t})\n\t\t.filter((n) => {\n\t\t\tif (!n.products) return true\n\t\t\treturn n.products.some((p) => {\n\t\t\t\treturn (\n\t\t\t\t\tp.host === config.product.host &&\n\t\t\t\t\t(p.slug ? p.slug === config.product.slug : true)\n\t\t\t\t)\n\t\t\t})\n\t\t})\n\t\t.concat(config.notifications)\n\n\tconst muted = await getMutedNotifications()\n\n\tconst visibleNotifications = notificationsToShow.filter(\n\t\t(n) => !muted.includes(n.id),\n\t)\n\n\treturn visibleNotifications\n}\n"]}
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,6 @@
1
+ export declare function getInBrowserTestPages(): Promise<{
2
+ path: string;
3
+ testFile: string;
4
+ }[]>;
5
+ export declare function setupInBrowserTests(): void;
6
+ //# sourceMappingURL=playwright.server.d.ts.map
@@ -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,SA6DlC"}
@@ -0,0 +1,95 @@
1
+ import { expect, test } from '@playwright/test';
2
+ import crossSpawn from 'cross-spawn';
3
+ import z from 'zod';
4
+ import { getApps, isSolutionApp } from './apps.server.js';
5
+ export async function getInBrowserTestPages() {
6
+ const apps = (await getApps())
7
+ .filter(isSolutionApp)
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)))).catch(e => {console.error(e);throw e;})`;
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
+ test.describe.parallel('in-browser tests', () => {
53
+ for (const testPage of testPages) {
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
+ }
95
+ //# sourceMappingURL=playwright.server.js.map