Skip to content

Commit cd2ca23

Browse files
committed
log more, refactor
1 parent 6a8a413 commit cd2ca23

8 files changed

Lines changed: 117 additions & 55 deletions

File tree

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ Supported attributes are:
4343
- `args`: arguments sent unchanged to Compiler Explorer.
4444
- `theme`: `auto`, `light`, or `dark`.
4545
- `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.
4746
- `width`: any valid CSS width for the complete block.
4847
- `height`: any valid CSS height for the editor area, such as `280px`, `40vh`, or `clamp(240px, 50vh, 600px)`.
4948
- `min-height`: any valid CSS minimum height for the editor area.
@@ -74,7 +73,7 @@ Supported attributes are:
7473
</script>
7574
```
7675

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`.
76+
Call `CodeBlocks.configure(options)` before a block is upgraded to set defaults for later blocks. The available options are `theme`, `showDebugControls`, `compiler`, `args`, `compilerExplorerUrl`, `editorOptions`, `styles`, and `onStatus`.
7877

7978
`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.
8079

@@ -93,6 +92,16 @@ codeblock {
9392

9493
The editor observes its container and relayouts whenever its width or height changes, including flexbox, grid, responsive, and script-driven resizing.
9594

95+
Debug mode also writes lifecycle messages to the browser console, including Monaco loading/loaded, clangd download progress, clangd starting/loaded/activated, and complete clangd error messages. Add `data-debug` to the loader script to include loader, HTTPS, service-worker, and isolation messages:
96+
97+
```html
98+
<script
99+
src="./codeblocks.js"
100+
data-coi-serviceworker="./coi-serviceworker.js"
101+
data-debug
102+
></script>
103+
```
104+
96105
The lower-level ES module entries remain available as `editor.js`, `fallback.js`, and `ansi.js`.
97106

98107
## Cross-origin isolation
@@ -115,6 +124,8 @@ The reusable loader does not register a service worker by default. Hosts such as
115124

116125
That opt-in registers a host-owned service worker and reloads once. Do not use the attribute when the server already sends the headers.
117126

127+
WebAssembly threads, service workers, and cross-origin isolation require a secure context. Enable "Enforce HTTPS" for a GitHub Pages custom domain. When the isolation helper is enabled, the loader also redirects non-local HTTP pages to the same HTTPS URL as a fallback. The included `clangd.cpp.social` example redirects before loading its external CSS or JavaScript.
128+
118129
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.
119130

120131
## Build and test

clangd-browser-runtime.tar.gz

-4.63 KB
Binary file not shown.

public/codeblocks.js

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,24 @@
33

44
var script = document.currentScript;
55
if (!script || !script.src) throw new Error("codeblocks.js requires a script URL");
6+
var debug = script.hasAttribute("data-debug");
7+
var serviceWorker = script.getAttribute("data-coi-serviceworker");
8+
var localHost = /^(localhost|127(?:\.\d+){3}|\[::1\])$/.test(location.hostname);
9+
function log(message) {
10+
if (debug) console.info("[CodeBlocks] " + message);
11+
}
12+
function fail(message, error) {
13+
console.error("[CodeBlocks] " + message, error || "");
14+
}
15+
16+
log("Loader started at " + location.href);
17+
if (serviceWorker !== null && location.protocol === "http:" && !localHost) {
18+
var secureUrl = new URL(location.href);
19+
secureUrl.protocol = "https:";
20+
log("HTTPS is required for clangd; redirecting to " + secureUrl.href);
21+
location.replace(secureUrl.href);
22+
return;
23+
}
624
var queuedConfigurations = [];
725
var loadedModule;
826
var api = {
@@ -14,26 +32,38 @@
1432
globalThis.CodeBlocks = api;
1533

1634
api.ready = (async function () {
17-
var serviceWorker = script.getAttribute("data-coi-serviceworker");
18-
if (serviceWorker !== null && !globalThis.crossOriginIsolated && "serviceWorker" in navigator) {
19-
await navigator.serviceWorker.register(
20-
new URL(serviceWorker || "./coi-serviceworker.js", script.src),
21-
{ scope: "./" },
22-
);
23-
await navigator.serviceWorker.ready;
24-
if (!globalThis.crossOriginIsolated) {
25-
location.reload();
26-
await new Promise(function () {});
35+
try {
36+
if (serviceWorker !== null && !globalThis.crossOriginIsolated) {
37+
if (!("serviceWorker" in navigator)) {
38+
fail("Service workers are unavailable; clangd cannot enable cross-origin isolation");
39+
} else {
40+
var workerUrl = new URL(serviceWorker || "./coi-serviceworker.js", script.src);
41+
log("Registering isolation service worker at " + workerUrl.href);
42+
await navigator.serviceWorker.register(workerUrl, { scope: "./" });
43+
await navigator.serviceWorker.ready;
44+
log("Isolation service worker activated");
45+
if (!globalThis.crossOriginIsolated) {
46+
log("Reloading once to enable cross-origin isolation");
47+
location.reload();
48+
await new Promise(function () {});
49+
}
50+
}
2751
}
28-
}
2952

30-
loadedModule = await import(new URL("./codeblocks-module.js", script.src).href);
31-
queuedConfigurations.forEach(loadedModule.configureCodeBlocks);
32-
api.configure = loadedModule.configureCodeBlocks;
33-
api.create = loadedModule.createCodeBlock;
34-
api.get = loadedModule.getCodeBlock;
35-
api.start = loadedModule.startCodeBlocks;
36-
loadedModule.startCodeBlocks();
37-
return api;
53+
log("Cross-origin isolation: " + globalThis.crossOriginIsolated);
54+
loadedModule = await import(new URL("./codeblocks-module.js", script.src).href);
55+
log("Code block module loaded");
56+
queuedConfigurations.forEach(loadedModule.configureCodeBlocks);
57+
api.configure = loadedModule.configureCodeBlocks;
58+
api.create = loadedModule.createCodeBlock;
59+
api.get = loadedModule.getCodeBlock;
60+
api.start = loadedModule.startCodeBlocks;
61+
loadedModule.startCodeBlocks();
62+
log("Code block scan started");
63+
return api;
64+
} catch (error) {
65+
fail("Loader failed: " + (error instanceof Error ? error.message : String(error)), error);
66+
throw error;
67+
}
3868
})();
3969
})();

public/index.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>C++ code block example</title>
77
<link rel="stylesheet" href="./codeblocks.css" />
8-
<script src="./codeblocks.js" data-coi-serviceworker="./coi-serviceworker.js"></script>
8+
<script
9+
src="./codeblocks.js"
10+
data-coi-serviceworker="./coi-serviceworker.js"
11+
data-debug
12+
></script>
913
<style>
1014
:root { color-scheme: light dark; background: #ffffff; color: #1f2328; font-family: system-ui, sans-serif; }
1115
body { margin: 0; }

src/codeblocks.css

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,6 @@ codeblock,
136136
.codeblocks-root a:hover { text-decoration: underline; }
137137
.codeblocks-debug { display: inline-flex; gap: 8px; }
138138
.codeblocks-debug[hidden] { display: none; }
139-
.codeblocks-status {
140-
margin-left: auto;
141-
color: var(--codeblocks-muted);
142-
font-size: 12px;
143-
}
144-
.codeblocks-status[hidden] { display: none; }
145-
146139
.codeblocks-output {
147140
border-top: 1px solid var(--codeblocks-border);
148141
background: var(--codeblocks-output-background);
@@ -175,5 +168,4 @@ codeblock,
175168

176169
@media (max-width: 640px) {
177170
.codeblocks-editor-shell { height: var(--codeblocks-editor-height-mobile); }
178-
.codeblocks-status { flex-basis: 100%; margin-left: 0; }
179171
}

src/codeblocks.ts

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export type CodeBlockTheme = "auto" | "light" | "dark";
99
export interface CodeBlocksConfiguration {
1010
theme?: CodeBlockTheme;
1111
showDebugControls?: boolean;
12-
showStatus?: boolean;
1312
compiler?: string;
1413
args?: string;
1514
compilerExplorerUrl?: string;
@@ -116,12 +115,7 @@ export function createCodeBlock(options: CreateCodeBlockOptions): CodeBlock {
116115
compilerLink.target = "_blank";
117116
compilerLink.rel = "noopener";
118117
compilerLink.textContent = "Open in Compiler Explorer";
119-
const status = document.createElement("span");
120-
status.className = "codeblocks-status";
121-
status.dataset.status = "";
122-
status.hidden = !options.showStatus;
123-
status.textContent = "Code editor ready";
124-
toolbar.append(runButton, debugControls, compilerLink, status);
118+
toolbar.append(runButton, debugControls, compilerLink);
125119

126120
const outputDrawer = document.createElement("section");
127121
outputDrawer.className = "codeblocks-output";
@@ -160,6 +154,7 @@ export function createCodeBlock(options: CreateCodeBlockOptions): CodeBlock {
160154
let editor: CppEditor | undefined;
161155
let activeEditor: Pick<CppEditor, "getValue" | "setValue" | "focus"> = fallback;
162156
let disposed = false;
157+
let lastLoggedDownload = -1;
163158
const changeListeners = new Set<(value: string) => void>();
164159
let unsubscribeActive = fallback.onDidChange(notifyChange);
165160

@@ -185,8 +180,9 @@ export function createCodeBlock(options: CreateCodeBlockOptions): CodeBlock {
185180
const editorReady = upgradeEditor();
186181
const monacoReady = editorReady.then((created) => created.getMonacoEditor());
187182
void editorReady.catch((error: unknown) => {
188-
status.textContent = "Basic editor ready";
189-
status.title = `Code help is unavailable: ${errorMessage(error)}`;
183+
if (options.showDebugControls) {
184+
console.error("[CodeBlocks] Editor failed", error);
185+
}
190186
rejectClangd(error);
191187
});
192188

@@ -216,26 +212,25 @@ export function createCodeBlock(options: CreateCodeBlockOptions): CodeBlock {
216212
activeEditor = created;
217213
subscribeToActive(created);
218214
editorToggle.disabled = false;
219-
status.textContent = "Code editor ready";
220215
created.clangdReady.then(resolveClangd, rejectClangd);
221216
return created;
222217
}
223218

224219
function reportStatus(event: EditorStatus): void {
225-
options.onStatus?.(event);
226-
if (event.type === "clangd-downloading") {
227-
const percent = event.total
228-
? ` ${Math.round((event.loaded / event.total) * 100)}%`
229-
: "";
230-
status.textContent = `Preparing code help${percent}`;
231-
} else if (event.type === "clangd-starting") {
232-
status.textContent = "Preparing code help";
233-
} else if (event.type === "clangd-ready") {
234-
status.textContent = "Code help ready";
235-
} else if (event.type === "clangd-error") {
236-
status.textContent = "Code editor ready";
237-
status.title = `Code help is unavailable: ${event.error.message}`;
220+
if (options.showDebugControls) {
221+
if (event.type !== "clangd-downloading") {
222+
logStatus(event);
223+
} else {
224+
const milestone = event.total
225+
? Math.floor((event.loaded / event.total) * 10) * 10
226+
: Math.floor(event.loaded / (5 * 1024 * 1024));
227+
if (milestone !== lastLoggedDownload) {
228+
lastLoggedDownload = milestone;
229+
logStatus(event);
230+
}
231+
}
238232
}
233+
options.onStatus?.(event);
239234
}
240235

241236
async function run(): Promise<void> {
@@ -400,7 +395,6 @@ function upgradeWithin(root: ParentNode): void {
400395
element,
401396
theme: attributeTheme(element) ?? configuration.theme,
402397
showDebugControls: element.hasAttribute("debug") || configuration.showDebugControls,
403-
showStatus: element.hasAttribute("status") || configuration.showStatus,
404398
compiler: element.getAttribute("compiler") ?? configuration.compiler,
405399
args: element.getAttribute("args") ?? configuration.args,
406400
});
@@ -462,6 +456,27 @@ function errorMessage(error: unknown): string {
462456
return error instanceof Error ? error.message : String(error);
463457
}
464458

459+
function logStatus(event: EditorStatus): void {
460+
if (event.type === "monaco-loading") {
461+
console.info("[CodeBlocks] Monaco loading");
462+
} else if (event.type === "monaco-ready") {
463+
console.info("[CodeBlocks] Monaco loaded");
464+
} else if (event.type === "clangd-downloading") {
465+
const progress = event.total
466+
? `${Math.round((event.loaded / event.total) * 100)}%`
467+
: `${event.loaded} bytes`;
468+
console.info(`[CodeBlocks] clangd downloading: ${progress}`);
469+
} else if (event.type === "clangd-starting") {
470+
console.info("[CodeBlocks] clangd starting");
471+
} else if (event.type === "clangd-loaded") {
472+
console.info("[CodeBlocks] clangd loaded");
473+
} else if (event.type === "clangd-ready") {
474+
console.info("[CodeBlocks] clangd activated");
475+
} else if (event.type === "clangd-error") {
476+
console.error(`[CodeBlocks] clangd error: ${event.error.message}`, event.error);
477+
}
478+
}
479+
465480
interface CompilerLine { text: string }
466481
interface CompilerResult {
467482
didExecute: boolean;

src/runtime/clangd.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type EditorStatus =
66
| { type: "monaco-ready" }
77
| { type: "clangd-downloading"; loaded: number; total?: number }
88
| { type: "clangd-starting" }
9+
| { type: "clangd-loaded" }
910
| { type: "clangd-ready" }
1011
| { type: "clangd-error"; error: Error };
1112

@@ -62,6 +63,7 @@ export function startClangd(report: StatusReporter): ClangdWorkerHandle {
6263
case "ready":
6364
if (!settled) {
6465
settled = true;
66+
report({ type: "clangd-loaded" });
6567
resolveReady();
6668
}
6769
break;

tests/runtime.spec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ test("the example uses one public script and stylesheet and shows debug controls
88
await expect(page.locator(".codeblocks-debug")).toBeVisible();
99
await expect(page.locator("[data-theme-toggle]")).toBeVisible();
1010
await expect(page.locator("[data-editor-toggle]")).toBeVisible();
11-
await expect(page.locator("[data-status]")).toBeHidden();
11+
await expect(page.locator("[data-status]")).toHaveCount(0);
1212
await expect(page.locator(".monaco-editor")).toBeVisible({ timeout: 60_000 });
1313
expect(await page.locator('link[rel="stylesheet"][href]').count()).toBe(1);
1414
expect(await page.locator('script[src]').count()).toBe(1);
@@ -150,7 +150,9 @@ test("the main API exposes Monaco options, styling, and the editor instance", as
150150

151151
test("code help works under the opt-in isolation service worker", async ({ page }) => {
152152
const errors: string[] = [];
153+
const messages: string[] = [];
153154
page.on("pageerror", (error) => errors.push(error.message));
155+
page.on("console", (message) => messages.push(message.text()));
154156
await page.goto("http://localhost:4173/", { waitUntil: "domcontentloaded" });
155157
await expect(page.locator("codeblock")).toHaveClass(/codeblocks-root/);
156158
expect(await page.evaluate(() => crossOriginIsolated)).toBe(true);
@@ -163,6 +165,12 @@ test("code help works under the opt-in isolation service worker", async ({ page
163165
});
164166
await expect(page.locator(".squiggly-error").first()).toBeVisible({ timeout: 30_000 });
165167
expect(errors).toEqual([]);
168+
expect(messages).toEqual(expect.arrayContaining([
169+
expect.stringContaining("[CodeBlocks] Monaco loaded"),
170+
expect.stringContaining("[CodeBlocks] clangd starting"),
171+
expect.stringContaining("[CodeBlocks] clangd loaded"),
172+
expect.stringContaining("[CodeBlocks] clangd activated"),
173+
]));
166174
});
167175

168176
test("overflow widgets are not clipped by the code block", async ({ page }) => {

0 commit comments

Comments
 (0)