Skip to content

VS Code for the web

Generated from docs/compat/vscode-web.md when the site was built at 7aa23e2.

  • Build: vscode-web 1.145.2 (commit c4453f8ea430795f941c1b2ff6c9321135a74a02), the standalone web build (web-standalone from update.code.visualstudio.com) served from a directory, with its own index.html.

  • Run: VS Code is an app of the corpus (docs/compat/apps.md): tests/apps/run.sh vscode-web [--engine quickjs|node|bun] [--screenshot] (tests/vscode-web/run.sh still works and runs it), after tests/apps/run.sh vscode-web --fetch downloads the build, checks its sha256 and writes its index.html (tests/apps/vscode-web/fetch.sh and make-index.mjs: the builtin extensions). Each run serves it with tests/apps/vscode-web/driver.js added, as

    Terminal window
    niso-run --page niso-app.html --root . --url http://localhost/ --dump --max-ms 20000

    driver.js opens an untitled JavaScript file through the workbench.action.files.newUntitledFile command, types a function into it with type, and checks the editor’s tokens (NISO-APP ok highlights the file).

Rechecked on 2026-11-09 at intl 93473a9 (main 1d8ddc8 with niso-intl), with nothing stubbed: the page is make-index.mjs’s index.html, whose driver only wraps WebAssembly’s functions to log them. The same page ran on all three engines, each with a storage directory of its own (a reused one restores the last session’s editors):

Terminal window
niso-run --page index.html --root . --url http://localhost/ --dump --max-ms 20000 # QuickJS
node --experimental-vm-modules bindings/js/napi/niso-node.js --page index.html --root . \
--url http://localhost/ --dump --max-ms 20000 # V8
bun bindings/js/napi/niso-node.js --page index.html ... # JavaScriptCore

On QuickJS, Node and Bun alike the workbench comes up, the driver’s newUntitledFile and type commands run without throwing, onig.wasm instantiates, and the typed file’s tokens are the same on all three, bracket pair colors included:

mtk23="function" mtk27="greet" mtk25="name" mtk22="// say hello" mtk17="return"
mtk29="\"Hello, \"" mtk3="+" mtk7="42" (and bracket-highlighting-0 on ( ) { })

VS Code on QuickJS, rendered by niso-iced

