feat: VS Code web extension POC (vscode.dev support)#340
feat: VS Code web extension POC (vscode.dev support)#340
Conversation
Agent-Logs-Url: https://github.com/Blockception/minecraft-bedrock-language-server/sessions/d2effceb-3b82-4158-a38a-5fca2eb56eba Co-authored-by: DaanV2 <2393905+DaanV2@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a proof-of-concept “web” build path so the VS Code extension in ide/vscode can run in the vscode.dev browser extension host, using a web-worker based language server transport.
Changes:
- Introduces browser-targeted webpack builds for the extension client and LSP server worker.
- Adds browser-specific extension activation and server bootstrap entrypoints.
- Adds a browser LSP server setup using
vscode-languageserver/browsermessage reader/writer plumbing and exposes it viabc-minecraft-lsp.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| ide/vscode/webpack.server.browser.js | New webpack config for bundling the LSP server for webworker. |
| ide/vscode/webpack.client.browser.js | New webpack config for bundling the extension client for webworker. |
| ide/vscode/src/server.browser.ts | New web-worker server entrypoint calling setupServerBrowser. |
| ide/vscode/src/client.browser.ts | New browser/web extension entrypoint using vscode-languageclient/browser and Worker. |
| ide/vscode/package.json | Adds browser entrypoint, web extensionKind, and scripts/devDeps for browser builds + @vscode/test-web. |
| ide/base/server/src/lsp/server/setup.browser.ts | New browser LSP setup using BrowserMessageReader/Writer instead of Node IPC. |
| ide/base/server/src/index.ts | Re-exports browser setup from the bc-minecraft-lsp package. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -0,0 +1,4 @@ | |||
| import { setupServerBrowser } from 'bc-minecraft-lsp'; | |||
There was a problem hiding this comment.
This imports setupServerBrowser from the bc-minecraft-lsp package root, which (via export *) also brings in the Node server setup and its Node-only dependencies into the web worker bundle. Import from a browser-only entrypoint/module instead (or split the package exports) so webpack doesn’t include vscode-languageserver/node and other Node code in the browser build.
| import { setupServerBrowser } from 'bc-minecraft-lsp'; | |
| import { setupServerBrowser } from 'bc-minecraft-lsp/browser'; |
ide/vscode/src/client.browser.ts
Outdated
| import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient/browser'; | ||
| import { Languages } from '@blockception/ide-shared'; | ||
| import { Manager } from 'bc-minecraft-lsp-client'; |
There was a problem hiding this comment.
bc-minecraft-lsp-client's Manager imports LanguageClient from vscode-languageclient/node (see ide/base/client/src/manager/manager.ts), which will pull Node-only code into the web bundle and typically breaks when targeting webworker. For the web entrypoint, avoid importing Manager from the Node client package; instead keep the LanguageClient instance in a local module-level variable (or add a browser-safe Manager that only uses import type).
ide/vscode/src/client.browser.ts
Outdated
| // Use Uri.joinPath on context.extensionUri — NOT context.asAbsolutePath — | ||
| // because asAbsolutePath is not available in the browser extension host. | ||
| const serverUri = Uri.joinPath(context.extensionUri, 'lsp', 'server.browser.js'); | ||
| const worker = new Worker(serverUri.toString(/* skipEncoding= */ true)); |
There was a problem hiding this comment.
This file uses the global Worker type, but the repo’s TS base config only includes lib: ["es2023"] (no dom/webworker), so type-checking this entrypoint will fail. Add a browser-specific tsconfig/ts-loader config for the web bundles (with lib including dom/webworker), or avoid relying on DOM types by casting via globalThis as any.
| const worker = new Worker(serverUri.toString(/* skipEncoding= */ true)); | |
| const worker = new (globalThis as any).Worker( | |
| serverUri.toString(/* skipEncoding= */ true), | |
| ); |
| // self is the DedicatedWorkerGlobalScope inside the Web Worker | ||
| const messageReader = new BrowserMessageReader(self as DedicatedWorkerGlobalScope); | ||
| const messageWriter = new BrowserMessageWriter(self as DedicatedWorkerGlobalScope); | ||
|
|
There was a problem hiding this comment.
This file references self and DedicatedWorkerGlobalScope, but the repo’s TS config doesn’t include the webworker lib, so these names will not type-check. Consider adding a browser/worker-specific tsconfig for the server web bundle or using globalThis with a narrow any cast to avoid depending on missing lib types.
| // self is the DedicatedWorkerGlobalScope inside the Web Worker | |
| const messageReader = new BrowserMessageReader(self as DedicatedWorkerGlobalScope); | |
| const messageWriter = new BrowserMessageWriter(self as DedicatedWorkerGlobalScope); | |
| interface BrowserMessageEndpoint { | |
| addEventListener: (...args: any[]) => void; | |
| removeEventListener: (...args: any[]) => void; | |
| postMessage: (...args: any[]) => void; | |
| } | |
| const workerScope = globalThis as unknown as BrowserMessageEndpoint; | |
| const messageReader = new BrowserMessageReader(workerScope); | |
| const messageWriter = new BrowserMessageWriter(workerScope); |
ide/vscode/webpack.server.browser.js
Outdated
| mainFields: ['module', 'main'], | ||
| extensions: ['.ts', '.js'], | ||
| fallback: { | ||
| path: require.resolve('path-browserify'), |
There was a problem hiding this comment.
The browser server bundle will include code paths that import Node built-ins like fs (e.g. ide/base/server/src/lsp/documents/io.ts and others). With webpack 5 targeting webworker, Node built-ins aren’t polyfilled, and this config only provides a path fallback—so the build will fail resolving fs (and potentially other built-ins). Either refactor the server code used in web to avoid Node APIs, or explicitly mark unsupported built-ins as false/external and provide web-compatible implementations.
| path: require.resolve('path-browserify'), | |
| path: require.resolve('path-browserify'), | |
| fs: false, |
| export * from './lsp/server/setup'; | ||
| export * from './lsp/server/setup.browser'; |
There was a problem hiding this comment.
Importing setupServerBrowser from bc-minecraft-lsp goes through ide/base/server/src/index.ts, which now re-exports the Node setupServer module as well. That causes the browser server bundle to pull in vscode-languageserver/node (and other Node-only dependencies) even though only the browser setup is needed. Expose a separate browser-only entry/module (or conditional exports) so the web worker bundle never imports the Node setup path.
| export * from './lsp/server/setup'; | |
| export * from './lsp/server/setup.browser'; | |
| export * from './lsp/server/setup.browser'; |
…372) `setup.browser.ts` was a near-complete clone of `setup.ts`, `client.browser.ts` inlined all client wiring that `bc-minecraft-lsp-client` should own, and two near-identical webpack configs existed for browser builds. ## Changes ### Server — extract `setupServerCore` - **New** `ide/base/server/src/lsp/server/setup.core.ts` exports `setupServerCore(connection: Connection, config: LSPConfig)` containing all shared service wiring, lifecycle handlers, and `connection.listen()`. `Connection` is imported from `vscode-languageserver` (base) so both Node and Browser connections satisfy it. - `setup.ts` and `setup.browser.ts` are now thin wrappers that only differ in how they construct the connection: ```typescript // setup.ts export function setupServer(config: LSPConfig): void { const connection = createConnection(ProposedFeatures.all); // Node IPC setupServerCore(connection, config); } // setup.browser.ts export function setupServerBrowser(config: LSPConfig): void { const messageReader = new BrowserMessageReader(self as DedicatedWorkerGlobalScope); const messageWriter = new BrowserMessageWriter(self as DedicatedWorkerGlobalScope); const connection = createConnection(ProposedFeatures.all, messageReader, messageWriter); setupServerCore(connection, config); } ``` ### Client — add `setupClientBrowser` to `bc-minecraft-lsp-client` - **New** `ide/base/client/src/client/client.browser.ts` exports `setupClientBrowser(context)`, mirroring how `setupClient` works for desktop. - Re-exported from `ide/base/client/src/index.ts`. - `ide/vscode/src/client.browser.ts` reduced to the same thin-wrapper pattern as `client.ts`. ### Webpack — merge browser configs - **Deleted** `webpack.client.browser.js` and `webpack.server.browser.js`. - **New** `webpack.browser.js` uses a `makeBrowserConfig()` factory and exports both configurations as an array. - `package.json` scripts: removed `webpack:client:browser` / `webpack:server:browser`; `webpack:browser` now points directly at `webpack.browser.js`. <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> ## Goal Refactor the code introduced in PR #340 (branch `copilot/make-vscode-extension-web-compatible`) to be DRY. All changes must be made **on the existing branch** `copilot/make-vscode-extension-web-compatible`, not on `main`. Desktop behaviour must remain completely unchanged. --- ## Change 1 — Extract `setupServerCore` to eliminate the duplicate between `setup.ts` and `setup.browser.ts` `ide/base/server/src/lsp/server/setup.browser.ts` (added in PR #340) is a near-complete clone of `ide/base/server/src/lsp/server/setup.ts`. The only difference is how the `connection` is created. ### Create `ide/base/server/src/lsp/server/setup.core.ts` Extract ALL shared logic (service construction, manager wiring, `onInitialize`, `onInitialized`, `onShutdown`, `onExit`, `connection.listen()`) into a new exported function `setupServerCore`: ```typescript import { Connection, BulkRegistration, InitializeResult, ProposedFeatures } from 'vscode-languageserver'; import { CodeActionService } from '../code-action/service'; import { CodeLensService } from '../code-lens/service'; import { CommandService } from '../commands/service'; import { DataSetService } from '../dataset/service'; import { CompletionService } from '../completion/service'; import { ConfigurationService } from '../configuration/service'; import { Database } from '../database/database'; import { DiagnoserService } from '../diagnostics/service'; import { DocumentManager, IDocumentManager } from '../documents/manager'; import { ExtensionContext } from '../extension'; import { FormatService } from '../format/service'; import { ExtendedLogger } from '../logger/logger'; import { DocumentProcessor, PackProcessor, WorkspaceProcessor } from '../process'; import { DefinitionService, ImplementationService, ReferenceService, TypeDefinitionService } from '../references'; import { SemanticsServer } from '../semantics/service'; import { CapabilityBuilder } from '../services/capabilities'; import { ServiceManager } from '../services/collection'; import { SignatureService } from '../signatures/service'; import { DocumentSymbolService } from '../symbols/document-service'; import { WorkspaceSymbolService } from '../symbols/workspace-service'; import { LSPConfig } from '../config/config'; export function setupServerCore(connection: Connection, config: LSPConfig): void { const logger = new ExtendedLogger(connection.console); const manager = new ServiceManager(logger); const extension = new ExtensionContext(config, connection, manager, logger, {} as IDocumentManager, {} as Database); const documents = new DocumentManager(logger, extension); const database = new Database(logger, documents); extension.documents = documents; extension.database = database; const diagnoserService = new DiagnoserService(logger, extension, documents); const documentProcessor = new DocumentProcessor(logger, extension, diagnoserService); const packProcessor = new PackProcessor(logger, extension, documentProcessor); const workspaceProcessor = new WorkspaceProcessor(logger, extension, packProcessor); manager .add(new ConfigurationService(logger, extension), documents, database) .add( diagnoserService, documentProcessor, packProcessor, workspaceProcessor, new CodeActionService(logger, extension), new CodeLensService(logger, extension), new CommandService(logger, extension), new DataSetService(logger, extension), new CompletionService(logger, extension), new DefinitionService(logger, extension), new DocumentSymbolService(logger, extension), new FormatService(logger, extension), new ImplementationService(logger, extension), new ReferenceService(logger, extension), new SemanticsServer(logger, extension), new SignatureService(logger, extension), new TypeDefinitionService(logger, extension), new WorkspaceSymbolService(logger, extension), ); logger.info('starting minecraft server'); connection.onInitialize((params, token, workDoneProgress) => { workDoneProgress.begin('initializing', 0); logger.info('Initializing minecraft server', { version: config.version }); const result: InitializeResult = { serverInfo: { name: 'bc-minecraft-language-server', version: config.version }, capabilities: {}, }; const builder = new CapabilityBuilder(result.capabilities); extension.parseClientCapabilities(params.capabilities); manager.onInitialize(builder, params, token, workDoneProgress); result.capabilities = builder.result(); workDoneProgress.done(); return result; }); connection.onInitialized(async () => { logger.info('Initialized minecraft server', { version: config.version }); manager.setupHandlers(connection); const register = BulkRegistration.create(); manager.dynamicRegister(register); await connection.client.register(register); return manager.start(); }); connection.onShutdown((... </details> <!-- START COPILOT CODING AGENT SUFFIX --> *This pull request was created from Copilot chat.* > <!-- START COPILOT CODING AGENT TIPS --> --- ⚡ Quickly spin up Copilot coding agent tasks from anywhere on your macOS or Windows machine with [Raycast](https://gh.io/cca-raycast-docs). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: DaanV2 <2393905+DaanV2@users.noreply.github.com>
Adds the minimal plumbing needed to run the extension in the vscode.dev browser environment, following the VS Code Web Extensions guide. Desktop behaviour is completely unchanged.
New files
webpack.client.browser.js/webpack.server.browser.js— Webpack configs withtarget: 'webworker',libraryTarget: 'commonjs'(client) andlibraryTarget: 'var'+library: 'serverExportVar'(server), pluspath-browserifyfallback.src/client.browser.ts— Browser extension entry point. Usesvscode-languageclient/browser, resolves the worker viaUri.joinPath(context.extensionUri, …)(required;asAbsolutePathis unavailable in the browser host), and includesvscode-vfsdocument selectors for vscode.dev/github.dev.src/server.browser.ts— Thin wrapper oversetupServerBrowser.ide/base/server/src/lsp/server/setup.browser.ts— Browser server setup replacing Node IPC withBrowserMessageReader/BrowserMessageWriterfromvscode-languageserver/browser.Modified files
ide/vscode/package.json— Adds"browser": "./lsp/client.browser.js","web"toextensionKind, newwebpack:browser/chromescripts,@vscode/test-webandpath-browserifydevDeps.ide/base/server/src/index.ts— Re-exportssetup.browsersosetupServerBrowseris available from thebc-minecraft-lsppackage.Original prompt
Goal
Make the VS Code extension in
ide/vscodework in the vscode.dev browser environment by following the official VS Code Web Extensions guide and the officiallsp-web-extension-sampleas reference.This is a POC — the existing desktop behaviour must remain 100% unchanged.
Background / Key Constraints
The official guide requires:
"browser"field inpackage.jsonpointing to a separate web-bundle entry point."extensionKind": ["workspace", "web"](add"web").target: "webworker"(not"node").LanguageClientfromvscode-languageclient/browserand passes aWorkeras the 4th constructor argument (noserverOptionsobject).BrowserMessageReader/BrowserMessageWriterfromvscode-languageserver/browserinstead of Node IPC.Uri.joinPath(context.extensionUri, ...)must be used (notcontext.asAbsolutePath) to resolve the worker URL in browser context.libraryTarget: 'var'+library: 'serverExportVar'(matching the official sample).libraryTarget: 'commonjs'.path-browserifyis needed as a fallback for thepathNode built-in in both browser webpack configs.@vscode/test-web.Changes Required
1.
ide/vscode/package.json"browser": "./lsp/client.browser.js"field (alongside the existing"main": "./lsp/client.js")."web"to the existing"extensionKind": ["workspace"]array →["workspace", "web"]."webpack"script to also runnpm run webpack:browser:"devDependencies":2. New file:
ide/vscode/webpack.client.browser.jsCreate a new webpack config for the browser client bundle. Match the style of the existing
webpack.client.jsbut withtarget: 'webworker',libraryTarget: 'commonjs', andpath-browserifyfallback:3. New file:
ide/vscode/webpack.server.browser.jsCreate a new webpack config for the browser server (Web Worker) bundle. Use
libraryTarget: 'var'andlibrary: 'serverExportVar'exactly as the official sample does:4. New file:
ide/vscode/src/client.browser.tsThis is the web entry point for the extension client. It mirrors
ide/vscode/src/client.tsb...This pull request was created from Copilot chat.
⚡ Quickly spin up Copilot coding agent tasks from anywhere on your macOS or Windows machine with Raycast.