Cross-Devframe Services
ctx.services lets one devframe expose a typed, namespaced capability visible to every devframe. Two tiers: in-process services (provide/get) share live objects between devframes; wire services also register RPC and advertise to RPC clients.
The Node-Side API reference collects the host methods and the definition/descriptor fields as lookup tables.
Providing a service
Augment DevframeServicesRegistry with your id and type, then provide in setup:
export interface SourcesService {
register: (entry: SourceEntry) => () => void
}
declare module 'devframe' {
interface DevframeServicesRegistry {
'my-plugin:sources': SourcesService
}
}
export function setup(ctx: DevframeNodeContext) {
ctx.services.provide('my-plugin:sources', createSourcesService())
}Service ids prefix the provider's id (<devframe-id>:<service>), unique per context — a second provide() under a taken id throws DF0037. provide() returns a revoke; guard idempotent setup with has(id).
Consuming a service
A consumer loads it with a types-only import (import type {} from '@my-org/my-plugin'), then reaches it in setup:
ctx.services.whenAvailable('my-plugin:sources', (sources) => {
sources.register({ id: 'other-plugin:state', data: () => state })
})Prefer whenAvailable over get: it fires immediately if provided, else on provide, and re-fires on revoke/re-provide. get(id) returns the implementation or undefined (ids without an augmentation as unknown).
The DevframeServicesHost API
DevframeServicesHost exposes provide(id, service) => revoke, get(id), has(id), whenAvailable(id, cb) => unsubscribe, and keys(); plus a wire tier install(input, options?) => Promise<api | undefined> and ready() => Promise<void>.
Wire services
A wire service is a shared node-side capability, an npm module.
Shipping one
A service package's default export is a factory returning DevframeServiceDefinition:
export default function createOpenService(options?: OpenServiceOptions): DevframeServiceDefinition<OpenServiceApi, OpenServiceOptions> {
return {
package: '@devframes/service-open', // the registry key
version: '1.0.0', // advertised; checked against declared ranges
scope: 'devframes:service:open', // RPC namespace
options,
setup(ctx, { options }) {
// `ctx` is pre-scoped: this registers `devframes:service:open:open-in-editor`
ctx.rpc.register({ name: 'open-in-editor', handler: input => api.openInEditor(input) })
return api // the node API served from ctx.services.get(package)
},
}
}Two declaration merges type it: RPC ids into DevframeRpcServerFunctions, package → scope into DevframeServicesScopeRegistry.
Declaring
Services are declarative: a devframe lists what it consumes; a hub, shared ones on initHub. The adapter resolves each package — for a devframe, against its own dependencies via importMetaUrl.
// devframe side — on the definition
defineDevframe({
importMetaUrl: import.meta.url, // resolution base for the declared packages
services: [
{ package: '@devframes/service-open' },
{ package: '@devframes/service-shiki', version: '^1', options: { langs: ['vue'] } },
],
})A hub lists shared, constructed services on initHub({ services: [createShikiService(opts)] }).
Entries are optional; uninstalled packages are skipped (has() === false). Mark one required: true to fail hard: DF0067 on a missing package, DF0068 on an unsatisfied version range (otherwise a range mismatch only warns, DF0069).
Lifecycle: ready before setup
The hub constructs every declared service (all devframes plus initHub) once — deep-merging option sets (objects recurse, arrays union-dedupe, scalars later-win; override with mergeOptions) — before any setup(ctx) runs, so setup consumes services synchronously via ctx.services.get(pkg).
For a runtime-only service, ctx.services.install(input) builds immediately; re-installing a constructed package returns the existing API, warning DF0066 if options can't merge.
Feature-detecting on the RPC client
Installed services are advertised via devframe:services shared state, mirrored on the RPC client's rpc.services:
const rpc = await connectDevframe()
if (rpc.services.has('@devframes/service-open')) {
const open = rpc.services.get('@devframes/service-open')!
await open.rpc.call('open-in-editor', { path })
}A reactive UI subscribes via rpc.services.state(). has()/get()/keys() are synchronous snapshots, empty before the first sync; each handle carries the version and meta.
Built-in services
Three first-party wire services ship ready to install, each with its own page under Add-ons › Services:
@devframes/service-open(devframes:service:open) — open files in an editor or OS explorer, refusing paths outside the workspace root.@devframes/service-git(devframes:service:git) — typed read/write git operations on one repo.@devframes/service-shiki(devframes:service:shiki) — node-side Shiki highlighting, LRU-cached and dual-theme.
Services, RPC, or shared state?
- Services — node-to-node in-process live references, never crossing a wire.
- RPC — browser-to-node: an RPC client calls a named function.
- Shared state — serializable data synced node side ↔ RPC clients.
A service serves other devframes, RPC surfaces or coding agents, a wire service both.
Serve a Hub Anywhere
initHub() from @devframes/hub/initiate serves a whole multi-devframe devtools install from one web-standard handler on a catch-all route.
Deep Linking
Send a user to a view inside a devframe — from another dock, a coding agent, or a copied URL — two ways: the hub relays a dock activation to focus a dock in place; a standalone SPA reads its URL hash to restore the view.