Consuming salesforcedx-vscode-services
Extensions depending on salesforcedx-vscode-services. Examples: salesforcedx-vscode-metadata, salesforcedx-vscode-org-browser.
Getting the API
Use ExtensionProviderService from @salesforce/effect-ext-utils:
import { ExtensionProviderService, getServicesApi } from '@salesforce/effect-ext-utils';
const ExtensionProviderServiceLive = Layer.effect(
ExtensionProviderService,
Effect.sync(() => ({
getServicesApi
}))
);
// In an Effect.gen:
const api = yield * (yield * ExtensionProviderService).getServicesApi;
Prebuilt vs Per-Extension Services
api.services.prebuiltServicesDependencies — pre-built Context.Context from services extension activation. Wrap with Layer.succeedContext(...).
Shares singleton instances (caches, watchers) across extensions; avoids re-building stateful services.
Per-extension layers (must build yourself):
| Layer | Why |
|---|---|
ChannelServiceLayer(displayName) |
Own output channel |
ErrorHandlerService.Default |
Depends on own ChannelService |
ExtensionContextServiceLayer(context) |
Own ExtensionContext |
SdkLayerFor(context) |
Own tracer (extension name/version in resource attributes) |
ExtensionProviderServiceLive |
Local singleton |
ExtensionContext Setup
Factory function building services layer with ExtensionContext:
export const buildAllServicesLayer = (context: ExtensionContext) =>
Layer.unwrapEffect(
Effect.gen(function* () {
const extensionProvider = yield* ExtensionProviderService;
const api = yield* extensionProvider.getServicesApi;
const channelLayer = api.services.ChannelServiceLayer(
context.extension.packageJSON.displayName ?? 'My Extension'
);
const errorHandlerWithChannel = Layer.provide(api.services.ErrorHandlerService.Default, channelLayer);
return Layer.mergeAll(
Layer.succeedContext(api.services.prebuiltServicesDependencies),
ExtensionProviderServiceLive,
errorHandlerWithChannel,
api.services.ExtensionContextServiceLayer(context),
api.services.SdkLayerFor(context),
channelLayer
);
}).pipe(Effect.provide(ExtensionProviderServiceLive))
);
In activate:
export const activate = async (context: vscode.ExtensionContext): Promise<void> => {
const extensionScope = Effect.runSync(getExtensionScope());
setAllServicesLayer(buildAllServicesLayer(context));
await getRuntime().runPromise(activateEffect(context).pipe(Scope.extend(extensionScope)));
};
Runtime vs provide
- Do: Build
ManagedRuntime.make(AllServicesLayer)and exportgetRuntime(). - Do: Use
getRuntime().runPromise(effect)/runFork(effect)for ad-hoc execution. - Don't: Use
Effect.provide(AllServicesLayer)at call sites — use the runtime instead. - Exception:
registerCommandWithLayer(AllServicesLayer)— keep passing the Layer; it internally uses provide.
Registering Commands
Use registerCommandWithLayer (for layers) or registerCommandWithRuntime (for runtimes):
import { myCommandEffect } from './commands/myCommand';
const api = yield * (yield * ExtensionProviderService).getServicesApi;
// Using Layer
const registerCommand = api.services.registerCommandWithLayer(AllServicesLayer);
yield * registerCommand('sf.my.command', myCommandEffect);
// Using Runtime
const registerCommand = api.services.registerCommandWithRuntime(getRuntime());
yield * registerCommand('sf.my.command', myCommandEffect);
Commands auto:
- Register with ExtensionContext subscriptions
- Wrap with error handling
- Trace with observability spans
- Handle Cancellation
Basic Services
Accessor pattern: call methods directly, don't assign to variable first.
- ChannelService - Output channel
- MediaService - Icons (ICONS) and NLS descriptions
- WorkspaceService - Workspace info
- ConnectionService - Org connections
- ProjectService - Project resolution, packageDirectories
- SettingsService - Settings read/write
- FsService - File ops (web-compatible) and uri/path conversion
- EditorService - Active editor changes and current URI
- Prompts - QuickPick, InputBox, and UserCancellationError handling
Watchers
File Watching
FileWatcherService exposes a PubSub of all workspace file changes (**/*). Subscribe and filter:
import * as PubSub from 'effect/PubSub';
import * as Stream from 'effect/Stream';
const fileWatcher = yield * api.services.FileWatcherService;
const dequeue = yield * PubSub.subscribe(fileWatcher.pubsub);
yield *
Stream.fromQueue(dequeue).pipe(
Stream.filter(event => /* match event.uri to your pattern */),
Stream.runForEach(event =>
Effect.sync(() => {
// Handle event: { type: 'create'|'change'|'delete', uri }
})
)
);
Config Watching
Watch VS Code config changes:
import * as PubSub from 'effect/PubSub';
import * as Stream from 'effect/Stream';
import * as Duration from 'effect/Duration';
const pubsub = yield * PubSub.sliding<vscode.ConfigurationChangeEvent>(100);
const disposable = vscode.workspace.onDidChangeConfiguration(event => {
Effect.runSync(PubSub.publish(pubsub, event));
});
yield *
Effect.addFinalizer(() =>
Effect.sync(() => {
disposable?.dispose();
})
);
yield *
Stream.fromPubSub(pubsub).pipe(
Stream.filter(event => event.affectsConfiguration('section.setting')),
Stream.debounce(Duration.millis(100)),
Stream.runForEach(() => {
// Handle config change
})
);
Target Org Changes
Watch org changes via TargetOrgRef (SubscriptionRef):
const ref = yield * api.services.TargetOrgRef();
yield *
ref.changes.pipe(
Stream.map(org => org.orgId),
Stream.changes,
Stream.tap(orgId => {
// Handle org change
}),
Stream.runForEach(() => {
// Refresh UI, invalidate caches, etc.
})
);
Complete Example Pattern
// extensionProvider.ts
import * as ManagedRuntime from 'effect/ManagedRuntime';
export const buildAllServicesLayer = (context: ExtensionContext) =>
Layer.unwrapEffect(
Effect.gen(function* () {
const extensionProvider = yield* ExtensionProviderService;
const api = yield* extensionProvider.getServicesApi;
const channelLayer = api.services.ChannelServiceLayer(
context.extension.packageJSON.displayName ?? 'My Extension'
);
const errorHandlerWithChannel = Layer.provide(api.services.ErrorHandlerService.Default, channelLayer);
return Layer.mergeAll(
Layer.succeedContext(api.services.prebuiltServicesDependencies),
ExtensionProviderServiceLive,
errorHandlerWithChannel,
api.services.ExtensionContextServiceLayer(context),
api.services.SdkLayerFor(context),
channelLayer
);
}).pipe(Effect.provide(ExtensionProviderServiceLive))
);
export let AllServicesLayer: ReturnType<typeof buildAllServicesLayer>;
export const setAllServicesLayer = (layer: ReturnType<typeof buildAllServicesLayer>) => {
AllServicesLayer = layer;
};
const createRuntime = () => ManagedRuntime.make(AllServicesLayer);
let _runtime: ReturnType<typeof createRuntime> | undefined;
export const getRuntime = () => {
_runtime ??= createRuntime();
return _runtime;
};
// index.ts
import { myCommandEffect } from './commands/myCommand';
export const activateEffect = Effect.fn(`activation:${EXTENSION_NAME}`)(function* (_context: vscode.ExtensionContext) {
const api = yield* (yield* ExtensionProviderService).getServicesApi;
yield* api.services.ChannelService.appendToChannel('Extension activating');
const registerCommand = api.services.registerCommandWithLayer(AllServicesLayer);
yield* registerCommand('sf.my.command', myCommandEffect);
yield* api.services.ChannelService.appendToChannel('Extension activation complete.');
});
Common Patterns
- Start with
Layer.succeedContext(api.services.prebuiltServicesDependencies)— don't add individual*.Defaultfor services already there - Only add per-extension layers on top
import { ICONS }outside Effect;MediaServiceinside EffectChannelServiceLayerbeforeErrorHandlerService- Pass
contexttoSdkLayerFor(extracts name/version from ExtensionContext) Effect.forkIn(..., yield* getExtensionScope())for watcher cleanup on deactivationregisterCommandWithLayerfor all commands (tracing + error handling)- Use
getRuntime().runPromise/runForkinstead ofEffect.provide(AllServicesLayer)for execution
Converted and distributed by TomeVault — claim your Tome and manage your conversions.