-
Notifications
You must be signed in to change notification settings - Fork 274
/
Copy pathboot-playground-remote.ts
226 lines (204 loc) · 6.68 KB
/
boot-playground-remote.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
import {
LatestSupportedPHPVersion,
MessageListener,
} from '@php-wasm/universal';
import {
registerServiceWorker,
spawnPHPWorkerThread,
exposeAPI,
consumeAPI,
} from '@php-wasm/web';
import type { PlaygroundWorkerEndpoint } from './worker-thread';
import type { WebClientMixin } from './playground-client';
import ProgressBar, { ProgressBarOptions } from './progress-bar';
// Avoid literal "import.meta.url" on purpose as vite would attempt
// to resolve it during build time. This should specifically be
// resolved by the browser at runtime to reflect the current origin.
const origin = new URL('/', (import.meta || {}).url).origin;
// @ts-ignore
import moduleWorkerUrl from './worker-thread?worker&url';
export const workerUrl: string = new URL(moduleWorkerUrl, origin) + '';
// @ts-ignore
import serviceWorkerPath from '../../service-worker.ts?worker&url';
import { LatestSupportedWordPressVersion } from './get-wordpress-module';
export const serviceWorkerUrl = new URL(serviceWorkerPath, origin);
// Prevent Vite from hot-reloading this file – it would
// cause bootPlaygroundRemote() to register another web worker
// without unregistering the previous one. The first web worker
// would then fight for service worker requests with the second
// one. It's a difficult problem to debug and HMR isn't that useful
// here anyway – let's just disable it for this file.
// @ts-ignore
if (import.meta.hot) {
// @ts-ignore
import.meta.hot.accept(() => {});
}
const query = new URL(document.location.href).searchParams;
export async function bootPlaygroundRemote() {
assertNotInfiniteLoadingLoop();
const hasProgressBar = query.has('progressbar');
let bar: ProgressBar | undefined;
if (hasProgressBar) {
bar = new ProgressBar();
document.body.prepend(bar.element);
}
const wpVersion = parseVersion(
query.get('wp'),
LatestSupportedWordPressVersion
);
const phpVersion = parseVersion(
query.get('php'),
LatestSupportedPHPVersion
);
const workerApi = consumeAPI<PlaygroundWorkerEndpoint>(
await spawnPHPWorkerThread(workerUrl, {
wpVersion,
phpVersion,
persistent: query.has('persistent') ? 'true' : 'false',
})
);
const wpFrame = document.querySelector('#wp') as HTMLIFrameElement;
const webApi: WebClientMixin = {
async onDownloadProgress(fn) {
return workerApi.onDownloadProgress(fn);
},
async setProgress(options: ProgressBarOptions) {
if (!bar) {
throw new Error('Progress bar not available');
}
bar.setOptions(options);
},
async setLoaded() {
if (!bar) {
throw new Error('Progress bar not available');
}
bar.destroy();
},
async onNavigation(fn) {
// Manage the address bar
wpFrame.addEventListener('load', async (e: any) => {
try {
const path = await playground.internalUrlToPath(
e.currentTarget!.contentWindow.location.href
);
fn(path);
} catch (e) {
// @TODO: The above call can fail if the remote iframe
// is embedded in StackBlitz, or presumably, any other
// environment with restrictive CSP. Any error thrown
// due to CORS-related stuff crashes the entire remote
// so let's ignore it for now and find a correct fix in time.
}
});
},
async goTo(requestedPath: string) {
/**
* People often forget to type the trailing slash at the end of
* /wp-admin/ URL and end up with wrong relative hrefs. Let's
* fix it for them.
*/
if (requestedPath === '/wp-admin') {
requestedPath = '/wp-admin/';
}
wpFrame.src = await playground.pathToInternalUrl(requestedPath);
},
async getCurrentURL() {
return await playground.internalUrlToPath(wpFrame.src);
},
async setIframeSandboxFlags(flags: string[]) {
wpFrame.setAttribute('sandbox', flags.join(' '));
},
/**
* This function is merely here to explicitly call workerApi.onMessage.
* Comlink should be able to handle that on its own, but something goes
* wrong and if this function is not here, we see the following error:
*
* Error: Failed to execute 'postMessage' on 'Worker': function() {
* } could not be cloned.
*
* In the future, this explicit declaration shouldn't be needed.
*
* @param callback
* @returns
*/
async onMessage(callback: MessageListener) {
return await workerApi.onMessage(callback);
},
};
await workerApi.isConnected();
// If onDownloadProgress is not explicitly re-exposed here,
// Comlink will throw an error and claim the callback
// cannot be cloned. Adding a transfer handler for functions
// doesn't help:
// https://github.com/GoogleChromeLabs/comlink/issues/426#issuecomment-578401454
// @TODO: Handle the callback conversion automatically and don't explicitly re-expose
// the onDownloadProgress method
const [setAPIReady, playground] = exposeAPI(webApi, workerApi);
await workerApi.isReady();
await registerServiceWorker(
workerApi,
await workerApi.scope,
serviceWorkerUrl + ''
);
wpFrame.src = await playground.pathToInternalUrl('/');
setupPostMessageRelay(wpFrame, getOrigin(await playground.absoluteUrl));
setAPIReady();
/*
* An asssertion to make sure Playground Client is compatible
* with Remote<PlaygroundClient>
*/
return playground;
}
function getOrigin(url: string) {
return new URL(url, 'https://example.com').origin;
}
function setupPostMessageRelay(
wpFrame: HTMLIFrameElement,
expectedOrigin: string
) {
window.addEventListener('message', (event) => {
if (event.source !== wpFrame.contentWindow) {
return;
}
if (event.origin !== expectedOrigin) {
return;
}
if (typeof event.data !== 'object' || event.data.type !== 'relay') {
return;
}
window.parent.postMessage(event.data, '*');
});
}
function parseVersion<T>(value: string | undefined | null, latest: T) {
if (!value || value === 'latest') {
return (latest as string).replace('.', '_');
}
/*
* Vite doesn't deal well with the dot in the parameters name,
* passed to the worker via a query string, so we replace
* it with an underscore
*/
return value.replace('.', '_');
}
/**
* When the service worker fails for any reason, the page displayed inside
* the iframe won't be a WordPress instance we expect from the service worker.
* Instead, it will be the original page trying to load the service worker. This
* causes an infinite loop with a loader inside a loader inside a loader.
*/
function assertNotInfiniteLoadingLoop() {
let isBrowserInABrowser = false;
try {
isBrowserInABrowser =
window.parent !== window &&
(window as any).parent.IS_WASM_WORDPRESS;
} catch (e) {
// ignore
}
if (isBrowserInABrowser) {
throw new Error(
'The service worker did not load correctly. This is a bug, please report it on https://github.com/WordPress/wordpress-playground/issues'
);
}
(window as any).IS_WASM_WORDPRESS = true;
}