|
1 | | -# clangd browser runtime |
| 1 | +# clangd in the browser |
2 | 2 |
|
3 | | -This project packages Monaco and the WebAssembly build of clangd as a reusable browser component. It creates an editor only inside the element supplied by the host page; it does not use an iframe, register a service worker, replace page-level DOM, or own Run/output UI. |
| 3 | +This package turns ordinary `<codeblock>` tags into editable, runnable C++ examples. It starts with a small textarea editor, upgrades to Monaco when the main bundle is ready, and runs a WebAssembly build of clangd for code help. Multiple blocks on one page share the same runtime, worker, and clangd download. |
4 | 4 |
|
5 | | -## Use the hosted runtime |
| 5 | +## Add a code block |
6 | 6 |
|
7 | | -The preferred deployment uses separate origins: |
| 7 | +Copy the complete contents of `dist/` to one public directory, then include one stylesheet and one classic script: |
8 | 8 |
|
9 | | -```text |
10 | | -https://www.example.com/ host page |
11 | | -https://clangd.example.com/ contents of dist/ |
| 9 | +```html |
| 10 | +<link rel="stylesheet" href="./codeblocks.css" /> |
| 11 | +<script src="./codeblocks.js"></script> |
| 12 | + |
| 13 | +<codeblock compiler="gsnapshot" args="-std=c++26 -freflection"> |
| 14 | +#include <print> |
| 15 | +int main() { |
| 16 | + std::println("foo {} bar", 42); |
| 17 | +} |
| 18 | +</codeblock> |
12 | 19 | ``` |
13 | 20 |
|
14 | | -The host can be ordinary HTML and JavaScript: |
| 21 | +Angle brackets in source code must use normal HTML escaping, such as `<print>`, because the code is embedded in an HTML document. |
15 | 22 |
|
16 | | -```html |
17 | | -<div class="editor-shell"> |
18 | | - <textarea id="fallback">int main() { return 0; }</textarea> |
19 | | - <div id="monaco-host"></div> |
20 | | -</div> |
21 | | - |
22 | | -<script type="module"> |
23 | | - const fallback = document.querySelector("#fallback"); |
24 | | - const host = document.querySelector("#monaco-host"); |
25 | | - const { createCppEditor } = |
26 | | - await import("https://clangd.example.com/editor.js"); |
27 | | -
|
28 | | - const editor = await createCppEditor({ |
29 | | - element: host, |
30 | | - value: fallback.value, |
31 | | - theme: "dark", |
32 | | - onStatus(status) { |
33 | | - console.log(status); |
34 | | - }, |
35 | | - }); |
| 23 | +Add as many `<codeblock>` elements as needed. Tags inserted later are upgraded automatically. Monaco, clangd, and the WebAssembly file are initialized once per page. |
36 | 24 |
|
37 | | - // Capture edits made while Monaco loaded, then swap only the editor nodes. |
38 | | - editor.setValue(fallback.value); |
39 | | - host.style.visibility = "visible"; |
40 | | - editor.layout(); |
41 | | - fallback.remove(); |
| 25 | +A block can also contain multiple tabbed sources: |
42 | 26 |
|
43 | | - await editor.clangdReady; |
44 | | -</script> |
| 27 | +```html |
| 28 | +<codeblock compiler="clang2110" args="-std=c++23" height="360px"> |
| 29 | + <codeblock-tab name="first.cpp"> |
| 30 | +int main() { return 1; } |
| 31 | + </codeblock-tab> |
| 32 | + <codeblock-tab name="second.cpp"> |
| 33 | +int main() { return 2; } |
| 34 | + </codeblock-tab> |
| 35 | +</codeblock> |
45 | 36 | ``` |
46 | 37 |
|
47 | | -`createCppEditor()` resolves as soon as Monaco is usable. clangd downloads and starts independently; `editor.clangdReady` resolves after the language client connects and rejects if clangd is unavailable. A missing cross-origin-isolation policy affects clangd only; Monaco remains usable. |
48 | | - |
49 | | -The returned component API is: |
| 38 | +Each tab keeps its own source. Run compiles the active tab. Use separate `<codeblock>` elements when examples need different compiler settings. |
50 | 39 |
|
51 | | -```js |
52 | | -editor.getValue(); |
53 | | -editor.setValue(source); |
54 | | -editor.focus(); |
55 | | -editor.layout(); |
56 | | -editor.dispose(); |
57 | | -const unsubscribe = editor.onDidChange((source) => {}); |
58 | | -await editor.clangdReady; |
59 | | -``` |
| 40 | +Supported attributes are: |
60 | 41 |
|
61 | | -### Lightweight fallback |
| 42 | +- `compiler`: Compiler Explorer compiler ID, defaulting to `clang2110`. |
| 43 | +- `args`: arguments sent unchanged to Compiler Explorer. |
| 44 | +- `theme`: `auto`, `light`, or `dark`. |
| 45 | +- `debug`: show the basic/full editor and light/dark switches. These are hidden by default. |
| 46 | +- `status`: show the optional user-facing code-help status. |
| 47 | +- `width`: any valid CSS width for the complete block. |
| 48 | +- `height`: any valid CSS height for the editor area, such as `280px`, `40vh`, or `clamp(240px, 50vh, 600px)`. |
| 49 | +- `min-height`: any valid CSS minimum height for the editor area. |
62 | 50 |
|
63 | | -The fallback editor is a separate, small public module and does not load Monaco: |
| 51 | +## JavaScript API |
64 | 52 |
|
65 | | -```js |
66 | | -const { createCppFallbackEditor } = |
67 | | - await import("https://clangd.example.com/fallback.js"); |
| 53 | +`codeblocks.js` creates `globalThis.CodeBlocks` immediately. Its `ready` promise resolves after the module API has loaded: |
68 | 54 |
|
69 | | -const fallback = createCppFallbackEditor({ |
70 | | - element: document.querySelector(".fallback-host"), |
71 | | - value: "int main() { return 0; }", |
72 | | -}); |
| 55 | +```html |
| 56 | +<script> |
| 57 | + CodeBlocks.configure({ |
| 58 | + theme: "dark", |
| 59 | + editorOptions: { |
| 60 | + fontSize: 15, |
| 61 | + wordWrap: "on", |
| 62 | + }, |
| 63 | + styles: { |
| 64 | + "editor-height": "360px", |
| 65 | + "accent": "#7c3aed", |
| 66 | + }, |
| 67 | + }); |
73 | 68 |
|
74 | | -fallback.getValue(); |
75 | | -fallback.setValue("int main() { return 1; }"); |
76 | | -fallback.focus(); |
77 | | -const unsubscribe = fallback.onDidChange((source) => {}); |
78 | | -fallback.dispose(); |
| 69 | + CodeBlocks.ready.then(() => { |
| 70 | + const element = document.querySelector("codeblock"); |
| 71 | + const block = CodeBlocks.get(element); |
| 72 | + block.setValue("int main() { return 0; }"); |
| 73 | + }); |
| 74 | +</script> |
79 | 75 | ``` |
80 | 76 |
|
81 | | -`fallback.js` contains its scoped styles, native textarea overlay, line numbers, scrolling synchronization, and lightweight C++ syntax highlighter. It can also enhance an existing textarea inside the supplied element, preserving source included in the initial HTML. |
82 | | - |
83 | | -### ANSI output |
| 77 | +Call `CodeBlocks.configure(options)` before a block is upgraded to set defaults for later blocks. The available options are `theme`, `showDebugControls`, `showStatus`, `compiler`, `args`, `compilerExplorerUrl`, `editorOptions`, `styles`, and `onStatus`. |
84 | 78 |
|
85 | | -ANSI SGR rendering is available from the main entry and as a lightweight standalone module: |
| 79 | +`CodeBlocks.get(element)` returns the upgraded block instance. It exposes `getValue`, `setValue`, `getTabs`, `selectTab`, `focus`, `run`, `setTheme`, `dispose`, `onDidChange`, `editorReady`, `monacoReady`, and `clangdReady`. `getValue` and `setValue` act on the active tab. `monacoReady` resolves to the underlying Monaco standalone editor for integrations that need the native editor API. |
86 | 80 |
|
87 | | -```js |
88 | | -const { appendAnsi, ansiToFragment, stripAnsi } = |
89 | | - await import("https://clangd.example.com/ansi.js"); |
| 81 | +Styling uses CSS custom properties. Common properties include: |
90 | 82 |
|
91 | | -appendAnsi(outputElement, compilerOutput); |
92 | | -appendAnsi(outputElement, stderr, { className: "stderr" }); |
| 83 | +```css |
| 84 | +codeblock { |
| 85 | + width: min(100%, 900px); |
| 86 | + --codeblocks-editor-height: 360px; |
| 87 | + --codeblocks-editor-min-height: 120px; |
| 88 | + --codeblocks-editor-height-mobile: 300px; |
| 89 | + --codeblocks-accent: #7c3aed; |
| 90 | + --codeblocks-border: #475569; |
| 91 | +} |
93 | 92 | ``` |
94 | 93 |
|
95 | | -The renderer creates text nodes and styled spans rather than injecting HTML. The same functions are also exported by `editor.js`. |
| 94 | +The editor observes its container and relayouts whenever its width or height changes, including flexbox, grid, responsive, and script-driven resizing. |
96 | 95 |
|
97 | | -Status events have a `type` of `monaco-loading`, `monaco-ready`, `clangd-downloading`, `clangd-starting`, `clangd-ready`, or `clangd-error`. Download events also contain `loaded` and, when the server provides it, `total` bytes. |
| 96 | +The lower-level ES module entries remain available as `editor.js`, `fallback.js`, and `ansi.js`. |
98 | 97 |
|
99 | | -## Headers |
| 98 | +## Cross-origin isolation |
100 | 99 |
|
101 | | -The host document must be cross-origin isolated for clangd's SharedArrayBuffer/pthread build: |
| 100 | +clangd uses WebAssembly threads and therefore requires `crossOriginIsolated === true`. The preferred deployment sends these response headers for the document and same-origin runtime files: |
102 | 101 |
|
103 | | -```http |
| 102 | +```text |
104 | 103 | Cross-Origin-Opener-Policy: same-origin |
105 | 104 | Cross-Origin-Embedder-Policy: require-corp |
106 | 105 | ``` |
107 | 106 |
|
108 | | -The runtime asset server must make its public modules, workers, CSS, and WASM available cross-origin: |
109 | | - |
110 | | -```http |
111 | | -Access-Control-Allow-Origin: * |
112 | | -Cross-Origin-Resource-Policy: cross-origin |
113 | | -``` |
114 | | - |
115 | | -Serve `.wasm` as `application/wasm`. [public/_headers](public/_headers) is an example configuration for static hosts that support Netlify-style header files. |
116 | | - |
117 | | -The cross-origin worker bootstrap uses Blob module workers. A restrictive host Content Security Policy therefore needs at least: |
118 | | - |
119 | | -```http |
120 | | -Content-Security-Policy: worker-src blob:; script-src 'self' https://clangd.example.com blob: |
121 | | -``` |
122 | | - |
123 | | -Adjust the runtime origin and the rest of the policy for the site. The runtime never registers a service worker or attempts to change the host's COOP/COEP policy. GitHub Pages users need a fronting service capable of setting these headers, or a COI service-worker solution managed by the host site itself. |
124 | | - |
125 | | -## Self-host the same package |
| 107 | +The reusable loader does not register a service worker by default. Hosts such as GitHub Pages that cannot set these headers can explicitly opt into the included helper: |
126 | 108 |
|
127 | | -Extract the archive without rebuilding: |
128 | | - |
129 | | -```bash |
130 | | -mkdir -p assets/clangd |
131 | | -tar xf clangd-browser-runtime.tar.gz -C assets/clangd |
| 109 | +```html |
| 110 | +<script |
| 111 | + src="./codeblocks.js" |
| 112 | + data-coi-serviceworker="./coi-serviceworker.js" |
| 113 | +></script> |
132 | 114 | ``` |
133 | 115 |
|
134 | | -Then import the exact same distribution from the host origin: |
135 | | - |
136 | | -```js |
137 | | -const { createCppEditor } = await import("/assets/clangd/editor.js"); |
138 | | -``` |
| 116 | +That opt-in registers a host-owned service worker and reloads once. Do not use the attribute when the server already sends the headers. |
139 | 117 |
|
140 | | -All resource URLs are relative to emitted modules, so the distribution works at the origin root or any directory such as `/assets/clangd/`. |
| 118 | +If assets are hosted on another origin, that origin must allow CORS and send `Cross-Origin-Resource-Policy: cross-origin` or an equivalent policy accepted by the embedding page. |
141 | 119 |
|
142 | | -## Build and package |
| 120 | +## Build and test |
143 | 121 |
|
144 | | -Normal frontend builds use prebuilt artifacts and never clone or compile LLVM: |
| 122 | +The repository already contains the prebuilt clangd artifacts in `public/wasm`. Front-end changes do not rebuild LLVM or clangd. |
145 | 123 |
|
146 | 124 | ```bash |
147 | 125 | pnpm install |
148 | 126 | pnpm build |
149 | | -pnpm pack:runtime |
| 127 | +pnpm demo:origins |
150 | 128 | ``` |
151 | 129 |
|
152 | | -The build produces stable `dist/editor.js`, `dist/fallback.js`, `dist/ansi.js`, `dist/editor.css`, internal chunks in `dist/assets/`, and the complete Emscripten artifact set in `dist/wasm/`. Packaging creates `clangd-browser-runtime.tar.gz`. |
| 130 | +Open `http://localhost:4173/`. The example includes `debug`, so both visual editor and theme switches are shown. |
153 | 131 |
|
154 | | -Install an existing artifact directory or archive with: |
| 132 | +Run the browser suite in another terminal: |
155 | 133 |
|
156 | 134 | ```bash |
157 | | -./scripts/install-clangd-artifacts path/to/clangd-artifacts.tar.gz |
| 135 | +pnpm exec playwright install chromium |
| 136 | +pnpm test:browser |
158 | 137 | ``` |
159 | 138 |
|
160 | | -The input must contain `clangd.js`, `clangd.wasm`, and any other `clangd*` files produced by Emscripten. `build.sh` remains a separate manual reference workflow and is not invoked by `pnpm build` or CI. |
| 139 | +Create a deployable archive with: |
161 | 140 |
|
162 | | -## Compiler Explorer Run UI |
| 141 | +```bash |
| 142 | +pnpm pack:runtime |
| 143 | +``` |
163 | 144 |
|
164 | | -Compilation is intentionally a host concern. The demo keeps an `activeEditor.getValue()` abstraction that initially reads the textarea and later points to Monaco, then sends that source directly to the Compiler Explorer API. This makes Run independent of both Monaco and clangd readiness. |
| 145 | +The output is `clangd-browser-runtime.tar.gz`. It includes the main code-block files, lower-level entries, internal chunks, and prebuilt WebAssembly artifacts. |
0 commit comments