Skip to content

feat: VS Code web extension POC (vscode.dev support)#340

Draft
Copilot wants to merge 3 commits intomainfrom
copilot/make-vscode-extension-web-compatible
Draft

feat: VS Code web extension POC (vscode.dev support)#340
Copilot wants to merge 3 commits intomainfrom
copilot/make-vscode-extension-web-compatible

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 26, 2026

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 with target: 'webworker', libraryTarget: 'commonjs' (client) and libraryTarget: 'var' + library: 'serverExportVar' (server), plus path-browserify fallback.
  • src/client.browser.ts — Browser extension entry point. Uses vscode-languageclient/browser, resolves the worker via Uri.joinPath(context.extensionUri, …) (required; asAbsolutePath is unavailable in the browser host), and includes vscode-vfs document selectors for vscode.dev/github.dev.
    const serverUri = Uri.joinPath(context.extensionUri, 'lsp', 'server.browser.js');
    Manager.Client = new LanguageClient('…', '…', clientOptions, new Worker(serverUri.toString(true)));
  • src/server.browser.ts — Thin wrapper over setupServerBrowser.
  • ide/base/server/src/lsp/server/setup.browser.ts — Browser server setup replacing Node IPC with BrowserMessageReader / BrowserMessageWriter from vscode-languageserver/browser.

Modified files

  • ide/vscode/package.json — Adds "browser": "./lsp/client.browser.js", "web" to extensionKind, new webpack:browser / chrome scripts, @vscode/test-web and path-browserify devDeps.
  • ide/base/server/src/index.ts — Re-exports setup.browser so setupServerBrowser is available from the bc-minecraft-lsp package.
Original prompt

Goal

Make the VS Code extension in ide/vscode work in the vscode.dev browser environment by following the official VS Code Web Extensions guide and the official lsp-web-extension-sample as reference.

This is a POC — the existing desktop behaviour must remain 100% unchanged.


Background / Key Constraints

The official guide requires:

  1. A "browser" field in package.json pointing to a separate web-bundle entry point.
  2. "extensionKind": ["workspace", "web"] (add "web").
  3. Both the client and server webpack bundles use target: "webworker" (not "node").
  4. The browser client uses LanguageClient from vscode-languageclient/browser and passes a Worker as the 4th constructor argument (no serverOptions object).
  5. The browser server uses BrowserMessageReader / BrowserMessageWriter from vscode-languageserver/browser instead of Node IPC.
  6. Uri.joinPath(context.extensionUri, ...) must be used (not context.asAbsolutePath) to resolve the worker URL in browser context.
  7. Server webpack output must use libraryTarget: 'var' + library: 'serverExportVar' (matching the official sample).
  8. Client webpack output uses libraryTarget: 'commonjs'.
  9. path-browserify is needed as a fallback for the path Node built-in in both browser webpack configs.
  10. Testing uses @vscode/test-web.

Changes Required

1. ide/vscode/package.json

  • Add "browser": "./lsp/client.browser.js" field (alongside the existing "main": "./lsp/client.js").
  • Add "web" to the existing "extensionKind": ["workspace"] array → ["workspace", "web"].
  • Add new npm scripts:
    "webpack:client:browser": "webpack --mode production --config ./webpack.client.browser.js",
    "webpack:server:browser": "webpack --mode production --config ./webpack.server.browser.js",
    "webpack:browser": "npm run webpack:client:browser && npm run webpack:server:browser",
    "chrome": "npm run webpack:browser && vscode-test-web --browserType=chromium --extensionDevelopmentPath=. ."
  • Update "webpack" script to also run npm run webpack:browser:
    "webpack": "npm run webpack:client && npm run webpack:server && npm run webpack:browser"
  • Add to "devDependencies":
    "@vscode/test-web": "^0.0.51",
    "path-browserify": "^1.0.1"

2. New file: ide/vscode/webpack.client.browser.js

Create a new webpack config for the browser client bundle. Match the style of the existing webpack.client.js but with target: 'webworker', libraryTarget: 'commonjs', and path-browserify fallback:

//@ts-check
'use strict';

