Enable running the VS Code extension host on the workspace (Node.js) by default#3093
Enable running the VS Code extension host on the workspace (Node.js) by default#3093joao-boechat wants to merge 16 commits intomainfrom
Conversation
There was a problem hiding this comment.
now that we're just using the web target, should I simplify the code to account for that, or do I leave the structure as is in case we want to add the nodejs target back?
|
Since web workers was touched, I just wanted to double-check that the worker logic responsible for state-viz calculation on the circuit editor was still functional. I tested it and it seems to be working fine, but is there any reason to be concerned about that? |
|
@ScottCarda-MS I don't see any of the worker code I touched being referenced there. I think that the worker code in state-viz is named that way because it is supposed to be agnostic (browser and worker), so it avoids using API that's only available in browser. But my modifications shouldn't affect that part! |
Summary
This PR makes the Q# VS Code extension run its extension host primarily on the workspace side (Node.js) rather than on the UI side (browser). This is important for remote development scenarios — WSL, SSH, Codespaces, Dev Containers — where keeping the extension host close to the workspace avoids bugs and simplifies the architecture. The extension still supports running on the UI side for VS Code for Web.
To make this work, the Node.js entry point of the
qsharp-langnpm package needed to function correctly. Rather than fixing the separate Node.js codepath, we unified both entry points into a single platform-agnostic module, using theweb-workerpackage to abstract away Worker API differences between browsers and Node.js.Motivation
The Q# extension was originally built as a web-first extension, meaning the extension host always ran on the UI side (browser), even in desktop VS Code. This caused issues in remote development scenarios (WSL, SSH, Codespaces, Dev Containers) where the extension host needs to run close to the workspace for correct behavior. By making the Node.js entry point the default, the extension host runs on the workspace side, avoiding these issues.
Additionally, the npm package previously maintained two parallel codepaths —
browser.tsandmain.ts— each with its own wasm loading strategy, worker proxy implementation, and per-service worker scripts. The build system also produced two separate wasm-bindgen targets (webandnodejs) and used npm conditional exports to route consumers to the right entry point. The Node.js path was incomplete and buggy, and the duplication made changes error-prone. Unifying these into a single platform-agnostic entry point simplifies the codebase and eliminates this class of bugs.Architecture (of qsharp-lang)
Before
graph TD subgraph "npm package entry points" B["browser.ts (browser entry)"] M["main.ts (Node.js entry)"] end B --> WB["workers/browser.ts"] M --> WN["workers/node.ts"] WB --> CWB["compiler/worker-browser.ts"] WB --> LSWB["language-service/worker-browser.ts"] WB --> DSWB["debug-service/worker-browser.ts"] WN --> CWN["compiler/worker-node.ts"] WN --> LSWN["language-service/worker-node.ts"] WN --> DSWN["debug-service/worker-node.ts"] CWB --> WASMW["lib/web/qsc_wasm.js"] CWN --> WASMN["lib/nodejs/qsc_wasm.cjs"]After
graph TD subgraph "npm package entry point" M["main.ts (single entry)"] end M --> WM["workers/main.ts (proxy, uses web-worker pkg)"] WW["workers/worker.ts (runs inside worker)"] WW --> CW["compiler/worker.ts"] WW --> LSW["language-service/worker.ts"] WW --> DSW["debug-service/worker.ts"] CW --> WASM["lib/web/qsc_wasm.js"] LSW --> WASM DSW --> WASM WM -.->|spawns| CW WM -.->|spawns| LSW WM -.->|spawns| DSWChanges
qsharp-langnpm package (source/npm/qsharp/)browser.tsand merged all functionality intomain.ts— a single platform-agnostic entry point.worker-browser.ts/worker-node.ts) into a singleworker.tseach.workers/browser.tsandworkers/node.tswithworkers/worker.ts(worker-side) andworkers/main.ts(main-thread proxy using theweb-workerpackage).common-exports.tsfor shared re-exports.webwasm-bindgen target is used; thenodejstarget is no longer needed.package.jsonexports — removed conditionalbrowser/node/defaultrouting.web-worker(^1.5.0) as a dependency.async(getCompiler(),getDebugService(),getLanguageService(),getProjectLoader()), and worker factory functions now require an explicit worker path argument.VS Code extension (
source/vscode/)__PLATFORM__build-time constant ("browser"or"node") to resolve the correct worker script paths at runtime.extension.ts) and the worker entry points (compilerWorker.ts,debug-service-worker.ts).wasm/), which is the main reason the overall VSIX size only grew from 3.9 MB to 4.09 MB despite adding a full Node.js bundle.web-workerpackage must be marked as an external dependency in the Node.js esbuild bundle. This is becauseweb-workerperforms an internal runtime check that breaks if its code is inlined into the bundle. As a result, it is copied into the extension'snode_modules/so it can be resolved at runtime (e.g. when installed from a VSIX)../out/node/extension.js.Platform,UI Kind, andRemotename during activation for diagnostics.Build system (
build.py)webwasm-bindgen target is built. Removed thenodejstarget and its.js→.cjsrenaming logic.Tests (
source/npm/qsharp/test/)loadWasmModule, async service getters, explicit worker paths).Extra