Skip to content

Commit 4a64c4f

Browse files
committed
refactor and extend for our use case
1 parent 6cc2d7a commit 4a64c4f

41 files changed

Lines changed: 1602 additions & 1608 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ lerna-debug.log*
1010
node_modules
1111
dist
1212
dist-ssr
13+
test-results
14+
playwright-report
1315
*.local
1416

1517
# Editor directories and files
@@ -23,4 +25,4 @@ dist-ssr
2325
*.sln
2426
*.sw?
2527

26-
.pnpm-store
28+
.pnpm-store

‎Dockerfile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ RUN pnpm install --frozen-lockfile --prefer-offline
2626
COPY --chown=node:node . .
2727

2828
ENV NODE_ENV=development
29-
EXPOSE 5173
29+
EXPOSE 4173 4174 4175 4176
3030

3131
USER node
3232
CMD ["pnpm", "dev", "--host", "0.0.0.0"]

‎README.md‎

Lines changed: 179 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,189 @@
1-
# [Clangd](https://clangd.llvm.org) in Browser
1+
# clangd browser runtime
22

3-
Finally, I made clangd work in browser. You can now get C++ IntelliSense directly without installing any native compiler or relying on a remote server.
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.
44

5-
This repo contains a build script for clangd on WebAssembly (TBD), and an example of using clangd WASM module together with [Monaco Editor](https://microsoft.github.io/monaco-editor) powered by it's Language Client port (thanks [TypeFox/monaco-languageclient](https://github.com/TypeFox/monaco-languageclient)).
5+
## Use the hosted runtime
66

7-
## How to build
7+
The preferred deployment uses separate origins:
88

9-
You can take a look at `build.sh` for how to build clangd to WASM. Once `clangd.wasm` generated at `public/wasm`, you can build those frontend code using `pnpm install` and `pnpm build`.
9+
```text
10+
https://www.example.com/ host page
11+
https://clangd.example.com/ contents of dist/
12+
```
1013

11-
## About the example page
14+
The host can be ordinary HTML and JavaScript:
1215

13-
I've publish a [GitHub Page](https://clangd.guyutongxue.site/) for demonstrating how clangd works in your browser.
16+
```html
17+
<div class="editor-shell">
18+
<textarea id="fallback">int main() { return 0; }</textarea>
19+
<div id="monaco-host"></div>
20+
</div>
1421

15-
Notice that clangd is a multi-thread program, so we need `SharedArrayBuffer` -- and more over, a strict context called `crossOriginIsolated`. GitHub Pages do not send COOP/COEP headers for us to enabling that context, so I served this site through CloudFlare with a custom rule adding those headers. If you want to deploy this project by yourself, make sure correct COOP/COEP header is set on the server side, or you can use [`coi-serviceworker`](https://github.com/gzuidhof/coi-serviceworker).
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");
1627
17-
You can pass URL search parameters to control the initial state of this page ([see here](./docs/params.md)), or embed it in your website ([see here](./docs/embed.md), **your website needs to be cross-origin-isolated too**)
28+
const editor = await createCppEditor({
29+
element: host,
30+
value: fallback.value,
31+
theme: "dark",
32+
onStatus(status) {
33+
console.log(status);
34+
},
35+
});
1836
19-
## Acknowledgement
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();
2042
21-
- [soedirgo/llvm-wasm](https://github.com/soedirgo/llvm-wasm) provides a very detailed guide on how to build LLVM to wasm. Better than famous works like [binji/wasm-clang](https://github.com/binji/wasm-clang) or [tbfleming/cib](https://github.com/tbfleming/cib) (but also thank you guys :P);
22-
- [nokotan/vscode-clangd](https://github.com/nokotan/vscode-clangd) patch vscode's clangd extension with WASM binary to make it work on vscode-web. Some bugs but useful; no build scripts or further documentation;
23-
- [ConorBobbleHat/clangd-wasm](https://github.com/ConorBobbleHat/clangd-wasm) and [ConorBobbleHat/clangd-wasm-core](https://github.com/ConorBobbleHat/clangd-wasm-core); build scripts provided but lack examples on how to use them.
43+
await editor.clangdReady;
44+
</script>
45+
```
46+
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:
50+
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+
```
60+
61+
### Lightweight fallback
62+
63+
The fallback editor is a separate, small public module and does not load Monaco:
64+
65+
```js
66+
const { createCppFallbackEditor } =
67+
await import("https://clangd.example.com/fallback.js");
68+
69+
const fallback = createCppFallbackEditor({
70+
element: document.querySelector(".fallback-host"),
71+
value: "int main() { return 0; }",
72+
});
73+
74+
fallback.getValue();
75+
fallback.setValue("int main() { return 1; }");
76+
fallback.focus();
77+
const unsubscribe = fallback.onDidChange((source) => {});
78+
fallback.dispose();
79+
```
80+
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
84+
85+
ANSI SGR rendering is available from the main entry and as a lightweight standalone module:
86+
87+
```js
88+
const { appendAnsi, ansiToFragment, stripAnsi } =
89+
await import("https://clangd.example.com/ansi.js");
90+
91+
appendAnsi(outputElement, compilerOutput);
92+
appendAnsi(outputElement, stderr, { className: "stderr" });
93+
```
94+
95+
The renderer creates text nodes and styled spans rather than injecting HTML. The same functions are also exported by `editor.js`.
96+
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.
98+
99+
See [demo/index.html](demo/index.html), [demo/demo.js](demo/demo.js), and [demo/demo.css](demo/demo.css) for a complete framework-free highlighted fallback editor, seamless handoff, ANSI-aware Run output, and Compiler Explorer link. The demo's **Show fallback / Show Monaco** toggle recreates the fallback on demand for visual comparison; the normal one-way startup handoff still removes and cleans up the fallback.
100+
101+
## Headers
102+
103+
The host document must be cross-origin isolated for clangd's SharedArrayBuffer/pthread build:
104+
105+
```http
106+
Cross-Origin-Opener-Policy: same-origin
107+
Cross-Origin-Embedder-Policy: require-corp
108+
```
109+
110+
The runtime asset server must make its public modules, workers, CSS, and WASM available cross-origin:
111+
112+
```http
113+
Access-Control-Allow-Origin: *
114+
Cross-Origin-Resource-Policy: cross-origin
115+
```
116+
117+
Serve `.wasm` as `application/wasm`. [public/_headers](public/_headers) is an example configuration for static hosts that support Netlify-style header files.
118+
119+
The cross-origin worker bootstrap uses Blob module workers. A restrictive host Content Security Policy therefore needs at least:
120+
121+
```http
122+
Content-Security-Policy: worker-src blob:; script-src 'self' https://clangd.example.com blob:
123+
```
124+
125+
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.
126+
127+
## Self-host the same package
128+
129+
Extract the archive without rebuilding:
130+
131+
```bash
132+
mkdir -p assets/clangd
133+
tar xf clangd-browser-runtime.tar.gz -C assets/clangd
134+
```
135+
136+
Then import the exact same distribution from the host origin:
137+
138+
```js
139+
const { createCppEditor } = await import("/assets/clangd/editor.js");
140+
```
141+
142+
All resource URLs are relative to emitted modules, so the distribution works at the origin root or any directory such as `/assets/clangd/`.
143+
144+
## Build and package
145+
146+
Normal frontend builds use prebuilt artifacts and never clone or compile LLVM:
147+
148+
```bash
149+
pnpm install
150+
pnpm build
151+
pnpm pack:runtime
152+
```
153+
154+
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`.
155+
156+
Install an existing artifact directory or archive with:
157+
158+
```bash
159+
./scripts/install-clangd-artifacts path/to/clangd-artifacts.tar.gz
160+
```
161+
162+
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.
163+
164+
## Cross-origin demo and tests
165+
166+
Build first, then start all acceptance origins:
167+
168+
```bash
169+
pnpm build
170+
pnpm demo:origins
171+
```
172+
173+
- `http://localhost:4173` serves the demo host with COOP/COEP.
174+
- `http://localhost:4174` serves runtime assets with CORS/CORP.
175+
- `http://localhost:4175` serves the same runtime at `/assets/clangd/` for same-origin testing.
176+
- `http://localhost:4176` serves a non-isolated host for graceful-degradation testing.
177+
178+
Run the Chromium acceptance suite with:
179+
180+
```bash
181+
pnpm exec playwright install chromium
182+
pnpm test:browser
183+
```
184+
185+
The tests cover immediate fallback editing and Run, delayed runtime/WASM, edit-preserving handoff, DOM ownership, cross-origin isolation and worker startup, real clangd diagnostics, failure behavior, and same-origin extraction layout.
186+
187+
## Compiler Explorer Run UI
188+
189+
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.

‎demo/demo.css‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
:root {
2+
color-scheme: dark;
3+
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
4+
background: #0a0d12;
5+
color: #e8edf5;
6+
}
7+
* { box-sizing: border-box; }
8+
body { margin: 0; min-height: 100vh; background: radial-gradient(circle at 30% 0, #17243a, transparent 40rem); }
9+
main { width: min(960px, calc(100% - 2rem)); margin: 0 auto; padding: 4rem 0; }
10+
.eyebrow { color: #70d6ff; font: 700 .75rem/1 monospace; letter-spacing: .14em; text-transform: uppercase; }
11+
h1 { margin: .5rem 0; font-size: clamp(2rem, 6vw, 4rem); letter-spacing: -.05em; }
12+
.intro { color: #aab7cb; max-width: 42rem; }
13+
.cpp-demo { margin-top: 2.5rem; overflow: hidden; border: 1px solid #344157; border-radius: 14px; background: #111720; box-shadow: 0 20px 80px #0008; }
14+
.editor-shell { position: relative; height: 430px; background: #1f1f1f; }
15+
.fallback-host, .monaco-host { position: absolute; inset: 0; }
16+
/* Before fallback.js arrives, keep the initial HTML textarea usable and close
17+
to its enhanced appearance. The package supplies all highlighting styles. */
18+
.fallback-host textarea { width: 100%; height: 100%; resize: none; border: 0; outline: 0; padding: 16px 12px 16px 31px; background: #1f1f1f; color: #d4d4d4; font: 14px/23px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
19+
.monaco-host { visibility: hidden; }
20+
.monaco-host.is-ready { visibility: visible; }
21+
.toolbar { min-height: 58px; display: flex; align-items: center; gap: .75rem; padding: .65rem .9rem; border-top: 1px solid #344157; }
22+
button { border: 0; border-radius: 8px; padding: .65rem 1.25rem; background: #70d6ff; color: #071018; font-weight: 800; cursor: pointer; }
23+
button.secondary { border: 1px solid #3b4a61; background: #1a2230; color: #c7d3e5; }
24+
button:disabled { opacity: .65; cursor: wait; }
25+
a { color: #b9c9df; text-decoration: none; }
26+
a:hover { color: white; }
27+
.status { margin-left: auto; color: #8190a6; font-size: .82rem; }
28+
.output-drawer { border-top: 1px solid #344157; background: #080b10; }
29+
.output-drawer header { padding: .75rem 1rem; color: #8190a6; font-size: .75rem; text-transform: uppercase; letter-spacing: .1em; }
30+
.output-drawer pre { min-height: 4rem; max-height: 20rem; overflow: auto; margin: 0; padding: 0 1rem 1rem; white-space: pre-wrap; color: #d7dee9; }
31+
.output-drawer .stderr { color: #f48771; }
32+
@media (max-width: 700px) {
33+
main { padding-top: 2rem; }
34+
.editor-shell { height: 360px; }
35+
.toolbar { flex-wrap: wrap; }
36+
.status { flex-basis: 100%; margin-left: 0; }
37+
}

0 commit comments

Comments
 (0)