Skip to content

Commit 8c8baf5

Browse files
authored
Merge pull request #7947 from QwikDev/fix-frontpage
Fix-frontpage
2 parents 3211b0e + 103db43 commit 8c8baf5

File tree

8 files changed

+116
-84
lines changed

8 files changed

+116
-84
lines changed

packages/docs/src/repl/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ This README gives a high-level overview of how the REPL is structured, what each
1111
The REPL is a browser-first system composed of three main parts:
1212

1313
- Bundler subsystem (web-worker + helpers): transforms user code, resolves imports, bundles modules, and prepares runnable artifacts.
14-
- Service worker: Intercepts requests to `/repl/[id]/*` and messages the bundler subsystem (via BroadcastChannel) to get responses. This allows the REPL to work without a server.
14+
- Service worker: Intercepts requests to `/repl/` and messages the bundler subsystem (via BroadcastChannel) to get responses. This allows the REPL to work without a server.
1515
- UI (React/Qwik components + Monaco): editor, panels, console, options and controls that let users edit code and view outputs.
1616

1717
The codebase keeps bundling and heavy work off the main thread by using a web worker script per Qwik version.
@@ -37,12 +37,12 @@ The codebase keeps bundling and heavy work off the main thread by using a web wo
3737
1. The UI component (`ui/editor.tsx` + `ui/monaco.tsx`) presents a code editor and controls.
3838
2. When code is executed, the bundler subsystem is invoked. The UI posts a message to the bundler worker.
3939
3. The bundler worker (`bundler-worker.ts`) makes the client and SSR bundles, and executes `render()` from the `entry.ssr` to get the SSR result HTML.
40-
4. The UI also shows an iframe, loading `/repl/[id]/`.
40+
4. The UI also shows an iframe, loading `/repl/client/[id]/`.
4141
5. The service worker intercepts all requests and uses messages to get the bundle + html files from the worker.
4242

4343
### Flow diagram
4444

45-
- The iframe requests `/repl/[id]/`
45+
- The iframe requests `/repl/client/[id]/`
4646
- The service worker intercepts and sends a message to the REPL instance
4747
- The REPL instance messages the bundler to bundle the user code
4848
- The bundler messages the REPL instance with the result
@@ -52,7 +52,7 @@ The codebase keeps bundling and heavy work off the main thread by using a web wo
5252

5353
```mermaid
5454
flowchart TD
55-
Iframe["Iframe - /repl/[replId]/"]
55+
Iframe["Iframe - /repl/client/[replId]/"]
5656
ServiceWorker["Service Worker (/repl/*)"]
5757
ReplInstance["REPL instance"]
5858
BundlerWorker["Bundler Worker"]

packages/docs/src/repl/bundler/bundler-worker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ async function performBundle(message: BundleMessage): Promise<ReplResult> {
125125

126126
let start = performance.now();
127127

128-
const baseUrl = `/repl/${replId}/`;
128+
const baseUrl = `/repl/client/${replId}/`;
129129
const defines = {
130130
'import.meta.env.BASE_URL': JSON.stringify(baseUrl),
131131
'import.meta.env': JSON.stringify({}),

packages/docs/src/repl/bundler/index.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,7 @@
33
import type { ReplInputOptions, ReplResult } from '../types';
44
import { getDeps } from './bundled';
55
import type { InitMessage, BundleMessage, OutgoingMessage } from './bundler-worker';
6-
7-
import ssrWorkerStringPre from './repl-ssr-worker?compiled-string';
8-
import listenerScript from './client-events-listener?compiled-string';
9-
10-
export const ssrWorkerString = ssrWorkerStringPre
11-
.replace(/globalThis\.DO_NOT_TOUCH_IMPORT/g, 'import')
12-
.replace('globalThis.LISTENER_SCRIPT', JSON.stringify(listenerScript));
6+
import bundlerWorkerUrl from './bundler-worker?worker&url';
137

148
const bundlers = new Map<string, Bundler>();
159

@@ -32,7 +26,8 @@ class Bundler {
3226

3327
initWorker() {
3428
this.initP = new Promise<void>((res) => (this.ready = res));
35-
this.worker = new Worker(new URL('./bundler-worker', import.meta.url), { type: 'module' });
29+
// Start from /repl so repl-sw can add COEP headers
30+
this.worker = new Worker(`/repl${bundlerWorkerUrl}`, { type: 'module' });
3631
this.worker.addEventListener('message', this.messageHandler);
3732
this.worker.addEventListener('error', (e: ErrorEvent) => {
3833
console.error(`Bundler worker for ${this.version} failed`, e.message);

packages/docs/src/repl/bundler/repl-ssr-worker.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,17 @@ self.onmessage = async (e: MessageEvent<IncomingMessage>) => {
6767
}
6868
};
6969

70+
// Workaround so vite doesn't try to process this import
71+
const importFrom = (url: string) => {
72+
return import(/*@vite-ignore*/ url);
73+
};
74+
7075
async function executeSSR(message: InitSSRMessage): Promise<{ html: string; events: any[] }> {
7176
const { baseUrl, manifest, entry } = message;
7277
const start = performance.now();
7378

7479
// We prevent Vite from touching this import() and replace it after bundling
75-
const module = await (globalThis as any).DO_NOT_TOUCH_IMPORT(`/repl/${replId}-ssr/${entry}`);
80+
const module = await importFrom(`/repl/ssr/${replId}/${entry}`);
7681
const server = module.default;
7782

7883
const render = typeof server === 'function' ? server : server?.render;
@@ -106,12 +111,6 @@ async function executeSSR(message: InitSSRMessage): Promise<{ html: string; even
106111
prefetchStrategy: null,
107112
});
108113

109-
// Inject the event listener script
110-
ssrResult.html = ssrResult.html.replace(
111-
'</body>',
112-
`<script>${(globalThis as any).LISTENER_SCRIPT}</script></body>`
113-
);
114-
115114
events.push({
116115
kind: 'console-log',
117116
scope: 'build',

packages/docs/src/repl/repl-instance.ts

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** Maintains the state for a REPL instance */
22

33
import { isServer, unwrapStore } from '@builder.io/qwik';
4-
import { getBundler, ssrWorkerString } from './bundler';
4+
import { getBundler } from './bundler';
55
import { registerReplSW } from './register-repl-sw';
66
import type { RequestMessage, ResponseMessage } from './repl-sw';
77
import type { ReplAppInput, ReplResult, ReplStore } from './types';
@@ -10,6 +10,8 @@ import type {
1010
InitSSRMessage,
1111
OutgoingMessage as SSROutgoingMessage,
1212
} from './bundler/repl-ssr-worker';
13+
import ssrWorkerUrl from './bundler/repl-ssr-worker?worker&url';
14+
import listenerScript from './bundler/client-events-listener?compiled-string';
1315

1416
let channel: BroadcastChannel;
1517
let registered = false;
@@ -172,34 +174,37 @@ export class ReplInstance {
172174
};
173175

174176
getContentType = (url: string): string => {
175-
if (url.endsWith('.js')) {
176-
return 'application/javascript';
177-
}
178-
if (url.endsWith('.css')) {
179-
return 'text/css';
180-
}
181-
if (url.endsWith('.json')) {
182-
return 'application/json';
183-
}
184-
if (url.endsWith('.html') || url.endsWith('/')) {
177+
const noQuery = url.split('?')[0];
178+
if (noQuery.endsWith('/')) {
185179
return 'text/html';
186180
}
181+
const ext = noQuery.split('.').pop()?.toLowerCase();
182+
if (ext) {
183+
switch (ext) {
184+
case 'js':
185+
case 'mjs':
186+
case 'cjs':
187+
return 'application/javascript';
188+
case 'json':
189+
return 'application/json';
190+
case 'css':
191+
return 'text/css';
192+
case 'html':
193+
case 'htm':
194+
return 'text/html';
195+
case 'svg':
196+
return 'image/svg+xml';
197+
}
198+
}
187199
return 'text/plain';
188200
};
189201

190202
async getFile(path: string): Promise<string | null> {
191-
const match = path.match(/\/repl\/([a-z0-9]+)(-ssr)?\/(.*)/);
203+
const match = path.match(/\/repl\/(client|ssr)\/([a-z0-9]+)\/(.*)/);
192204
if (!match) {
193205
throw new Error(`Invalid REPL path ${path}`);
194206
}
195-
const [, , ssrFlag, filePath] = match;
196-
/**
197-
* We must serve the SSR worker string from /repl/ so that the import() inside the worker can be
198-
* intercepted by our repl-sw.js
199-
*/
200-
if (ssrFlag && filePath === 'repl-ssr-worker.js') {
201-
return ssrWorkerString;
202-
}
207+
const [, target, , filePath] = match;
203208

204209
const ssrPromise = this.ensureBuilt();
205210
// First wait only for the bundles
@@ -209,7 +214,7 @@ export class ReplInstance {
209214
}
210215

211216
// Serve SSR modules at /server/* path
212-
if (ssrFlag) {
217+
if (target === 'ssr') {
213218
// vite adds ?import to some imports, remove it for matching
214219
const serverPath = filePath.replace(/\?import$/, '');
215220
for (const module of this.lastResult.ssrModules) {
@@ -230,7 +235,11 @@ export class ReplInstance {
230235
if (filePath === 'index.html' || filePath === '') {
231236
// Here, also wait for SSR
232237
await ssrPromise.catch(() => {});
233-
return this.lastResult.html || errorHtml('No HTML generated', 'REPL');
238+
if (this.lastResult.html) {
239+
// Inject the event listener script
240+
return this.lastResult.html + `<script>${listenerScript}</script>`;
241+
}
242+
return errorHtml('No HTML generated', 'REPL');
234243
}
235244

236245
return null;
@@ -247,13 +256,8 @@ export class ReplInstance {
247256
return resolve({ html: errorHtml('No SSR module found', 'SSR') });
248257
}
249258

250-
/**
251-
* We could also serve it from /repl/ directly but then we need to special-case the repl-sw
252-
* and then docs.dev mode wouldn't reload the worker
253-
*/
254-
const ssrWorker = new Worker(`/repl/${this.replId}-ssr/repl-ssr-worker.js`, {
255-
type: 'module',
256-
});
259+
// Start from /repl so repl-sw can intercept the requests
260+
const ssrWorker = new Worker(`/repl${ssrWorkerUrl}`, { type: 'module' });
257261

258262
ssrWorker.onmessage = (e: MessageEvent<SSROutgoingMessage>) => {
259263
const { type } = e.data;
@@ -263,7 +267,7 @@ export class ReplInstance {
263267
type: 'run-ssr',
264268
replId: this.replId,
265269
entry: entryModule.path,
266-
baseUrl: `/repl/${this.replId}/build/`,
270+
baseUrl: `/repl/client/${this.replId}/build/`,
267271
manifest: result.manifest,
268272
};
269273
ssrWorker.postMessage(initMessage);

packages/docs/src/repl/repl-sw.ts

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
/** Simple proxy, proxies requests for /repl/[id]/* to the main thread */
1+
/**
2+
* Simple proxy, proxies requests for /repl/* to the main thread
3+
*
4+
* - /repl/client/[id]/* => client-side only requests
5+
* - /repl/ssr/[id]/* => ssr scripts
6+
* - /repl/* => proxy to / with COEP headers
7+
*
8+
* This allows the REPL to load scripts from the same origin, which is required when using `COEP:
9+
* require-corp`, and it also allows us to still use vite for worker bundling.
10+
*/
211

312
const channel = new BroadcastChannel('qwik-docs-repl');
413

@@ -28,37 +37,62 @@ let nextId = 1;
2837
// Only GET requests
2938
if (ev.request.method === 'GET') {
3039
const reqUrl = new URL(ev.request.url);
31-
const pathname = reqUrl.pathname;
32-
const match = pathname.match(/^\/repl\/([a-z0-9]+)(-ssr)?\//);
33-
// Only paths that look like a REPL id
34-
if (match) {
35-
const replId = match[1];
36-
ev.respondWith(
37-
new Promise((resolve) => {
38-
const requestId = nextId++;
40+
const origin = self.location.origin;
41+
if (reqUrl.origin === origin) {
42+
const pathname = reqUrl.pathname;
43+
const match = pathname.match(/^\/repl\/(client|ssr)\/([a-z0-9]+)\//);
44+
// Only paths that look like a REPL id
45+
if (match) {
46+
const replId = match[2];
47+
ev.respondWith(
48+
new Promise((resolve) => {
49+
const requestId = nextId++;
3950

40-
const timeoutId = setTimeout(() => {
41-
if (pendingRequests.has(requestId)) {
42-
pendingRequests.delete(requestId);
43-
resolve(new Response('504 - Request timeout - try reloading', { status: 504 }));
44-
}
45-
}, 10000);
51+
const timeoutId = setTimeout(() => {
52+
if (pendingRequests.has(requestId)) {
53+
pendingRequests.delete(requestId);
54+
resolve(new Response('504 - Request timeout - try reloading', { status: 504 }));
55+
}
56+
}, 10000);
4657

47-
pendingRequests.set(requestId, { resolve, timeoutId });
58+
pendingRequests.set(requestId, { resolve, timeoutId });
4859

49-
// Send request to main thread
50-
channel.postMessage({
51-
type: 'repl-request',
52-
requestId,
53-
replId,
54-
url: pathname,
55-
// useful later when adding Qwik Router support
56-
// method: ev.request.method,
57-
// headers: Object.fromEntries(ev.request.headers.entries()),
58-
});
59-
})
60-
);
61-
return;
60+
// Send request to main thread
61+
channel.postMessage({
62+
type: 'repl-request',
63+
requestId,
64+
replId,
65+
url: pathname + reqUrl.search,
66+
// useful later when adding Qwik Router support
67+
// method: ev.request.method,
68+
// headers: Object.fromEntries(ev.request.headers.entries()),
69+
});
70+
})
71+
);
72+
return;
73+
} else {
74+
// Proxy other requests to / and return COEP headers
75+
const url = pathname.replace(/^\/repl\//, '/') + reqUrl.search;
76+
const req = new Request(url, {
77+
method: ev.request.method,
78+
headers: ev.request.headers,
79+
redirect: 'manual',
80+
});
81+
ev.respondWith(
82+
fetch(req).then((res) => {
83+
// Create a new response so we can modify headers
84+
const newHeaders = new Headers(res.headers);
85+
newHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp');
86+
newHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');
87+
return new Response(res.body, {
88+
status: res.status,
89+
statusText: res.statusText,
90+
headers: newHeaders,
91+
});
92+
})
93+
);
94+
return;
95+
}
6296
}
6397
}
6498

packages/docs/src/repl/ui/repl-output-panel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export const ReplOutputPanel = component$(({ input, store }: ReplOutputPanelProp
103103
<iframe
104104
key={store.reload}
105105
class="repl-server"
106-
src={`/repl/${store.replId}/`}
106+
src={`/repl/client/${store.replId}/`}
107107
sandbox="allow-popups allow-modals allow-scripts allow-same-origin"
108108
/>
109109
)}

0 commit comments

Comments
 (0)