Cross-Extension Code Sharing in VS Code Monorepos: The Activate-Exports Pattern
TLDR
Developing a multi-extension suite in a monorepo (like pnpm workspaces) relies on local package symlinks. However, once published to the VS Code Marketplace, each.vsix extension is unpacked into an isolated directory without a shared node_modules. To share code across extensions in production, you must use extensionDependencies in package.json combined with VS Code's runtime vscode.extensions.getExtension('publisher.id').exports API.
| Execution Context | Module Resolution Method | Dependency Boundary | Type Safety Strategy |
|---|---|---|---|
| Dev Host (F5) | pnpm workspace symlinks (packages/core) | Shared node_modules | Standard import statements |
| Marketplace Production | vscode.extensions.getExtension().exports | Sandboxed extension dir | Type-only imports (import type) |
Published VS Code extensions lack shared workspace symlinks
Our extension suite consists of five packages in a pnpm workspace:
extension/ ├── pnpm-workspace.yaml └── packages/ ├── core/ ← Shared: AuthProvider, GinexysRouter, html-rewriter ├── tafne/ ← Satellite tool extension ├── pdf/ ← Satellite tool extension ├── schema/ ← Satellite tool extension └── pack/ ← Extension pack umbrella manifest
In development, satellite extensions imported core code directly:
import { rewriteHtmlForWebview } from 'core/webview/html-rewriter'; This compiled cleanly in dev because pnpm symlinked packages/core into tafne/node_modules/core.
However, when published to the Marketplace, VS Code unpacks ginexys-tafne into its own isolated directory (~/.vscode/extensions/ginexys.ginexys-tafne-0.1.0/). Because VS Code extensions do not share node_modules, require("core/webview/html-rewriter") throws Cannot find module 'core/...' the moment a user opens a webview.
Extension sandboxes isolate node_modules dependencies
VS Code places every installed extension into an isolated classloader scope. Even if a user installs ginexys-core and ginexys-tafne side by side, tafne cannot access core files on disk via standard relative or Node module imports.
Activate-exports exposes public interfaces at runtime
VS Code Runtime Host ginexys-core Extension ginexys-tafne Satellite
| | |
|--- 1. Activate core ---------->| |
| (extensionDependencies) | |
|<-- 2. Return GinexysCoreApi ---| |
| | |
|--- 3. Activate satellite --------------------------------------->|
| | |
|<-- 4. getExtension('core') --------------------------------------|
|--- 5. Return active core --------------------------------------->|
| | |
|--- 6. Access exports (router, authProvider, html rewriter) ----->|
Declaring extensionDependencies guarantees activation order
Declare the dependency in the satellite extension's manifest to guarantee VS Code installs and activatesginexys-core first:
{
"name": "ginexys-tafne",
"publisher": "ginexys",
"extensionDependencies": [
"ginexys.ginexys-core"
]
}
Exporting API objects exposes interfaces to caller context
Return an API object fromactivate() in core/src/extension.ts:
export interface GinexysCoreApi {
router: GinexysRouter;
authProvider: AuthProvider;
rewriteHtmlForWebview: typeof rewriteHtmlForWebview;
}
export function activate(context: vscode.ExtensionContext): GinexysCoreApi { const authProvider = new AuthProvider(context); const router = new GinexysRouter();
return { router, authProvider, rewriteHtmlForWebview }; }
Consuming exports dynamically resolves runtime APIs
In satellite extensions, resolve core exports object at runtime usingtype-only imports for compilation:
import * as vscode from 'vscode';
import type { GinexysCoreApi } from 'ginexys-core'; // Erased at compile time!
async function getCoreApi(): Promise<GinexysCoreApi> { const coreExt = vscode.extensions.getExtension<GinexysCoreApi>('ginexys.ginexys-core'); if (!coreExt) { throw new Error('Ginexys Core extension is missing.'); } if (!coreExt.isActive) { await coreExt.activate(); } return coreExt.exports; }
Because import type is stripped during TypeScript compilation, no require('core/...') statements exist in the compiled JavaScript output.
Rule of thumb: Never use direct Noderequire()statements to import code across published VS Code extensions. Export a public API from your core extensionactivate()function and consume it viavscode.extensions.getExtension().exportsusingimport type.