tangled
alpha
login
or
join now
danabra.mov
/
rscexplorer
37
fork
atom
A tool for people curious about the React Server Components protocol
rscexplorer.dev/
rsc
react
37
fork
atom
overview
issues
pulls
pipelines
improve ux
danabra.mov
2 months ago
43aa9736
1e9eaa1a
+312
-130
13 changed files
expand all
collapse all
unified
split
src
client
embed.tsx
index.tsx
ui
App.css
App.tsx
CodeEditor.css
CodeEditor.tsx
EmbedApp.tsx
FlightLog.tsx
LivePreview.tsx
Workspace.tsx
codemirror.ts
embed.ts
vite.config.js
+1
-1
src/client/embed.tsx
···
5
5
import { EmbedApp } from "./ui/EmbedApp.tsx";
6
6
7
7
const container = document.getElementById("embed-root")!;
8
8
-
const root = createRoot(container);
8
8
+
const root = createRoot(container!);
9
9
root.render(<EmbedApp />);
+3
-8
src/client/index.tsx
···
4
4
import { createRoot } from "react-dom/client";
5
5
import { App } from "./ui/App.tsx";
6
6
7
7
-
document.addEventListener("DOMContentLoaded", () => {
8
8
-
const container = document.getElementById("app");
9
9
-
if (!container) {
10
10
-
throw new Error("Could not find #app element");
11
11
-
}
12
12
-
const root = createRoot(container);
13
13
-
root.render(<App />);
14
14
-
});
7
7
+
const container = document.getElementById("app");
8
8
+
const root = createRoot(container!);
9
9
+
root.render(<App />);
+5
src/client/ui/App.css
···
197
197
border-color: #555;
198
198
}
199
199
200
200
+
.App-embedModal-textarea::selection {
201
201
+
background: rgba(255, 213, 79, 0.3);
202
202
+
color: var(--text-bright);
203
203
+
}
204
204
+
200
205
.App-embedModal-tabs {
201
206
display: inline-flex;
202
207
background: var(--bg);
+39
-22
src/client/ui/App.tsx
···
1
1
-
import React, { useState, useEffect, type ChangeEvent, type MouseEvent } from "react";
1
1
+
import React, { useState, useEffect, useRef, type ChangeEvent, type MouseEvent } from "react";
2
2
import { version } from "react";
3
3
import { SAMPLES, type Sample } from "../samples.ts";
4
4
import REACT_VERSIONS from "../../../scripts/versions.json";
···
188
188
export function App(): React.ReactElement {
189
189
const [initialCode] = useState(getInitialCode);
190
190
const [currentSample, setCurrentSample] = useState<string | null>(initialCode.sampleKey);
191
191
-
const [workspaceCode, setWorkspaceCode] = useState<CodeState>({
191
191
+
const [liveCode, setLiveCode] = useState<CodeState>({
192
192
server: initialCode.server,
193
193
client: initialCode.client,
194
194
});
195
195
-
const [liveCode, setLiveCode] = useState<CodeState>(workspaceCode);
196
195
const [showEmbedModal, setShowEmbedModal] = useState(false);
196
196
+
const iframeRef = useRef<HTMLIFrameElement>(null);
197
197
198
198
-
// Listen for code changes from the embed iframe
198
198
+
const [initialEmbedUrl] = useState(
199
199
+
() => `embed.html?seamless=1&c=${encodeURIComponent(encodeCode(initialCode))}`,
200
200
+
);
201
201
+
199
202
useEffect(() => {
200
203
const handleMessage = (event: MessageEvent): void => {
201
204
const data = event.data as { type?: string; code?: CodeState };
···
203
206
setLiveCode(data.code);
204
207
}
205
208
};
206
206
-
207
209
window.addEventListener("message", handleMessage);
208
210
return () => window.removeEventListener("message", handleMessage);
209
211
}, []);
210
212
211
211
-
// Reset liveCode when workspaceCode changes (e.g., sample switch)
212
213
useEffect(() => {
213
213
-
setLiveCode(workspaceCode);
214
214
-
}, [workspaceCode]);
215
215
-
216
216
-
const embedUrl = `embed.html?c=${encodeURIComponent(encodeCode(workspaceCode))}`;
217
217
-
218
218
-
const handleSave = (): void => {
219
219
-
saveToUrl(liveCode.server, liveCode.client);
220
220
-
setCurrentSample(null);
221
221
-
};
222
222
-
223
223
-
const isDirty = currentSample
224
224
-
? liveCode.server !== (SAMPLES[currentSample] as Sample).server ||
225
225
-
liveCode.client !== (SAMPLES[currentSample] as Sample).client
226
226
-
: liveCode.server !== initialCode.server || liveCode.client !== initialCode.client;
214
214
+
const handlePopState = (): void => {
215
215
+
const newCode = getInitialCode();
216
216
+
setLiveCode({ server: newCode.server, client: newCode.client });
217
217
+
setCurrentSample(newCode.sampleKey);
218
218
+
iframeRef.current?.contentWindow?.postMessage(
219
219
+
{ type: "rscexplorer:reset", code: { server: newCode.server, client: newCode.client } },
220
220
+
"*",
221
221
+
);
222
222
+
};
223
223
+
window.addEventListener("popstate", handlePopState);
224
224
+
return () => window.removeEventListener("popstate", handlePopState);
225
225
+
}, []);
227
226
228
227
const handleSampleChange = (e: ChangeEvent<HTMLSelectElement>): void => {
229
228
const key = e.target.value;
···
233
232
server: sample.server,
234
233
client: sample.client,
235
234
};
236
236
-
setWorkspaceCode(newCode);
237
235
setCurrentSample(key);
236
236
+
setLiveCode(newCode);
237
237
+
iframeRef.current?.contentWindow?.postMessage(
238
238
+
{ type: "rscexplorer:reset", code: newCode },
239
239
+
"*",
240
240
+
);
238
241
const url = new URL(window.location.href);
239
242
url.searchParams.delete("c");
240
243
url.searchParams.set("s", key);
241
244
window.history.pushState({}, "", url);
242
245
}
243
246
};
247
247
+
248
248
+
const handleSave = (): void => {
249
249
+
saveToUrl(liveCode.server, liveCode.client);
250
250
+
setCurrentSample(null);
251
251
+
};
252
252
+
253
253
+
const isDirty = currentSample
254
254
+
? liveCode.server !== (SAMPLES[currentSample] as Sample).server ||
255
255
+
liveCode.client !== (SAMPLES[currentSample] as Sample).client
256
256
+
: liveCode.server !== initialCode.server || liveCode.client !== initialCode.client;
244
257
245
258
return (
246
259
<>
···
322
335
</div>
323
336
<BuildSwitcher />
324
337
</header>
325
325
-
<iframe key={embedUrl} src={embedUrl} style={{ flex: 1, border: "none", width: "100%" }} />
338
338
+
<iframe
339
339
+
ref={iframeRef}
340
340
+
src={initialEmbedUrl}
341
341
+
style={{ flex: 1, border: "none", width: "100%" }}
342
342
+
/>
326
343
{showEmbedModal && <EmbedModal code={liveCode} onClose={() => setShowEmbedModal(false)} />}
327
344
</>
328
345
);
+28
src/client/ui/CodeEditor.css
···
21
21
.CodeEditor .cm-editor .cm-scroller {
22
22
overflow: auto !important;
23
23
}
24
24
+
25
25
+
/* Fallback textarea styled to match CodeMirror */
26
26
+
.CodeEditor-fallback {
27
27
+
position: absolute;
28
28
+
top: 0;
29
29
+
left: 0;
30
30
+
right: 0;
31
31
+
bottom: 0;
32
32
+
width: 100%;
33
33
+
height: 100%;
34
34
+
padding: 16px 18px;
35
35
+
margin: 0;
36
36
+
border: none;
37
37
+
outline: none;
38
38
+
resize: none;
39
39
+
background: transparent;
40
40
+
color: #b8b8b8;
41
41
+
font-family: "SF Mono", "Fira Code", Menlo, monospace;
42
42
+
font-size: 13px;
43
43
+
line-height: 1.6;
44
44
+
caret-color: #79b8ff;
45
45
+
white-space: pre;
46
46
+
overflow: auto;
47
47
+
tab-size: 4;
48
48
+
-moz-tab-size: 4;
49
49
+
word-wrap: normal;
50
50
+
overflow-wrap: normal;
51
51
+
}
+42
-60
src/client/ui/CodeEditor.tsx
···
1
1
-
import React, { useRef, useEffect, useEffectEvent, useState } from "react";
2
2
-
import { EditorView, keymap } from "@codemirror/view";
3
3
-
import { javascript } from "@codemirror/lang-javascript";
4
4
-
import { syntaxHighlighting, HighlightStyle } from "@codemirror/language";
5
5
-
import { tags } from "@lezer/highlight";
6
6
-
import { history, historyKeymap, defaultKeymap } from "@codemirror/commands";
7
7
-
import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
1
1
+
import React, { useRef, useLayoutEffect, useEffectEvent, useState } from "react";
8
2
import { Pane } from "./Pane.tsx";
9
3
import "./CodeEditor.css";
10
4
11
11
-
const highlightStyle = HighlightStyle.define([
12
12
-
{ tag: tags.keyword, color: "#c678dd" },
13
13
-
{ tag: tags.string, color: "#98c379" },
14
14
-
{ tag: tags.number, color: "#d19a66" },
15
15
-
{ tag: tags.comment, color: "#5c6370", fontStyle: "italic" },
16
16
-
{ tag: tags.function(tags.variableName), color: "#61afef" },
17
17
-
{ tag: tags.typeName, color: "#e5c07b" },
18
18
-
{ tag: [tags.tagName, tags.angleBracket], color: "#e06c75" },
19
19
-
{ tag: tags.attributeName, color: "#d19a66" },
20
20
-
{ tag: tags.propertyName, color: "#abb2bf" },
21
21
-
]);
22
22
-
23
23
-
const minimalTheme = EditorView.theme(
24
24
-
{
25
25
-
"&": { height: "100%", fontSize: "13px", backgroundColor: "transparent" },
26
26
-
".cm-scroller": {
27
27
-
overflow: "auto",
28
28
-
fontFamily: "'SF Mono', 'Fira Code', Menlo, monospace",
29
29
-
lineHeight: "1.6",
30
30
-
padding: "12px",
31
31
-
},
32
32
-
".cm-content": { caretColor: "#79b8ff" },
33
33
-
".cm-cursor": { borderLeftColor: "#79b8ff", borderLeftWidth: "2px" },
34
34
-
".cm-selectionBackground, &.cm-focused .cm-selectionBackground": {
35
35
-
backgroundColor: "#3a3a3a",
36
36
-
},
37
37
-
".cm-activeLine": { backgroundColor: "transparent" },
38
38
-
".cm-gutters": { display: "none" },
39
39
-
".cm-line": { color: "#b8b8b8" },
40
40
-
},
41
41
-
{ dark: true },
42
42
-
);
5
5
+
let cmModule: typeof import("./codemirror.ts") | null = null;
6
6
+
const cmModulePromise = import("./codemirror.ts").then((mod) => {
7
7
+
cmModule = mod;
8
8
+
return mod;
9
9
+
});
43
10
44
11
type CodeEditorProps = {
45
12
defaultValue: string;
···
49
16
50
17
export function CodeEditor({ defaultValue, onChange, label }: CodeEditorProps): React.ReactElement {
51
18
const [initialDefaultValue] = useState(defaultValue);
19
19
+
const [cmLoaded, setCmLoaded] = useState(cmModule !== null);
52
20
const containerRef = useRef<HTMLDivElement>(null);
21
21
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
53
22
54
23
const onEditorChange = useEffectEvent((doc: string) => {
55
24
onChange(doc);
56
25
});
57
26
58
58
-
useEffect(() => {
59
59
-
if (!containerRef.current) return;
27
27
+
useLayoutEffect(() => {
28
28
+
let editorHandle: { destroy: () => void } | null = null;
29
29
+
let destroyed = false;
60
30
61
61
-
const onChangeExtension = EditorView.updateListener.of((update) => {
62
62
-
if (update.docChanged) {
63
63
-
onEditorChange(update.state.doc.toString());
31
31
+
function initEditor(cm: typeof import("./codemirror.ts")) {
32
32
+
if (!destroyed) {
33
33
+
editorHandle = cm.createEditor(
34
34
+
containerRef.current!,
35
35
+
textareaRef.current?.value ?? initialDefaultValue,
36
36
+
onEditorChange,
37
37
+
);
38
38
+
setCmLoaded(true);
64
39
}
65
65
-
});
40
40
+
}
66
41
67
67
-
const editor = new EditorView({
68
68
-
doc: initialDefaultValue,
69
69
-
extensions: [
70
70
-
minimalTheme,
71
71
-
syntaxHighlighting(highlightStyle),
72
72
-
javascript({ jsx: true }),
73
73
-
history(),
74
74
-
closeBrackets(),
75
75
-
keymap.of([...defaultKeymap, ...historyKeymap, ...closeBracketsKeymap]),
76
76
-
onChangeExtension,
77
77
-
],
78
78
-
parent: containerRef.current,
79
79
-
});
42
42
+
if (cmModule) {
43
43
+
initEditor(cmModule);
44
44
+
} else {
45
45
+
cmModulePromise.then(initEditor);
46
46
+
}
80
47
81
81
-
return () => editor.destroy();
48
48
+
return () => {
49
49
+
destroyed = true;
50
50
+
editorHandle?.destroy();
51
51
+
};
82
52
}, [initialDefaultValue]);
83
53
84
54
return (
85
55
<Pane label={label}>
86
86
-
<div className="CodeEditor" ref={containerRef} />
56
56
+
<div className="CodeEditor" ref={containerRef}>
57
57
+
{!cmLoaded && (
58
58
+
<textarea
59
59
+
ref={textareaRef}
60
60
+
className="CodeEditor-fallback"
61
61
+
defaultValue={initialDefaultValue}
62
62
+
onChange={(e) => onChange(e.target.value)}
63
63
+
spellCheck={false}
64
64
+
autoCapitalize="off"
65
65
+
autoCorrect="off"
66
66
+
/>
67
67
+
)}
68
68
+
</div>
87
69
</Pane>
88
70
);
89
71
}
+63
-31
src/client/ui/EmbedApp.tsx
···
1
1
-
import React from "react";
1
1
+
import React, { useState, useEffect } from "react";
2
2
import { SAMPLES } from "../samples.ts";
3
3
import { Workspace } from "./Workspace.tsx";
4
4
import "./EmbedApp.css";
···
10
10
client: string;
11
11
};
12
12
13
13
-
function getCodeFromUrl(): CodeState {
13
13
+
function getParamsFromUrl(): { code: CodeState; seamless: boolean } {
14
14
const params = new URLSearchParams(window.location.search);
15
15
const encoded = params.get("c");
16
16
+
const seamless = params.get("seamless") === "1";
16
17
18
18
+
let code: CodeState;
17
19
if (encoded) {
18
20
try {
19
21
const json = decodeURIComponent(escape(atob(encoded)));
20
22
const parsed = JSON.parse(json) as { server?: string; client?: string };
21
21
-
return {
23
23
+
code = {
22
24
server: (parsed.server ?? DEFAULT_SAMPLE.server).trim(),
23
25
client: (parsed.client ?? DEFAULT_SAMPLE.client).trim(),
24
26
};
25
27
} catch {
26
26
-
// Fall through to defaults
28
28
+
code = {
29
29
+
server: DEFAULT_SAMPLE.server,
30
30
+
client: DEFAULT_SAMPLE.client,
31
31
+
};
27
32
}
33
33
+
} else {
34
34
+
code = {
35
35
+
server: DEFAULT_SAMPLE.server,
36
36
+
client: DEFAULT_SAMPLE.client,
37
37
+
};
28
38
}
29
39
30
30
-
return {
31
31
-
server: DEFAULT_SAMPLE.server,
32
32
-
client: DEFAULT_SAMPLE.client,
33
33
-
};
40
40
+
return { code, seamless };
34
41
}
35
42
36
43
function getFullscreenUrl(code: CodeState): string {
···
40
47
}
41
48
42
49
export function EmbedApp(): React.ReactElement {
43
43
-
const initialCode = getCodeFromUrl();
50
50
+
const [params] = useState(getParamsFromUrl);
51
51
+
const [code, setCode] = useState(params.code);
52
52
+
const seamless = params.seamless;
53
53
+
54
54
+
// Listen for code updates from parent via postMessage
55
55
+
useEffect(() => {
56
56
+
const handleMessage = (event: MessageEvent): void => {
57
57
+
const data = event.data as { type?: string; code?: CodeState };
58
58
+
if (data?.type === "rscexplorer:reset" && data.code) {
59
59
+
const newServer = data.code.server.trim();
60
60
+
const newClient = data.code.client.trim();
61
61
+
setCode((prev) => {
62
62
+
if (prev.server === newServer && prev.client === newClient) {
63
63
+
return prev;
64
64
+
}
65
65
+
return { server: newServer, client: newClient };
66
66
+
});
67
67
+
}
68
68
+
};
69
69
+
70
70
+
window.addEventListener("message", handleMessage);
71
71
+
return () => window.removeEventListener("message", handleMessage);
72
72
+
}, []);
44
73
45
74
const handleCodeChange = (server: string, client: string): void => {
46
75
if (window.parent !== window) {
···
56
85
57
86
return (
58
87
<>
59
59
-
<div className="EmbedApp-header">
60
60
-
<span className="EmbedApp-title">RSC Explorer</span>
61
61
-
<a
62
62
-
href={getFullscreenUrl(initialCode)}
63
63
-
target="_blank"
64
64
-
rel="noopener noreferrer"
65
65
-
className="EmbedApp-fullscreenLink"
66
66
-
title="Open in RSC Explorer"
67
67
-
>
68
68
-
<svg
69
69
-
width="14"
70
70
-
height="14"
71
71
-
viewBox="0 0 24 24"
72
72
-
fill="none"
73
73
-
stroke="currentColor"
74
74
-
strokeWidth="2"
88
88
+
{!seamless && (
89
89
+
<div className="EmbedApp-header">
90
90
+
<span className="EmbedApp-title">RSC Explorer</span>
91
91
+
<a
92
92
+
href={getFullscreenUrl(code)}
93
93
+
target="_blank"
94
94
+
rel="noopener noreferrer"
95
95
+
className="EmbedApp-fullscreenLink"
96
96
+
title="Open in RSC Explorer"
75
97
>
76
76
-
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3" />
77
77
-
</svg>
78
78
-
</a>
79
79
-
</div>
98
98
+
<svg
99
99
+
width="14"
100
100
+
height="14"
101
101
+
viewBox="0 0 24 24"
102
102
+
fill="none"
103
103
+
stroke="currentColor"
104
104
+
strokeWidth="2"
105
105
+
>
106
106
+
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3" />
107
107
+
</svg>
108
108
+
</a>
109
109
+
</div>
110
110
+
)}
80
111
<Workspace
81
81
-
initialServerCode={initialCode.server}
82
82
-
initialClientCode={initialCode.client}
112
112
+
key={`${code.server}:${code.client}`}
113
113
+
initialServerCode={code.server}
114
114
+
initialClientCode={code.client}
83
115
onCodeChange={handleCodeChange}
84
116
/>
85
117
</>
+1
-1
src/client/ui/FlightLog.tsx
···
145
145
if (entries.length === 0) {
146
146
return (
147
147
<div className="FlightLog-output">
148
148
-
<span className="FlightLog-empty FlightLog-empty--waiting">Compiling</span>
148
148
+
<span className="FlightLog-empty FlightLog-empty--waiting">Loading</span>
149
149
</div>
150
150
);
151
151
}
+2
-2
src/client/ui/LivePreview.tsx
···
102
102
103
103
let statusText = "";
104
104
if (isLoading) {
105
105
-
statusText = "Compiling";
105
105
+
statusText = "Loading";
106
106
} else if (isAtStart) {
107
107
statusText = "Ready";
108
108
} else if (isAtEnd) {
···
182
182
</div>
183
183
<div className="LivePreview-container">
184
184
{isLoading ? (
185
185
-
<span className="LivePreview-empty">Compiling...</span>
185
185
+
<span className="LivePreview-empty">Loading...</span>
186
186
) : showPlaceholder ? (
187
187
<span className="LivePreview-empty">{isAtStart ? "Step to begin..." : "Loading..."}</span>
188
188
) : flightPromise ? (
+9
-2
src/client/ui/Workspace.tsx
···
24
24
25
25
useEffect(() => {
26
26
const abort = new AbortController();
27
27
+
const timeoutId = setTimeout(() => {
28
28
+
setSession(null);
29
29
+
}, 1000);
27
30
WorkspaceSession.create(serverCode, clientCode, abort.signal).then((nextSession) => {
28
31
if (!abort.signal.aborted) {
32
32
+
clearTimeout(timeoutId);
29
33
setSession(nextSession);
30
34
}
31
35
});
32
32
-
return () => abort.abort();
36
36
+
return () => {
37
37
+
clearTimeout(timeoutId);
38
38
+
abort.abort();
39
39
+
};
33
40
}, [serverCode, clientCode, resetKey]);
34
41
35
42
function handleServerChange(code: string) {
···
68
75
{isLoading ? (
69
76
<div className="Workspace-loadingOutput">
70
77
<span className="Workspace-loadingEmpty Workspace-loadingEmpty--waiting">
71
71
-
Compiling
78
78
+
Loading
72
79
</span>
73
80
</div>
74
81
) : isError ? (
+75
src/client/ui/codemirror.ts
···
1
1
+
import { EditorView, keymap } from "@codemirror/view";
2
2
+
import { javascript } from "@codemirror/lang-javascript";
3
3
+
import { syntaxHighlighting, HighlightStyle } from "@codemirror/language";
4
4
+
import { tags } from "@lezer/highlight";
5
5
+
import { history, historyKeymap, defaultKeymap } from "@codemirror/commands";
6
6
+
import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
7
7
+
8
8
+
const highlightStyle = HighlightStyle.define([
9
9
+
{ tag: tags.keyword, color: "#c678dd" },
10
10
+
{ tag: tags.string, color: "#98c379" },
11
11
+
{ tag: tags.number, color: "#d19a66" },
12
12
+
{ tag: tags.comment, color: "#5c6370", fontStyle: "italic" },
13
13
+
{ tag: tags.function(tags.variableName), color: "#61afef" },
14
14
+
{ tag: tags.typeName, color: "#e5c07b" },
15
15
+
{ tag: [tags.tagName, tags.angleBracket], color: "#e06c75" },
16
16
+
{ tag: tags.attributeName, color: "#d19a66" },
17
17
+
{ tag: tags.propertyName, color: "#abb2bf" },
18
18
+
]);
19
19
+
20
20
+
const minimalTheme = EditorView.theme(
21
21
+
{
22
22
+
"&": { height: "100%", fontSize: "13px", backgroundColor: "transparent" },
23
23
+
".cm-scroller": {
24
24
+
overflow: "auto",
25
25
+
fontFamily: "'SF Mono', 'Fira Code', Menlo, monospace",
26
26
+
lineHeight: "1.6",
27
27
+
padding: "12px",
28
28
+
},
29
29
+
".cm-content": { caretColor: "#79b8ff" },
30
30
+
".cm-cursor": { borderLeftColor: "#79b8ff", borderLeftWidth: "2px" },
31
31
+
".cm-selectionBackground, &.cm-focused .cm-selectionBackground": {
32
32
+
backgroundColor: "#3a3a3a",
33
33
+
},
34
34
+
".cm-activeLine": { backgroundColor: "transparent" },
35
35
+
".cm-gutters": { display: "none" },
36
36
+
".cm-line": { color: "#b8b8b8" },
37
37
+
},
38
38
+
{ dark: true },
39
39
+
);
40
40
+
41
41
+
export type EditorHandle = {
42
42
+
view: EditorView;
43
43
+
destroy: () => void;
44
44
+
};
45
45
+
46
46
+
export function createEditor(
47
47
+
container: HTMLElement,
48
48
+
doc: string,
49
49
+
onChange: (doc: string) => void,
50
50
+
): EditorHandle {
51
51
+
const onChangeExtension = EditorView.updateListener.of((update) => {
52
52
+
if (update.docChanged) {
53
53
+
onChange(update.state.doc.toString());
54
54
+
}
55
55
+
});
56
56
+
57
57
+
const view = new EditorView({
58
58
+
doc,
59
59
+
extensions: [
60
60
+
minimalTheme,
61
61
+
syntaxHighlighting(highlightStyle),
62
62
+
javascript({ jsx: true }),
63
63
+
history(),
64
64
+
closeBrackets(),
65
65
+
keymap.of([...defaultKeymap, ...historyKeymap, ...closeBracketsKeymap]),
66
66
+
onChangeExtension,
67
67
+
],
68
68
+
parent: container,
69
69
+
});
70
70
+
71
71
+
return {
72
72
+
view,
73
73
+
destroy: () => view.destroy(),
74
74
+
};
75
75
+
}
+8
-2
src/embed.ts
···
27
27
type EmbedOptions = {
28
28
server: string;
29
29
client: string;
30
30
+
seamless?: boolean;
30
31
};
31
32
32
33
type EmbedControl = {
···
66
67
*/
67
68
export function mount(
68
69
container: string | HTMLElement,
69
69
-
{ server, client }: EmbedOptions,
70
70
+
{ server, client, seamless }: EmbedOptions,
70
71
): EmbedControl {
71
72
const el =
72
73
typeof container === "string" ? document.querySelector<HTMLElement>(container) : container;
···
75
76
throw new Error(`RSC Explorer: Container not found: ${container}`);
76
77
}
77
78
79
79
+
const embedUrl = new URL(getEmbedUrl());
80
80
+
if (seamless) {
81
81
+
embedUrl.searchParams.set("seamless", "1");
82
82
+
}
83
83
+
78
84
const iframe = document.createElement("iframe");
79
79
-
iframe.src = getEmbedUrl();
85
85
+
iframe.src = embedUrl.href;
80
86
iframe.style.cssText =
81
87
"width: 100%; height: 100%; border: 1px solid #e0e0e0; border-radius: 8px;";
82
88
+36
-1
vite.config.js
···
68
68
};
69
69
}
70
70
71
71
+
function preloadCodemirrorPlugin() {
72
72
+
return {
73
73
+
name: "preload-codemirror",
74
74
+
transformIndexHtml(html, { bundle, filename }) {
75
75
+
if (!bundle) return; // dev mode
76
76
+
const tags = [];
77
77
+
78
78
+
const cmChunk = Object.keys(bundle).find((k) => k.includes("codemirror"));
79
79
+
if (cmChunk) {
80
80
+
tags.push({
81
81
+
tag: "link",
82
82
+
attrs: { rel: "modulepreload", href: "/" + cmChunk },
83
83
+
injectTo: "head",
84
84
+
});
85
85
+
}
86
86
+
87
87
+
// From index.html, prefetch embed resources for the iframe
88
88
+
if (filename.endsWith("index.html")) {
89
89
+
const embedChunk = Object.keys(bundle).find(
90
90
+
(k) => k.startsWith("assets/embed") && k.endsWith(".js"),
91
91
+
);
92
92
+
if (embedChunk) {
93
93
+
tags.push({
94
94
+
tag: "link",
95
95
+
attrs: { rel: "preload", href: "/" + embedChunk, as: "script", crossorigin: true },
96
96
+
injectTo: "head",
97
97
+
});
98
98
+
}
99
99
+
}
100
100
+
101
101
+
return tags;
102
102
+
},
103
103
+
};
104
104
+
}
105
105
+
71
106
export default defineConfig(({ mode }) => ({
72
72
-
plugins: [react(), rolldownWorkerPlugin(), serveEmbedPlugin()],
107
107
+
plugins: [react(), rolldownWorkerPlugin(), serveEmbedPlugin(), preloadCodemirrorPlugin()],
73
108
server: { port: 3333 },
74
109
define: {
75
110
"process.env.NODE_ENV": JSON.stringify(mode === "development" ? "development" : "production"),