({ code, output })
| 42 | ) |
| 43 | |
| 44 | export function Playground({ code, output }) { |
| 45 | const edRef = useRef(null) |
| 46 | const editorRef = useRef(null) |
| 47 | const defaultText = fromB64(output).replace(/\n$/, '') |
| 48 | const defaultCode = fromB64(code).replace(/\n$/, '') |
| 49 | // Stable ref: React 19 compares dangerouslySetInnerHTML by object identity, so a fresh `{__html}` each render re-applies innerHTML and wipes CodeJar/Shiki's spans (e.g. while running). |
| 50 | const seedHtml = useMemo(() => ({ __html: escapeHtml(defaultCode) }), [defaultCode]) |
| 51 | const [result, setResult] = useState(null) // null = showing default; else { text, error, ms } |
| 52 | const [running, setRunning] = useState(false) |
| 53 | const [phase, setPhase] = useState(null) // cold-start/exec phase: 'runtime' | 'worker' | 'running' |
| 54 | |
| 55 | // Live guard: the mount effect captures runCode once, so a `running` state read here would be the first render's `false` forever — Ctrl+Enter could then fire concurrent runs. A ref reads the current value through that stale closure. |
| 56 | const runningRef = useRef(false) |
| 57 | const runCode = useCallback(async (src) => { |
| 58 | if (runningRef.current) return |
| 59 | runningRef.current = true |
| 60 | setRunning(true) |
| 61 | // Byte-stream contract: each chunk is raw stdout (print already includes its own `end`); concatenate, don't join. |
| 62 | let buf = '' |
| 63 | // Don't clear the terminal up front (collapse + flicker); keep old text until the first chunk replaces it. |
| 64 | try { |
| 65 | const res = await run(src, (chunk) => { |
| 66 | buf += chunk |
| 67 | setResult({ text: buf, error: '', ms: 0 }) |
| 68 | }, setPhase) |
| 69 | setResult({ text: buf, error: res.error, ms: res.ms }) |
| 70 | } catch (e) { |
| 71 | setResult({ text: buf, error: String(e?.message ?? e), ms: 0 }) |
| 72 | } finally { |
| 73 | runningRef.current = false |
| 74 | setRunning(false) |
| 75 | setPhase(null) |
| 76 | } |
| 77 | }, []) |
| 78 | |
| 79 | // Mount the CodeJar editor + Shiki highlighter client-side (lazy: keeps codejar/shiki out of SSR). |
| 80 | useEffect(() => { |
| 81 | let editor |
| 82 | let highlight |
| 83 | let disposed = false |
| 84 | Promise.all([import('./editor.js'), import('./shiki.js')]).then(([{ createEditor }, { createHighlight }]) => { |
| 85 | if (disposed || !edRef.current) return |
| 86 | highlight = createHighlight(() => editor && editor.setCode(editor.getCode())) |
| 87 | editor = createEditor({ |
| 88 | ed: edRef.current, |
| 89 | defaultCode, |
| 90 | onRun: (src) => runCode(src), |
| 91 | highlight, |
| 92 | }) |
| 93 | editorRef.current = editor |
| 94 | }) |
| 95 | // Tear down CodeJar and (critically) the Shiki MutationObserver on <html>; that observer holds a closure over the editor, so without dispose() each unmounted Playground stays pinned in memory. |
| 96 | return () => { |
| 97 | disposed = true |
| 98 | editor?.destroy() |
| 99 | highlight?.dispose() |
| 100 | editorRef.current = null |
| 101 | } |
nothing calls this directly
no test coverage detected