Skip to content

Commit 02d5c42

Browse files
authored
feat: service-open + service-shiki wire services, with plugin migrations (#260)
1 parent ac35c3b commit 02d5c42

76 files changed

Lines changed: 1456 additions & 393 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

alias.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { join, relative } from 'pathe'
55
const root = fileURLToPath(new URL('.', import.meta.url))
66
const r = (path: string) => fileURLToPath(new URL(`./packages/${path}`, import.meta.url))
77
const p = (path: string) => fileURLToPath(new URL(`./plugins/${path}`, import.meta.url))
8+
const s = (path: string) => fileURLToPath(new URL(`./services/${path}`, import.meta.url))
89

910
export const alias = {
1011
'devframe/rpc/transports/sse-client': r('devframe/src/rpc/transports/sse-client.ts'),
@@ -131,6 +132,8 @@ export const alias = {
131132
'@devframes/plugin-assets/cli': p('assets/src/cli.ts'),
132133
'@devframes/plugin-assets/vite': p('assets/src/vite.ts'),
133134
'@devframes/plugin-assets': p('assets/src/index.ts'),
135+
'@devframes/service-open': s('open/src/index.ts'),
136+
'@devframes/service-shiki': s('shiki/src/index.ts'),
134137
}
135138

136139
// update tsconfig.base.json - CSS aliases exist for Vite resolution only;

docs/errors/DF0066.md

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,27 @@ outline: deep
1010
1111
## Cause
1212

13-
Wire services are deduplicated by npm package name: the first installation wins, and later installs of the same package return the existing node API. Option sets from multiple installers only merge **before** the `ctx.services.ready()` barrier fires — an install that arrives after the service was constructed can no longer influence its configuration, so any options it carried are dropped with this warning.
13+
Wire services are deduplicated by npm package name: the first installation wins, and later installs of the same package return the existing node API. Declared services are constructed once **before setup runs**, deep-merging every declarer's options. Calling `ctx.services.install()` for an already-constructed package — the dynamic escape hatch used after that point — can no longer influence its configuration, so any options it carries are dropped with this warning.
1414

1515
## Example
1616

1717
```ts
18-
await ctx.services.ready()
19-
20-
// ✗ The service is already constructed; { themes } is ignored.
21-
await ctx.services.install(createShikiService({ themes }))
18+
// The package is already declared (and constructed pre-setup) elsewhere.
19+
// ✗ This late install can't merge; { themes } is ignored.
20+
ctx.services.install(createShikiService({ themes }))
2221
```
2322

2423
## Fix
2524

26-
Install the service (or declare it in `DevframeDefinition.services`) before the barrier — a host's explicit installs during setup/`configure` naturally run before the adapter fires `ready()`, so its options join the merge:
25+
Declare the service so its options join the pre-setup merge — on the plugin's `DevframeDefinition.services`, or host-wide via `initHub({ services })`:
2726

2827
```ts
29-
await initHub({
30-
async configure(ctx) {
31-
ctx.services.install(createShikiService({ themes })) // ✓ merges
32-
},
28+
initHub({
29+
services: [createShikiService({ themes })], // ✓ merges before setup
30+
devframes: [/**/],
3331
})
3432
```
3533

3634
## Source
3735

38-
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts)`install()`/the barrier flush warn when an already-installed package is installed again.
36+
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts)`installPackage` warns when an already-installed package is installed again.

docs/errors/DF0067.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ outline: deep
1010
1111
## Cause
1212

13-
A service descriptor marked `required: true` names a package that could not be resolved and imported at the `ctx.services.ready()` barrier. Descriptors resolve against the declaring plugin's own dependencies first (then the workspace root), so this usually means the service package is missing from the declarer's `dependencies`, or isn't installed.
13+
A service descriptor marked `required: true` names a package that could not be resolved and imported when services are constructed before setup. Descriptors resolve against the declaring plugin's own dependencies first (then the workspace root), so this usually means the service package is missing from the declarer's `dependencies`, or isn't installed.
1414

1515
Descriptors without `required` degrade instead: the missing service is skipped and clients observe `services.has(pkg) === false`.
1616

@@ -19,7 +19,7 @@ Descriptors without `required` degrade instead: the missing service is skipped a
1919
```ts
2020
defineDevframe({
2121
services: [
22-
// ✗ Throws at the ready() barrier when the package isn't installed.
22+
// ✗ Throws before setup when the package isn't installed.
2323
{ package: '@devframes/service-shiki', required: true },
2424
],
2525
})
@@ -31,4 +31,4 @@ Install the service package next to whoever declares it — a plugin declaring i
3131

3232
## Source
3333

34-
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts) — the barrier flush throws when a `required` descriptor's package fails to import.
34+
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts) — the pre-setup construction throws when a `required` descriptor's package fails to import.

docs/errors/DF0068.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ outline: deep
1010
1111
## Cause
1212

13-
A service descriptor marked `required: true` declares a `version` range, and the version of the service that actually resolved falls outside it. The range is checked at the `ctx.services.ready()` barrier against the resolved definition's own `version`.
13+
A service descriptor marked `required: true` declares a `version` range, and the version of the service that actually resolved falls outside it. The range is checked when services are constructed before setup against the resolved definition's own `version`.
1414

1515
Without `required`, the same mismatch installs the service anyway and warns with [`DF0069`](/errors/DF0069).
1616

@@ -31,4 +31,4 @@ Align the installed service package with the declared range (update whichever si
3131

3232
## Source
3333

34-
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts) — the barrier flush checks each descriptor's `version` range against the resolved definition.
34+
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts) — the pre-setup construction checks each descriptor's `version` range against the resolved definition.

docs/errors/DF0069.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ Align the installed service package with the declared range to silence the warni
3131

3232
## Source
3333

34-
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts) — the barrier flush checks each descriptor's `version` range against the resolved definition.
34+
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts) — the pre-setup construction checks each descriptor's `version` range against the resolved definition.

docs/errors/DF0070.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ A service package's default export must be its `create<X>Service` factory, retur
3333

3434
## Source
3535

36-
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts)`install()` validates its input; the barrier flush validates imported factories and the definitions they return.
36+
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts)`install()` validates its input; the pre-setup construction validates imported factories and the definitions they return.

docs/errors/DF0071.md

Lines changed: 0 additions & 28 deletions
This file was deleted.

docs/guide/services.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,30 +101,37 @@ export default function createOpenService(options?: OpenServiceOptions): Devfram
101101

102102
Two declaration merges make it fully typed for consumers: the fully-qualified RPC ids go into `DevframeRpcServerFunctions`, and the package → scope mapping into `DevframeServicesScopeRegistry` (so a client's `services.get()` returns a scoped, typed RPC handle).
103103

104-
### Installing
104+
### Declaring
105105

106-
A host with the factory at hand installs explicitly; a plugin declares what it consumes on its definition and the adapter resolves the package **against the plugin's own dependencies** the base for that resolution is the definition's [`importMetaUrl`](./devframe-definition#resolving-against-the-plugins-own-dependencies), so a plugin ships a service package as its own dependency and users install nothing extra:
106+
Services are **declarative**. A plugin lists what it consumes on its definition; a host lists shared ones on `initHub`. The adapter resolves each package — for a plugin, **against the plugin's own dependencies** via the definition's [`importMetaUrl`](./devframe-definition#resolving-against-the-plugins-own-dependencies), so a plugin ships a service package as its own dependency and users install nothing extra — and constructs it:
107107

108108
```ts
109-
// host side (e.g. inside initHub's configure)
110-
ctx.services.install(createShikiService({ themes }))
111-
112-
// plugin side — declarative
109+
// plugin side — on the definition
113110
defineDevframe({
114111
importMetaUrl: import.meta.url, // resolution base for the declared packages
115112
services: [
116113
{ package: '@devframes/service-open' },
117114
{ package: '@devframes/service-shiki', version: '^1', options: { langs: ['vue'] } },
118115
],
119116
})
117+
118+
// host side — shared services on initHub
119+
initHub({
120+
services: [createShikiService({ themes })],
121+
devframes: [/**/],
122+
})
120123
```
121124

122125
Entries are optional by default — a package that isn't installed is skipped and clients see `has() === false`. Mark an entry `required: true` to fail hard instead ([`DF0067`](https://devfra.me/errors/DF0067) on a missing package, [`DF0068`](https://devfra.me/errors/DF0068) on an unsatisfied `version` range; without it a range mismatch only warns with [`DF0069`](https://devfra.me/errors/DF0069)).
123126

124-
Installs queue until the adapter fires the `ctx.services.ready()` barrier after every devframe's setup has run. There each service is constructed **once**, with the option sets from every declarer merged — through the definition's `mergeOptions` when it declares one, otherwise shallow-merged in declaration order, so a host installing last wins. After the barrier, installing an already-installed package returns the existing API and warns ([`DF0066`](https://devfra.me/errors/DF0066)) when its options had to be ignored.
127+
### Lifecycle: ready before setup
128+
129+
Services are constructed and made ready **before any `setup(ctx)` runs**. The hub collects every declared service (across all devframes plus `initHub`), constructs each **once** — deep-merging the option sets from every declarer (objects recurse, arrays union-dedupe, scalars take the later value; a service may override with its own `mergeOptions`) — and only then runs the setups. So `setup(ctx)` can consume a service synchronously via `ctx.services.get(pkg)`, including one another devframe declared.
125130

126131
Server-side consumers get the node API from the same registry — `ctx.services.get('@devframes/service-open')` or `whenAvailable` — with no RPC hop.
127132

133+
Declarative covers the common case. For a service whose configuration is only known at runtime, `ctx.services.install(input)` is the dynamic escape hatch: after the pre-setup construction it builds immediately; re-installing an already-constructed package returns the existing API and warns ([`DF0066`](https://devfra.me/errors/DF0066)) if it carried options that can no longer merge.
134+
128135
### Feature-detecting on the client
129136

130137
Installed services are advertised through the `devframe:services` [shared state](./shared-state); the client mirrors it on `rpc.services`:
@@ -144,6 +151,12 @@ state.on('updated', render)
144151

145152
`has()`/`get()`/`keys()` are synchronous snapshots of the advertisement — before the first sync lands they read as empty, and `get()` returns `undefined` rather than throwing, so the natural shape of consuming code is "render the fallback until the service appears". Each handle carries the advertised `version` and `meta` for finer gating.
146153

154+
### Built-in services
155+
156+
**`@devframes/service-open`** (`devframes:service:open`) opens files in the user's editor (`open-in-editor`, with optional `line`/`column`) or reveals them in the OS file explorer (`open-in-finder`). Paths may be absolute or relative to the workspace root (so a client with only a workspace-relative path — a message's file position, say — calls it directly); the service refuses anything outside the workspace root and the configured extra `roots` (`DS_OPEN_0002`), and gates editor commands to the `KNOWN_EDITORS` picklist. Options: `{ editor?, roots? }` — the preferred editor (later installer wins) and additional openable directories (merged as a union). It supersedes the per-plugin `devframe/recipes/common-rpc-functions` registrations, now deprecated.
157+
158+
**`@devframes/service-shiki`** (`devframes:service:shiki`) renders [Shiki](https://shiki.style) syntax highlighting on the server, so plugin bundles stop shipping grammars and themes. Three RPC queries — `highlight` (dual-theme HTML), `code-to-hast`, and `code-to-tokens` (for renderers that own their DOM, e.g. diff views) — all client-`cacheable` and LRU-cached server-side per `(code, lang, themes)`. Unknown languages degrade to plain text. Options: `{ themes?, langs? }` — the default light/dark pair (defaults `vitesse-light`/`vitesse-dark`, matching the design system; later installer wins) and languages to eagerly load (merged as a union).
159+
147160
## Services, RPC, or shared state?
148161

149162
Each mechanism covers a different direction of travel:

docs/helpers/common-rpc-functions.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ outline: deep
44

55
# Common RPC Functions
66

7+
> [!WARNING]
8+
> Deprecated in favor of the [`@devframes/service-open` wire service](/guide/services#built-in-services) — one host-level installation shared by every plugin, feature-detectable from clients, with workspace-root path containment on top of the editor gating. The recipe keeps working; removal lands in a future major.
9+
710
Prebuilt RPC actions for the two file-system actions every CLI devtool needs — opening a file in the editor, revealing a path in the OS file explorer. Use the recipe instead of re-implementing them so every devframe converges on the same registered names and payload shape.
811

912
```ts

docs/plugins/assets.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,12 @@ All functions are namespaced `devframes:plugin:assets:*`:
9595
| `list` | `query`, `snapshot: true` | Every file under the managed directory, with type, size, and last-modified time. |
9696
| `capabilities` | `query`, `snapshot: true` | Whether write actions are enabled, and the upload allow-list — lets the UI gate itself proactively. |
9797
| `read-image-meta` | `query` | Width, height, and orientation for an image asset. |
98-
| `read-text` | `query` | Truncated text content, for preview. |
98+
| `read-text` | `query` | Truncated text content, for preview. When the host advertises the [`@devframes/service-shiki` wire service](/guide/services#built-in-services), the panel renders it server-highlighted; otherwise it falls back to a plain `<pre>`. |
9999
| `upload` | `action` | Allocates a streaming upload slot; the client pipes the file's bytes over the paired channel. |
100100
| `rename` | `action` | Renames an asset within its folder, preserving its extension. |
101101
| `delete` | `action` | Deletes one or more assets in a single call. |
102102
| `mkdir` | `action` | Creates a folder, including missing parents. |
103-
| `open-in-editor` / `reveal-in-folder` | `action` | Launch the asset in your editor, or reveal its containing folder in the OS file manager. Always registered, regardless of `write`. |
103+
| `open-in-editor` / `reveal-in-folder` | `action` | Launch the asset in your editor, or reveal its containing folder in the OS file manager, delegating to the [`@devframes/service-open` wire service](/guide/services#built-in-services) (installed by the plugin with the managed dir as an allowed root). Always registered, regardless of `write`. |
104104

105105
`upload` / `rename` / `delete` / `mkdir` are registered only when `write` is enabled.
106106

0 commit comments

Comments
 (0)