Skip to content

Commit f6a1efa

Browse files
committed
refactor: messages calls service-open directly, no wrapper RPC
service-open now resolves a relative `path` against its workspaceRoot (absolute paths unchanged), so a client holding only a workspace-relative path can call it directly. The messages panel drops its `devframes:plugin:messages:open-file` bridge (and DP_MESSAGES_0002) and calls `devframes:service:open:open-in-editor` straight from the client. Removes the now-dead DS_OPEN_0001 (paths are no longer required absolute).
1 parent ccf0a42 commit f6a1efa

12 files changed

Lines changed: 43 additions & 112 deletions

File tree

‎docs/guide/services.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ state.on('updated', render)
153153

154154
### Built-in services
155155

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`). Callers pass **absolute** paths; 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.
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.
157157

158158
**`@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).
159159

‎plugins/messages/src/client/App.vue‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<script setup lang="ts">
22
import type { DevframeMessageAction, DevframeMessageEntry } from '@devframes/hub/types'
3+
// Types-only: loads service-open's RPC/scope augmentations so the scoped
4+
// `open.rpc.call('open-in-editor', …)` below is fully typed.
5+
import type {} from '@devframes/service-open'
36
import type { DevframeConnectionStatus, DevframeRpcClient } from 'devframe/client'
47
import DisplayBadge from '@antfu/design/components/Display/DisplayBadge.vue'
58
import FormSearchField from '@antfu/design/components/Form/FormSearchField.vue'
@@ -108,7 +111,10 @@ async function onOpenFile(entry: DevframeMessageEntry): Promise<void> {
108111
if (!entry.filePosition)
109112
return
110113
const { file, line, column } = entry.filePosition
111-
await props.rpc.call('devframes:plugin:messages:open-file', { file, line, column })
114+
// Call the open wire service directly — it resolves the workspace-relative
115+
// path itself. `file` may be relative or absolute.
116+
const open = props.rpc.services.get('@devframes/service-open')
117+
await open?.rpc.call('open-in-editor', { path: file, line, column })
112118
}
113119
</script>
114120

‎plugins/messages/src/diagnostics.ts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,5 @@ export const diagnostics = defineDiagnostics({
1414
`"${p.id}" is mounted on a context without a hub messages host (\`ctx.messages\`) — its RPC surface stays registered but no-ops, so the panel will show an empty feed.`,
1515
fix: 'Mount this devframe through a hub host (`@devframes/hub`\'s `initHub`, or `createHubContext` + `ctx.install`) to get a live message feed.',
1616
},
17-
DP_MESSAGES_0002: {
18-
why: 'Cannot open the file: the "@devframes/service-open" wire service is not installed on this host.',
19-
fix: 'Install the service package next to the messages plugin (it is declared in the plugin\'s `services`), or install it host-side via `ctx.services.install(createOpenService())`.',
20-
},
2117
},
2218
})

‎plugins/messages/src/node/index.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ export function setupMessages(ctx: DevframeNodeContext): void {
1717
if (!getMessagesHost(ctx))
1818
diagnostics.DP_MESSAGES_0001({ id: PLUGIN_ID })
1919

20-
// The detail panel's "open file" affordance delegates to the
20+
// The detail panel's "open file" affordance calls the
2121
// `@devframes/service-open` wire service (declared in the definition's
22-
// `services`) through `devframes:plugin:messages:open-file`, which
23-
// resolves workspace-relative file positions server-side.
22+
// `services`) directly from the client — the service resolves the
23+
// workspace-relative file position itself, so the plugin needs no bridge.
2424
for (const fn of serverFunctions)
2525
ctx.rpc.register(fn)
2626
}

‎plugins/messages/src/rpc/functions/open-file.ts‎

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

‎plugins/messages/src/rpc/index.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { RpcDefinitionsToFunctions } from 'devframe/rpc'
22
import { messagesAdd } from './functions/add'
33
import { messagesClear } from './functions/clear'
44
import { messagesList } from './functions/list'
5-
import { messagesOpenFile } from './functions/open-file'
65
import { messagesRemove } from './functions/remove'
76
import { messagesUpdate } from './functions/update'
87

@@ -17,7 +16,6 @@ export const serverFunctions = [
1716
messagesUpdate,
1817
messagesRemove,
1918
messagesClear,
20-
messagesOpenFile,
2119
] as const
2220

2321
declare module 'devframe' {

‎plugins/messages/test/dev-server.test.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,16 @@ describe('messages dev-server (hub context)', () => {
4343
expect(meta.websocket).toBe(server.port)
4444
})
4545

46-
it('registers the open-file bridge and the open service alongside the feed RPCs', () => {
46+
it('installs the open service (called directly by the client) alongside the feed RPCs', () => {
4747
const names = Array.from(server.ctx.rpc.definitions.keys())
4848
expect(names).toContain('devframes:plugin:messages:list')
4949
expect(names).toContain('devframes:plugin:messages:add')
5050
expect(names).toContain('devframes:plugin:messages:update')
5151
expect(names).toContain('devframes:plugin:messages:remove')
5252
expect(names).toContain('devframes:plugin:messages:clear')
53-
expect(names).toContain('devframes:plugin:messages:open-file')
54-
// The declared `@devframes/service-open` wire service is installed and
55-
// registers its own scoped RPC.
53+
// The detail panel calls the declared `@devframes/service-open` wire
54+
// service directly; the plugin registers no open-file bridge of its own.
55+
expect(names).not.toContain('devframes:plugin:messages:open-file')
5656
expect(names).toContain('devframes:service:open:open-in-editor')
5757
expect(server.ctx.services.has('@devframes/service-open')).toBe(true)
5858
})

‎services/open/src/diagnostics.ts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ import { defineDiagnostics } from 'devframe/utils/nostics'
66
export const diagnostics = defineDiagnostics({
77
docsBase: 'https://devfra.me/errors',
88
codes: {
9-
DS_OPEN_0001: {
10-
why: (p: { path: string }) => `Refusing to open "${p.path}": the path is not absolute.`,
11-
fix: 'Resolve the path on the caller side (e.g. against the workspace root or your plugin\'s managed directory) before calling the open service.',
12-
},
139
DS_OPEN_0002: {
1410
why: (p: { path: string }) => `Refusing to open "${p.path}": the path is outside the workspace root and every configured extra root.`,
1511
fix: 'The open service only touches files under the workspace root by default. Pass additional allowed directories via the service\'s `roots` option when your tool manages files elsewhere (e.g. a global storage dir).',

‎services/open/src/index.ts‎

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ export interface OpenServiceOptions {
2626
}
2727

2828
export interface OpenInEditorInput {
29-
/** Absolute path of the file to open. */
29+
/**
30+
* File to open — absolute, or relative to the service's `workspaceRoot`
31+
* (so a client with only a workspace-relative path, e.g. a message's file
32+
* position, can call this directly without a server-side bridge).
33+
*/
3034
path: string
3135
line?: number
3236
column?: number
@@ -57,11 +61,12 @@ declare module 'devframe' {
5761
/**
5862
* The open wire service — `open-in-editor` / `open-in-finder` RPC shared by
5963
* every plugin on the host, replacing per-plugin registrations of the
60-
* (deprecated) `devframe/recipes/common-rpc-functions` recipes. Callers pass
61-
* **absolute** paths; the service refuses paths outside the workspace root
62-
* and the configured extra {@link OpenServiceOptions.roots} (`DS_OPEN_0002`),
63-
* and gates editor commands to the `KNOWN_EDITORS` picklist so the RPC
64-
* surface can't spawn arbitrary commands.
64+
* (deprecated) `devframe/recipes/common-rpc-functions` recipes. Paths may be
65+
* absolute or relative to the `workspaceRoot`; the service refuses paths
66+
* outside the workspace root and the configured extra
67+
* {@link OpenServiceOptions.roots} (`DS_OPEN_0002`), and gates editor
68+
* commands to the `KNOWN_EDITORS` picklist so the RPC surface can't spawn an
69+
* arbitrary command.
6570
*/
6671
export function createOpenService(options?: OpenServiceOptions): DevframeServiceDefinition<OpenServiceApi, OpenServiceOptions> {
6772
return {
@@ -74,11 +79,12 @@ export function createOpenService(options?: OpenServiceOptions): DevframeService
7479
setup(ctx, { options }) {
7580
const allowedRoots = [ctx.workspaceRoot, ...(options?.roots ?? [])].map(root => resolve(root))
7681

77-
/** Absolute + contained in one of the allowed roots, or throws. */
82+
/**
83+
* Resolve `path` (relative paths against `workspaceRoot`) and assert it
84+
* lands inside one of the allowed roots, or throw.
85+
*/
7886
function assertAllowedPath(path: string): string {
79-
if (!isAbsolute(path))
80-
throw diagnostics.DS_OPEN_0001({ path })
81-
const resolved = resolve(path)
87+
const resolved = isAbsolute(path) ? resolve(path) : resolve(ctx.workspaceRoot, path)
8288
const contained = allowedRoots.some((root) => {
8389
const rel = relative(root, resolved)
8490
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel))

‎services/open/test/service.test.ts‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,23 @@ describe('@devframes/service-open', () => {
6666
expect(launchEditor).toHaveBeenLastCalledWith(join(dir, 'a.ts'), 'zed')
6767
})
6868

69-
it('refuses relative paths and paths outside the allowed roots', async () => {
69+
it('resolves relative paths against the workspace root', async () => {
70+
const { ctx, dir } = await createCtx()
71+
const install = ctx.services.install(createOpenService())
72+
await ctx.services.ready()
73+
const api = await install
74+
75+
await api!.openInEditor({ path: 'src/a.ts' })
76+
expect(launchEditor).toHaveBeenCalledWith(join(dir, 'src/a.ts'), undefined)
77+
})
78+
79+
it('refuses paths outside the allowed roots', async () => {
7080
const { ctx } = await createCtx()
7181
const install = ctx.services.install(createOpenService())
7282
await ctx.services.ready()
7383
const api = await install
7484

75-
await expect(api!.openInEditor({ path: 'src/a.ts' })).rejects.toThrowError(/not absolute/)
7685
await expect(api!.openInFinder({ path: '/etc/passwd' })).rejects.toThrowError(/outside the workspace root/)
77-
expect(launchEditor).not.toHaveBeenCalled()
7886
expect(open).not.toHaveBeenCalled()
7987
})
8088

0 commit comments

Comments
 (0)