Skip to content

feat: services - installable, client-advertised shared capabilities - #256

Merged
antfu merged 4 commits into
mainfrom
feat/services-core
Aug 18, 2026
Merged

feat: services - installable, client-advertised shared capabilities#256
antfu merged 4 commits into
mainfrom
feat/services-core

Conversation

@antfubot

@antfubot antfubot commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

What

Introduces the wire-service layer on top of the existing node-side ctx.services registry: npm-packaged, server-side capabilities (open-in-editor, shiki highlighting, …) that a host installs once and every plugin and browser client consumes — no per-plugin re-implementation or re-bundling, and client UIs can feature-detect a service and degrade gracefully (hide the "open in editor" button, render a plain <pre>, …).

This is PR 1 of the series designed with the maintainer: core mechanism here; @devframes/service-open + @devframes/service-shiki and the plugin migrations follow.

How it works

Definition & identity — a service ships as an npm package whose default export is a create<X>Service factory returning a DevframeServiceDefinition { package, version, scope, meta?, options?, mergeOptions?, setup }. The registry key is the npm package name; its RPC functions live under the definition's scope namespace (devframes:service:<slug>:<fn>), and setup receives a pre-scoped context and returns the service's node API (provided under the package name, so server-side consumers skip the RPC hop).

Install & the ready() barrierctx.services.install() accepts a ready definition or a declarative descriptor { package, version?, required?, options? }; DevframeDefinition.services declares them per plugin, resolved against the declaring plugin's own dependencies. Installs queue until the collect-then-setup barrier ctx.services.ready() (fired by every adapter — initiate/build/embedded/MCP — and by initHub after configure/UI setup, with a first-connection safety net): option sets from every declarer are merged (mergeOptions or shallow, later wins) and each service is constructed exactly once. Missing or version-mismatched services throw when required, otherwise degrade with a debug skip / warning.

Client discovery — installed services are advertised on the reactive devframe:services shared state. The client gains rpc.services with synchronous has() / get() / keys() (plus the state itself for reactive UI); get() returns the advertisement meta and a scoped, typed RPC handle, typed via the new DevframeServicesScopeRegistry declaration-merge point.

Diagnostics — new coded diagnostics DF0066DF0071 (duplicate install, required import failure, required/optional version-range mismatch, invalid definition, deferred-flush failure).

Public surface (kept minimal)

  • DevframeServicesHost gains install() + ready() only; option/info bags are inline types
  • New exported types: the definition/descriptor/meta/state family and the DevframeServicesScopeRegistry merge point — nothing else
  • installDefinitionServices lives on devframe/internal (the explicitly-unstable surface); the client factory is internal, only its types are exported

Docs

  • guide/services.md — new Wire services section (shipping, installing, option merging, client feature-detection)
  • guide/client.md + guide/devframe-definition.mdrpc.services / services field mentions
  • errors/DF0066.mdDF0071.md reference pages

Validation

  • pnpm lint && pnpm knip && pnpm test && pnpm typecheck && pnpm build all green (1190 tests, 106 files)
  • New unit coverage: barrier/merge semantics, descriptor import resolution (runtime-written fake packages), version-range checks, duplicate/post-barrier installs, advertisement state, client mirror + scoped handles
  • API snapshots regenerated

Created with the help of an agent.

Introduce the wire-service layer on top of the existing ctx.services
registry: npm-packaged server-side capabilities (open-in-editor, shiki
highlighting, …) that a host installs once and every plugin/client
consumes without re-implementing or re-bundling them.

- ServiceDefinition / ServiceDescriptor types, keyed by npm package name,
  with RPC functions namespaced under a service scope
- ctx.services.install() + collect-then-setup ready() barrier: option
  sets from every declarer merge (mergeOptions or shallow, later wins)
  and each service constructs once; its node API is provided under the
  package name for in-process consumers
- DevframeDefinition.services declarative list, resolved relative to the
  declaring plugin's own dependencies; required entries throw on missing
  package or unsatisfied version range, optional ones degrade silently
- advertisement over the reactive devframe:services shared state;
  client.services.has()/get()/keys() with scoped, typed RPC handles
- adapters (initiate/build/embedded/mcp) and the hub fire the barrier;
  a first-connection safety net covers hosts that forget
- diagnostics DF0066–DF0071, tests, API snapshots
@netlify

netlify Bot commented Aug 18, 2026

Copy link
Copy Markdown

Deploy Preview for devfra ready!

Name Link
🔨 Latest commit 6ead8b6
🔍 Latest deploy log https://app.netlify.com/projects/devfra/deploys/6a83da4f3efa3100080c0f94
😎 Deploy Preview https://deploy-preview-256--devfra.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

antfubot and others added 3 commits August 18, 2026 03:05
- fold DevframeServiceSetupInfo / DevframeServiceInstallOptions into
  inline types; drop isReady and the scoped-context services passthrough
- move installDefinitionServices to devframe/internal (unstable surface)
  and keep createDevframeServicesClient internal to the client
- docs: wire-services guide section, client + definition-field mentions,
  error pages DF0066–DF0071
install()'s resolveFrom now also accepts an npm package name (the
declaring plugin's packageName), expanded to that package's location so
its declared services resolve against the plugin's own dependencies —
adapters and the hub queue def.services with a plain loop, and the
installDefinitionServices helper (whose awaited form could deadlock
pre-barrier) is gone.
@antfu antfu changed the title feat: wire services — installable, client-advertised shared capabilities feat: services - installable, client-advertised shared capabilities Aug 18, 2026
@antfu
antfu merged commit ea78f24 into main Aug 18, 2026
12 checks passed
@antfu
antfu deleted the feat/services-core branch August 18, 2026 05:24
antfubot added a commit to vitejs/devtools that referenced this pull request Aug 18, 2026
v0.9.1's Services feature (devframes/devframe#256) has the client read
the devframe:services shared state on load. A static snapshot doesn't
stand up the live hub, so nothing fired the services barrier that
publishes that state — leaving the RPC dump without a match and the
client logging a hard error. Fire context.services.ready() before
collecting the dump (idempotent; publishes an empty state when no
services are installed), mirroring the dock-renderers seeding.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants