Guidelines for consuming salesforcedx-vscode-services extension API. Use when working with extensions that have extensionDependency on salesforcedx-vscode-services, registering commands, using Workspace/Connection/Project/Settings/FS/Channel/Media services, or implementing file/config watchers.
Resources
1Install
npx skillscat add forcedotcom/salesforcedx-vscode/services-extension-consumption Install via the SkillsCat registry.
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;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 extension = vscode.extensions.getExtension(`salesforce.${EXTENSION_NAME}`);
const extensionVersion = extension?.packageJSON?.version ?? 'unknown';
const o11yEndpoint = process.env.O11Y_ENDPOINT ?? extension?.packageJSON?.o11yUploadEndpoint;
// ErrorHandlerService needs ChannelService
const channelLayer = api.services.ChannelServiceLayer(extension?.packageJSON.displayName);
const errorHandlerWithChannel = Layer.provide(api.services.ErrorHandlerService.Default, channelLayer);
return Layer.mergeAll(
ExtensionProviderServiceLive,
api.services.ExtensionContextServiceLayer(context),
api.services.sdkLayerFor(context)
// ... other services
channelLayer,
errorHandlerWithChannel
);
}).pipe(Effect.provide(ExtensionProviderServiceLive))
);In activate:
export const activate = async (context: vscode.ExtensionContext): Promise<void> => {
const extensionScope = Effect.runSync(getExtensionScope());
setAllServicesLayer(buildAllServicesLayer(context));
await Effect.runPromise(activateEffect(context).pipe(Effect.provide(AllServicesLayer), Scope.extend(extensionScope)));
};Registering Commands
Use registerCommandWithLayer pre-loaded with AllServicesLayer:
import { myCommandEffect } from './commands/myCommand';
const api = yield * (yield * ExtensionProviderService).getServicesApi;
const registerCommand = api.services.registerCommandWithLayer(AllServicesLayer);
yield * registerCommand('sf.my.command', myCommandEffect);Commands auto:
- Register with ExtensionContext subscriptions
- Wrap with error handling
- Trace with observability spans
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
Watchers
File Watching
Watch file changes:
const watcher = yield * api.services.FileWatcherService.watchFiles(pattern, options);
yield *
Stream.runForEach(watcher, event =>
Effect.sync(() => {
// Handle file change
})
);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:
const targetOrgRef = yield * api.services.TargetOrgRef();
yield *
Effect.forkDaemon(
targetOrgRef.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
export const buildAllServicesLayer = (context: ExtensionContext) =>
Layer.unwrapEffect(
Effect.gen(function* () {
const extensionProvider = yield* ExtensionProviderService;
const api = yield* extensionProvider.getServicesApi;
const extension = vscode.extensions.getExtension(`salesforce.${EXTENSION_NAME}`);
const extensionVersion = extension?.packageJSON?.version ?? 'unknown';
const o11yEndpoint = process.env.O11Y_ENDPOINT ?? extension?.packageJSON?.o11yUploadEndpoint;
const channelLayer = api.services.ChannelServiceLayer(extension?.packageJSON.displayName);
const errorHandlerWithChannel = Layer.provide(api.services.ErrorHandlerService.Default, channelLayer);
return Layer.mergeAll(
// list whatever you need. TS will tell you if there's something missing
ExtensionProviderServiceLive,
api.services.ConnectionService.Default,
api.services.EditorService.Default,
api.services.MediaService.Default,
api.services.ExtensionContextServiceLayer(context),
api.services.ProjectService.Default,
api.services.WorkspaceService.Default,
api.services.SdkLayerFor({ extensionName: EXTENSION_NAME, extensionVersion, o11yEndpoint }),
channelLayer,
errorHandlerWithChannel
);
}).pipe(Effect.provide(ExtensionProviderServiceLive))
);
// 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');
// useful if registering multiple commands...they all get the layer provided to them
const registerCommand = api.services.registerCommandWithLayer(AllServicesLayer);
yield* registerCommand('sf.my.command', myCommandEffect);
yield* api.services.ChannelService.appendToChannel('Extension activation complete.');
});Common Patterns
- Include
ExtensionContextServiceLayer(context)when you have ExtensionContext - Use
MediaService.Defaultwhen building UI strings in Effect; use directimport { ICONS }when outside Effect - Provide
ChannelServiceLayerbeforeErrorHandlerService - Use
SdkLayerForwith extension name/version for observability - Fork watchers with
Effect.forkIn(..., yield* getExtensionScope())for cleanup on deactivation - Use
registerCommandWithLayerfor all commands (tracing + error handling)