That is niso-iced running the page on QuickJS (--page index.html --root . --url http://localhost/ --max-ms 20000 --size 1280x800 --screenshot). niso-iced only runs QuickJS, so for Node and Bun these are the DOM each run ended with (--dump, scripts removed) rendered by niso-iced at niso-run’s 800x600; canvases, such as the minimap, are blank in them:

Node 26.10.0 (V8) Bun 1.4.4 (JavaScriptCore)

The only visible difference is that Node and Bun keep the Welcome tab open beside the untitled file.

What still breaks, on all three engines unless noted:

  1. blitz panics with “invalid SlotMap key used” at layout/mod.rs:160, three times in each Node and Bun run and not in this QuickJS run (it varies from run to run, as before).
  2. The language detection worker starts, but its model loader, the webpack runtime of @vscode/vscode-languagedetection’s Node build, loads a chunk with require and fails with “ReferenceError: require is not defined”. This was not compared with a browser.
  3. ENOPRO for file:///.copilot/config.json and the .copilot, .claude and .agents folders: the workbench’s own rejections when no folder is open, which make niso-run and niso-node exit with status 1.
  4. The extension host: nothing logs a failure, but the driver doesn’t check language features, so whether it runs is not known.

Fixed since the previous list: HTMLTextAreaElement.setSelectionRange, Worker (no run falls back to the main thread with “Could not create web worker(s)” any more, and the language detection worker runs), globalThis.origin, CanvasRenderingContext2D’s drawing calls (the overview ruler and minimap draw), and Intl.

Before Node and Bun (2026-11-08 to 2026-11-09, QuickJS only)

Section titled “Before Node and Bun (2026-11-08 to 2026-11-09, QuickJS only)”

Syntax highlighting worked (2026-11-08, the WebAssembly branch). On the main thread, vscode-oniguruma’s onig.wasm (466 KB) instantiates on wasmi through WebAssembly.instantiate, and the TextMate grammar tokenizes the typed file:

mtk23="function" mtk27="greet" mtk25="name" mtk22="// say hello" mtk17="return"
mtk29="\"Hello, \"" mtk3="+" mtk7="42"

The theme’s CSS colors those classes (keyword #cf222e, function name #8250df, comment #6e7781, string #0a3069, number #098658). The tokenization worker cannot start (no Worker), so VS Code tokenizes on the main thread.

Next, in the order they matter for editing:

  1. Fixed: blitz panicked with “invalid SlotMap key used” in layout/mod.rs:160 when offsetWidth/offsetHeight/getBoundingClientRect ran after the editor opened (about 200 times a run). The tab’s .tab-fill is a containing block that turns display: none and then loses a ::before; blitz keeps the freed pseudo-element in the hidden block’s hoisted list (repros/blitz-dom-hidden-containing-block-keeps-hoisted), and niso now drops freed boxes from those lists and lays out again. Not fully: rechecked on plat-workers after merging main (4071d11), the panic still happens in some runs, on main too (two runs each: main 0 and 2 panics, plat-workers 0 and 6), still at layout/mod.rs:160 from an offsetHeight read. A debug niso-run also needs a 64 MB main-thread stack (ulimit -s 65520) or wasmi overflows it while onig.wasm runs.
  2. HTMLTextAreaElement.setSelectionRange is not a function, so the editor’s hidden textarea throws after each edit (the edit itself lands).
  3. Fixed: Intl was not defined (QuickJS has none), which broke the highlighter in one contribution. With niso-intl (docs/engines/intl.md), rechecked on 2026-11-09, nothing in the run touches Intl and fails, and the typed file’s tokens carry bracket pair colors (bracket-highlighting-0 on the parentheses and braces).
  4. No Worker: the editor and tokenization workers fall back to the main thread, and the extension host does not start (so no language features).
  5. IndexedDB, as before.
  6. globalThis.origin is undefined, so the worker factory (tLr) throws “cannot read property ‘length’ of undefined” before it gets as far as new Worker, for the tokenizer and the language detection workers.
  7. CanvasRenderingContext2D has no fillRect, so the editor’s overview ruler throws “not a function” each time it renders.

Unmodified, the first screen renders: the activity bar, the Explorer with “No Folder Opened” and its Open Folder / Open Recent buttons, the Outline and Timeline panes, the Welcome editor (Start, Recent, Walkthroughs), the panel and the status bar.

Blob, AbortController and crypto.subtle no longer fail: the Blob rejection, the crypto.subtle rejection (“webviews will not work”) and the two AbortController errors are gone. On 2026-11-08, with the standalone build and an index.html that only creates the workbench (its builtin extensions in the vscode-workbench-builtin-extensions meta), the dumped DOM has 338 divs both before and after these APIs, and the run still exits with status 1. Two rejections remain unhandled: Worker is not defined (the web worker extension host, the same blocker as the Svelte playground), and ENOPRO for file:///.copilot/config.json, which the workbench rejects itself when no folder is open. With main’s fetch, the theme JSON and the other resources it fetches load from the served directory.

# What failed Cause Fix
1 customElements.get("connection-observer") threw at startup No Custom Elements Custom Elements in the core (api/custom_elements.rs): definitions, upgrades, the reaction queues run when each C API call returns, the HTMLElement constructor. The binding does the steps of define that read the constructor.
2 crypto.randomUUID undefined No Web Crypto crypto.getRandomValues and randomUUID (idl/webcrypto.webidl); new QuotaExceededError and TypeMismatchError paths.
3 performance.getEntriesByType is not a function Performance was a stub in the glue The Performance timeline and User Timing in the core (api/timing.rs, idl/performance.webidl).
4 CSS.escape undefined No CSS namespace CSS.escape and CSS.supports (stylo’s parser), plus WebIDL namespaces in the generator.
5 canvas.getContext (pixel ratio probe) No HTMLCanvasElement Main’s HTMLCanvasElement and 2D context replaced the width/height reflection this effort added.
6 this.element.dataset.activeComposite = … No dataset DOMStringMap backed by data-* attributes, through new generator support for anonymous named getters, setters and deleters and [LegacyOverrideBuiltIns].
7 WebAssembly is not defined, so onig.wasm never started and nothing was highlighted No WebAssembly, and fetch’s Response kept bodies as strings The WebAssembly JS API on wasmi (docs/platform/webassembly.md), and binary response bodies.
8 Blob is not defined, an unhandled rejection No File API Blob, File, FileReader and blob URLs in the binding, with fetch’s Request and byte bodies.
9 AbortController is not defined (a workbench contribution and the chat control request) No aborting AbortController and AbortSignal in the core (api/abort.rs), with fetch’s and addEventListener’s signal.
10 'crypto.subtle' is not available so webviews will not work, an unhandled rejection No SubtleCrypto crypto.subtle on AWS-LC (crates/niso-crypto), in secure contexts, which http://localhost is.

Remaining errors before the editor opens (none blocks the first screen)

Section titled “Remaining errors before the editor opens (none blocks the first screen)”
  • Workers: on plat-workers the LocalWebWorker extension host did not start because it hashes its iframe’s source with crypto.subtle before it makes a worker, and Monaco fell back to running its editor workers on the main thread because it creates them from blob: URLs and niso had no Blob. Both now exist after the merge of plat-workers into plat-blob-abort-crypto, and a window and its workers share one blob URL store, so a blob URL made in any of them resolves in all of them until any of them revokes it, or the worker that made it ends (closes or is terminated). Tested (crates/niso-quickjs/tests/messaging.rs): a classic new Worker of a blob URL the window made, importScripts and fetch of the window’s blob URLs inside that worker, a blob URL the worker made loading as an <img> src in the window, and a worker revoking a window’s blob URL so the window’s fetch of it fails, and terminated or closed workers’ blob URLs failing to load in the window. A module worker (type: 'module') from a blob URL, and a module script’s imports of blob URLs, are untested. The workbench has not been rerun since.
  • The driver’s typing into an untitled file stops at HTMLTextAreaElement.setSelectionRange, which niso does not have; after it, an offsetHeight read panics in blitz’s layout (“invalid SlotMap key used”). The task fails and the run goes on.
  • IndexedDB works now: VS Code opens its storage databases (SQLite files in the storage directory), and the seven IndexedDB errors it logged without it are gone.
  • Customized built-in elements (extends) are rejected with NotSupportedError, and adoptedCallback never runs because niso has one document.
  • The wrapper of a custom element is held for as long as the binding lives, because a collected wrapper would lose its class and constructor state. Custom elements that pages discard are never freed.
  • Blob has no stream() and Blob-constructor reads of detached buffers do not throw: niso has no ReadableStream.
  • Whether a page is a secure context, and so whether crypto.subtle, SubtleCrypto, CryptoKey and crypto.randomUUID exist, follows the document’s URL until the page’s own first script runs, and is frozen from then on, because niso’s realm exists before the document’s URL is known. A host script that runs before the host sets the URL sees what about:blank gives, and the members change when the URL does. Only localhost and loopback addresses count as trustworthy hosts; names under .localhost do not, because niso’s network resolves them like any other name.
  • SubtleCrypto’s work runs in the task that settles its promise, on the event loop’s thread, so a long PBKDF2 holds the page meanwhile.