const path = require('path');

/**@type {import('webpack').Configuration}*/
const config = {
  context: path.join(__dirname),
  mode: 'none',
  target: 'webworker', // web extensions run in a webworker context

  entry: './src/client.browser.ts',
  output: {
    path: path.resolve(__dirname, 'lsp'),
    filename: 'client.browser.js',
    libraryTarget: 'commonjs',
    devtoolModuleFilenameTemplate: '../[resource-path]',
  },
  devtool: 'nosources-source-map',
  externals: {
    vscode: 'commonjs vscode',
  },
  resolve: {
    mainFields: ['module', 'main'],
    extensions: ['.ts', '.js'],
    fallback: {
      path: require.resolve('path-browserify'),
    },
  },
  module: {
    rules: [
      { test: /\.ts$/, exclude: /node_modules/, use: [{ loader: 'ts-loader' }] },
    ],
  },
  performance: {
    hints: false,
  },
};
module.exports = config;

3. New file: ide/vscode/webpack.server.browser.js

Create a new webpack config for the browser server (Web Worker) bundle. Use libraryTarget: 'var' and library: 'serverExportVar' exactly as the official sample does:

//@ts-check
'use strict';

const path = require('path');

/**@type {import('webpack').Configuration}*/
const config = {
  context: path.join(__dirname),
  mode: 'none',
  target: 'webworker', // web extensions run in a webworker context

  entry: './src/server.browser.ts',
  output: {
    path: path.resolve(__dirname, 'lsp'),
    filename: 'server.browser.js',
    libraryTarget: 'var',
    library: 'serverExportVar',
    devtoolModuleFilenameTemplate: '../[resource-path]',
  },
  devtool: 'nosources-source-map',
  externals: {
    vscode: 'commonjs vscode',
  },
  resolve: {
    mainFields: ['module', 'main'],
    extensions: ['.ts', '.js'],
    fallback: {
      path: require.resolve('path-browserify'),
    },
  },
  module: {
    rules: [
      { test: /\.ts$/, exclude: /node_modules/, use: [{ loader: 'ts-loader' }] },
    ],
  },
  performance: {
    hints: false,
  },
};
module.exports = config;

4. New file: ide/vscode/src/client.browser.ts

This is the web entry point for the extension client. It mirrors ide/vscode/src/client.ts b...

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.

Copilot AI changed the title [WIP] Make VS Code extension work in vscode.dev browser environment feat: VS Code web extension POC (vscode.dev support) Mar 26, 2026
Copilot AI requested a review from DaanV2 March 26, 2026 19:26
@DaanV2 DaanV2 requested a review from Copilot March 29, 2026 12:45
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/browser message reader/writer plumbing and exposes it via bc-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';
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
import { setupServerBrowser } from 'bc-minecraft-lsp';
import { setupServerBrowser } from 'bc-minecraft-lsp/browser';

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +5
import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient/browser';
import { Languages } from '@blockception/ide-shared';
import { Manager } from 'bc-minecraft-lsp-client';
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
// 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));
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
const worker = new Worker(serverUri.toString(/* skipEncoding= */ true));
const worker = new (globalThis as any).Worker(
serverUri.toString(/* skipEncoding= */ true),
);

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +38
// self is the DedicatedWorkerGlobalScope inside the Web Worker
const messageReader = new BrowserMessageReader(self as DedicatedWorkerGlobalScope);
const messageWriter = new BrowserMessageWriter(self as DedicatedWorkerGlobalScope);

Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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);

Copilot uses AI. Check for mistakes.
mainFields: ['module', 'main'],
extensions: ['.ts', '.js'],
fallback: {
path: require.resolve('path-browserify'),
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
path: require.resolve('path-browserify'),
path: require.resolve('path-browserify'),
fs: false,

Copilot uses AI. Check for mistakes.
Comment on lines 1 to +2
export * from './lsp/server/setup';
export * from './lsp/server/setup.browser';
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
export * from './lsp/server/setup';
export * from './lsp/server/setup.browser';
export * from './lsp/server/setup.browser';

Copilot uses AI. Check for mistakes.
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants