Skip to content

Commit ccd5add

Browse files
committed
refactor(starter): simplify - one RPC function, no unused MCP route
Merge the two demo RPC functions (get-info static, list-items query+snapshot) into a single get-state query+snapshot returning { cwd, node, items } in one round trip. Cuts an RPC file, a namespace entry, a client fetch, and a test, without losing the pattern this starter exists to demonstrate. Also: - Drop `cli.mcp: true` from the devframe definition - nothing in the starter exercised the MCP route, so it was dead surface. - Drop the redundant `play` (duplicate of `play:single`) and `test:unit` (duplicate of `test`) package.json scripts. - Rename test/rpc.test.ts -> test/get-state.test.ts to match the single function it now covers. This PR was created with the help of an agent.
1 parent eddc277 commit ccd5add

9 files changed

Lines changed: 55 additions & 69 deletions

File tree

starter/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pnpm run typecheck
2424
| Path | Purpose |
2525
|------|---------|
2626
| `src/devframe.ts` | The single `DevframeDefinition` every surface below consumes. |
27-
| `src/rpc/` | RPC function definitions (`get-info` static, `list-items` query+snapshot) and their namespace declaration. |
27+
| `src/rpc/` | The one RPC function (`get-state` - a query+snapshot returning runtime info and a directory listing) and its namespace declaration. |
2828
| `src/client/` | The vanilla-TS SPA: `index.html`, `main.ts`, `app.ts`, `styles.css`. |
2929
| `src/shared/base-path.ts` | The devframe's base path, shared between the node-side definition and browser-side client entries. |
3030
| `bin.mjs` | `createCac(devframe).parse()` - exposes `dev`, `build`, `mcp`. |

starter/e2e/app.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import process from 'node:process'
22
import { expect, test } from '@playwright/test'
33

44
// Exercises the single playground end to end: the vanilla-TS SPA connects
5-
// over the real WebSocket RPC bridge and renders the `list-items` /
6-
// `get-info` results for the fixed `./fixtures` directory (wired via
7-
// `playwright.config.ts`'s `webServer.env.DEVFRAME_E2E_CWD`).
5+
// over the real WebSocket RPC bridge and renders the `get-state` result for
6+
// the fixed `./fixtures` directory (wired via `playwright.config.ts`'s
7+
// `webServer.env.DEVFRAME_E2E_CWD`).
88
test('connects and renders the list of items', async ({ page }) => {
99
await page.goto('/')
1010

@@ -18,7 +18,7 @@ test('connects and renders the list of items', async ({ page }) => {
1818
await expect(items.nth(1)).toContainText('file1.txt')
1919
await expect(items.nth(1)).toContainText('file')
2020

21-
// The "cwd" / "node" info line renders from the `get-info` RPC call.
21+
// The "node" info line renders from the same `get-state` call.
2222
await expect(page.locator('.meta code').first()).toContainText(process.version)
2323
})
2424

starter/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,10 @@
2828
"dev": "node bin.mjs",
2929
"build": "vite build --config src/client/vite.config.ts",
3030
"cli:build": "node bin.mjs build --out-dir dist/static",
31-
"play": "vite --config playground/single/vite.config.ts",
3231
"play:single": "vite --config playground/single/vite.config.ts",
3332
"play:hub": "vite --config playground/hub/vite.config.ts",
3433
"lint": "eslint",
3534
"test": "vitest run",
36-
"test:unit": "vitest run",
3735
"test:e2e": "playwright test",
3836
"typecheck": "tsc --noEmit",
3937
"release": "bumpp"

starter/src/client/app.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
import type { DevframeScopedClientContext } from 'devframe/client'
2+
import type { StarterItem, StarterState } from '../rpc/functions/get-state.ts'
23
import { connectDevframe } from 'devframe/client'
34

45
const NAMESPACE = 'devframe-starter'
56
type StarterCtx = DevframeScopedClientContext<typeof NAMESPACE>
67

7-
interface Item {
8-
name: string
9-
kind: 'dir' | 'file'
10-
}
11-
128
function h<K extends keyof HTMLElementTagNameMap>(
139
tag: K,
1410
attrs: Partial<Record<string, string>> = {},
@@ -37,7 +33,7 @@ export interface MountOptions {
3733
baseURL?: string
3834
}
3935

40-
/** Boot the SPA into `root`: connect to devframe, then render the list. */
36+
/** Boot the SPA into `root`: connect to devframe, then render its state. */
4137
export async function mount(root: HTMLElement, options: MountOptions = {}): Promise<void> {
4238
root.replaceChildren(h('div', { class: 'connecting' }, ['Connecting to devframe…']))
4339

@@ -46,15 +42,20 @@ export async function mount(root: HTMLElement, options: MountOptions = {}): Prom
4642

4743
const listCard = h('div', { class: 'card' })
4844
const count = h('span', { class: 'count' }, ['0'])
45+
const nodeCode = h('code', {}, ['…'])
46+
const cwdCode = h('code', {}, ['…'])
4947
const refreshBtn = h('button', { type: 'button' }, ['Refresh'])
5048

5149
async function refresh(): Promise<void> {
5250
refreshBtn.disabled = true
5351
refreshBtn.textContent = 'Loading…'
5452
try {
55-
const items = await ctx.rpc.call('list-items') as Item[]
56-
count.textContent = String(items.length)
57-
renderList(listCard, items)
53+
// The one RPC round trip this SPA makes.
54+
const state = await ctx.rpc.call('get-state') as StarterState
55+
count.textContent = String(state.items.length)
56+
nodeCode.textContent = state.node
57+
cwdCode.textContent = state.cwd
58+
renderList(listCard, state.items)
5859
}
5960
finally {
6061
refreshBtn.disabled = false
@@ -63,16 +64,14 @@ export async function mount(root: HTMLElement, options: MountOptions = {}): Prom
6364
}
6465
refreshBtn.addEventListener('click', () => void refresh())
6566

66-
const info = await ctx.rpc.call('get-info') as { cwd: string, node: string }
67-
6867
root.replaceChildren(
6968
h('div', { class: 'app' }, [
7069
h('header', { class: 'nav' }, [
7170
h('span', { class: 'brand' }, [h('span', { class: 'dot' }), 'Devframe Starter']),
7271
h('span', { class: 'spacer' }),
7372
h('small', { class: 'meta' }, [
7473
'node ',
75-
h('code', {}, [info.node]),
74+
nodeCode,
7675
' · backend ',
7776
h('code', {}, [ctx.base.connectionMeta.backend]),
7877
]),
@@ -85,15 +84,15 @@ export async function mount(root: HTMLElement, options: MountOptions = {}): Prom
8584
refreshBtn,
8685
]),
8786
listCard,
88-
h('p', { class: 'meta' }, ['cwd: ', h('code', {}, [info.cwd])]),
87+
h('p', { class: 'meta' }, ['cwd: ', cwdCode]),
8988
]),
9089
]),
9190
)
9291

9392
await refresh()
9493
}
9594

96-
function renderList(card: HTMLElement, items: Item[]): void {
95+
function renderList(card: HTMLElement, items: StarterItem[]): void {
9796
if (items.length === 0) {
9897
card.replaceChildren(h('p', { class: 'empty' }, ['Nothing in the working directory.']))
9998
return

starter/src/devframe.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@ export default defineDevframe({
3333
// developer who wants to skip the prompt for a one-off loopback-only
3434
// session can pass `--no-auth` per run (`devframe-starter --no-auth`)
3535
// rather than baking the opt-out into the definition.
36-
//
37-
// Serve the agent (MCP) surface over the dev server's `/__mcp` route.
38-
mcp: true,
3936
},
4037
setup(ctx) {
4138
// A scoped context auto-namespaces every registered id with `NAMESPACE:`.

starter/src/rpc/functions/get-info.ts

Lines changed: 0 additions & 19 deletions
This file was deleted.
Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,37 @@ import { readdir } from 'node:fs/promises'
22
import process from 'node:process'
33
import { defineRpcFunction } from 'devframe'
44

5+
export interface StarterItem {
6+
name: string
7+
kind: 'dir' | 'file'
8+
}
9+
10+
export interface StarterState {
11+
cwd: string
12+
node: string
13+
items: StarterItem[]
14+
}
15+
516
/**
617
* A `query` RPC with `snapshot: true`: live over WebSocket in dev, and its
718
* dump is baked into a static build so the SPA keeps working with no server.
8-
* Lists the top-level entries of the working directory.
19+
* The one round trip the client makes - runtime info plus the top-level
20+
* entries of the working directory.
921
*/
10-
export const listItems = defineRpcFunction({
11-
name: 'list-items',
22+
export const getState = defineRpcFunction({
23+
name: 'get-state',
1224
type: 'query',
1325
jsonSerializable: true,
1426
snapshot: true,
1527
setup: ctx => ({
16-
handler: async () => {
28+
handler: async (): Promise<StarterState> => {
1729
const cwd = process.env.DEVFRAME_E2E_CWD || ctx.cwd
1830
const entries = await readdir(cwd, { withFileTypes: true })
19-
return entries
31+
const items = entries
2032
.filter(e => !e.name.startsWith('.'))
2133
.map(e => ({ name: e.name, kind: e.isDirectory() ? 'dir' as const : 'file' as const }))
2234
.sort((a, b) => a.name.localeCompare(b.name))
35+
return { cwd, node: process.version, items }
2336
},
2437
}),
2538
})

starter/src/rpc/index.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import type { RpcDefinitionsToFunctionsWithNamespace } from 'devframe/rpc'
2-
import { getInfo } from './functions/get-info.ts'
3-
import { listItems } from './functions/list-items.ts'
2+
import { getState } from './functions/get-state.ts'
43

54
export const NAMESPACE = 'devframe-starter'
65

7-
export const serverFunctions = [getInfo, listItems] as const
6+
export const serverFunctions = [getState] as const
87

98
declare module 'devframe' {
109
// Functions are defined with bare names and registered through a scoped
11-
// context, so the registry keys are namespaced to match the runtime ids
12-
// (`devframe-starter:get-info`, `devframe-starter:list-items`).
10+
// context, so the registry key is namespaced to match the runtime id
11+
// (`devframe-starter:get-state`).
1312
interface DevframeRpcServerFunctions
1413
extends RpcDefinitionsToFunctionsWithNamespace<typeof NAMESPACE, typeof serverFunctions> {}
1514
}
Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import devframe from '../src/devframe.ts'
1111

1212
// The public `initDevframe` handler used by every hosted adapter (see
1313
// `@devframes/vite/single`) - a side-car WS server on a free port, no SPA
14-
// (`distDir: false`), so this test exercises the RPC functions over the real
15-
// wire without booting the CLI dev server or building the client first.
14+
// (`distDir: false`), so this test exercises `get-state` over the real wire
15+
// without booting the CLI dev server or building the client first.
1616
vi.stubGlobal('WebSocket', WebSocket)
1717

18-
describe('rpc functions', () => {
18+
describe('get-state', () => {
1919
let tmpDir: string
2020
let instance: ReturnType<typeof initDevframe>
2121
let rpc: ReturnType<typeof createRpcClient<any, any>>
@@ -25,7 +25,7 @@ describe('rpc functions', () => {
2525
await writeFile(path.join(tmpDir, 'file1.txt'), '')
2626
await writeFile(path.join(tmpDir, 'file2.txt'), '')
2727
await mkdir(path.join(tmpDir, 'dir1'))
28-
// The RPC functions fall back to this env var over `ctx.cwd` so tests
28+
// `get-state` falls back to this env var over `ctx.cwd` so tests
2929
// control the working directory without touching the real `process.cwd()`.
3030
process.env.DEVFRAME_E2E_CWD = tmpDir
3131

@@ -58,17 +58,16 @@ describe('rpc functions', () => {
5858
delete process.env.DEVFRAME_E2E_CWD
5959
})
6060

61-
it('get-info returns node version and cwd', async () => {
62-
const info = await rpc.$call('devframe-starter:get-info')
63-
expect(info).toEqual({ node: process.version, cwd: tmpDir })
64-
})
65-
66-
it('list-items lists files and directories, sorted, dotfiles excluded', async () => {
67-
const items = await rpc.$call('devframe-starter:list-items')
68-
expect(items).toEqual([
69-
{ name: 'dir1', kind: 'dir' },
70-
{ name: 'file1.txt', kind: 'file' },
71-
{ name: 'file2.txt', kind: 'file' },
72-
])
61+
it('returns runtime info and the working directory listing, sorted with dotfiles excluded', async () => {
62+
const state = await rpc.$call('devframe-starter:get-state')
63+
expect(state).toEqual({
64+
node: process.version,
65+
cwd: tmpDir,
66+
items: [
67+
{ name: 'dir1', kind: 'dir' },
68+
{ name: 'file1.txt', kind: 'file' },
69+
{ name: 'file2.txt', kind: 'file' },
70+
],
71+
})
7372
})
7473
})

0 commit comments

Comments
 (0)