Skip to content

Commit bc9cfcb

Browse files
committed
Merge remote-tracking branch 'origin/main' into antfu/devframe-hub-eval
# Conflicts: # pnpm-lock.yaml
2 parents 4b8c4be + 9358a77 commit bc9cfcb

31 files changed

Lines changed: 1853 additions & 2 deletions

.github/workflows/ecosystem-ci.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Ecosystem CI
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
ref:
7+
description: 'vitejs/devtools ref to test against (tag, branch, or commit). Defaults to latest released tag.'
8+
required: false
9+
type: string
10+
schedule:
11+
- cron: '0 4 * * 1'
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
vitejs-devtools:
18+
runs-on: ubuntu-latest
19+
timeout-minutes: 30
20+
steps:
21+
- uses: actions/checkout@v4
22+
- uses: pnpm/action-setup@v4
23+
- uses: actions/setup-node@v4
24+
with:
25+
node-version: 22
26+
cache: pnpm
27+
- run: pnpm install --frozen-lockfile
28+
- run: pnpm test:ecosystem
29+
env:
30+
ECOSYSTEM_DEVTOOLS_REF: ${{ inputs.ref }}
31+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32+
- if: failure()
33+
uses: actions/upload-artifact@v4
34+
with:
35+
name: ecosystem-devtools-logs
36+
path: |
37+
.ecosystem/devtools/**/*.log
38+
.ecosystem/devtools/packages/**/test-results/**
39+
retention-days: 7

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ test-results
1919
playwright-report
2020
playwright/.cache
2121
blob-report
22+
.ecosystem

docs/guide/built-with.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ End-to-end examples in this repo, exercising the full adapter surface:
1414

1515
- [**files-inspector**](https://github.com/devframes/devframe/tree/main/examples/files-inspector) — lists files in cwd via RPC; exercises CLI dev/build/spa surfaces.
1616
- [**streaming-chat**](https://github.com/devframes/devframe/tree/main/examples/streaming-chat) — streams synthetic chat tokens from server to client via `ctx.rpc.streaming`.
17+
- [**next-runtime-snapshot**](https://github.com/devframes/devframe/tree/main/examples/next-runtime-snapshot) — Next.js App Router SPA over RPC, surfacing the host Node runtime (system info, memory, env).

docs/guide/standalone-cli.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,50 @@ export default defineNuxtConfig({
100100

101101
Build with `nuxt build` and point `cli.distDir` at `./dist/public`. The SPA discovers its effective base at runtime — no `--base` rewrite needed. See the [Nuxt helper docs](/helpers/nuxt) for the full reference.
102102

103+
## Next.js SPA setup
104+
105+
For a Next.js App Router SPA, the integration is plain Next.js static export — devframe owns the HTTP and RPC server, Next.js produces the static bundle and stops there. Three config settings cover the integration:
106+
107+
```js [next.config.mjs]
108+
/** @type {import('next').NextConfig} */
109+
export default {
110+
output: 'export',
111+
assetPrefix: '.',
112+
trailingSlash: true,
113+
images: { unoptimized: true },
114+
}
115+
```
116+
117+
- **`output: 'export'`** emits the SPA as static HTML/JS/CSS — no Next.js runtime is needed at serve time. Server Components are pre-rendered at build; Client Components hydrate against the devframe RPC connection.
118+
- **`assetPrefix: '.'`** is the setting that makes the build base-agnostic. Assets are referenced as `./_next/...` so the same bundle works at `/`, `/__my-tool/`, and any other mount path the host adapter chooses. Without it, Next.js bakes in `/_next/...` and the build only works at the root.
119+
- **`trailingSlash: true`** emits `foo/index.html` rather than `foo.html`, which composes cleanly with devframe's static-handler directory-with-index resolution.
120+
121+
`next build` writes the export to `<project>/out/` next to `next.config.mjs`. Copy or move that to wherever you point `cli.distDir`:
122+
123+
```json [package.json]
124+
{
125+
"scripts": {
126+
"build": "next build src/client && rm -rf dist/client && mkdir -p dist && cp -r src/client/out dist/client"
127+
}
128+
}
129+
```
130+
131+
```ts [src/cli.ts]
132+
import { fileURLToPath } from 'node:url'
133+
134+
defineDevframe({
135+
id: 'my-tool',
136+
cli: {
137+
distDir: fileURLToPath(new URL('../dist/client', import.meta.url)),
138+
},
139+
//
140+
})
141+
```
142+
143+
Inside Client Components, call `connectDevframe()` once and share the result via React context. See [Client](./client) for the full reference — the Next.js side is plain React, with no devframe-specific wrapper.
144+
145+
End-to-end example: [`examples/next-runtime-snapshot`](https://github.com/devframes/devframe/tree/main/examples/next-runtime-snapshot).
146+
103147
## Connecting from the client
104148

105149
With the Nuxt helper installed, use `$rpc` directly:

eslint.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ export default antfu({
66
ignores: [
77
'skills',
88
'**/dist',
9+
'**/.next',
10+
'**/out',
11+
'**/next-env.d.ts',
912
'**/.vitepress/cache',
1013
'**/.vitepress/dist',
1114
],
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.next
2+
dist
3+
next-env.d.ts
4+
node_modules
5+
out
6+
.turbo
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# next-runtime-snapshot
2+
3+
End-to-end devframe demo with a **Next.js App Router** SPA. Shows that any
4+
React+Next.js build is a drop-in replacement for a Preact+Vite SPA: devframe
5+
serves the static export, the client calls into the host Node process via
6+
type-safe RPC.
7+
8+
## What it shows
9+
10+
- `next-runtime-snapshot:system` — a `static` RPC function. Runs once at
11+
build time when baked into a static dump, otherwise resolved live over
12+
WebSocket. Returns Node version, platform/arch, pid, cwd, start time.
13+
- `next-runtime-snapshot:memory` — a `query` RPC function. Re-runnable;
14+
the UI has a refresh button that re-invokes the handler.
15+
- `next-runtime-snapshot:env` — a `query` RPC function with valibot-validated
16+
args (`pattern`, `limit`). Lists environment variables matching a regex,
17+
redacting keys that look secret.
18+
- Next.js App Router with `'use client'` components calling
19+
`connectDevframe()` once and passing the RPC client through React context.
20+
21+
## Run it
22+
23+
```sh
24+
pnpm -C examples/next-runtime-snapshot run build # next build → static export → dist/client/
25+
pnpm -C examples/next-runtime-snapshot run dev # node bin.mjs → devframe CLI
26+
```
27+
28+
Open `http://localhost:9899/__next-runtime-snapshot/` — the three cards
29+
populate from RPC, the env filter is debounced, and the footer shows the
30+
connection backend.
31+
32+
## Build a static deployment
33+
34+
```sh
35+
pnpm -C examples/next-runtime-snapshot run cli:build
36+
```
37+
38+
Output lands in `dist/static/`. Serve it from any static host (`npx serve
39+
dist/static`) — the `static` and `query` RPCs that opted into the dump still
40+
work because the snapshot is baked at build time.
41+
42+
## Next.js config — three settings worth knowing
43+
44+
`src/client/next.config.mjs` is short on purpose. The three non-defaults
45+
each correspond to a devframe design principle:
46+
47+
- **`output: 'export'`** — devframe owns the HTTP server; Next.js produces a
48+
fully static SPA. No Next.js runtime is required at serve time, so server
49+
components and route handlers are rendered at build time only.
50+
- **`assetPrefix: '.'`** — relative asset paths so the same build works at
51+
`/`, `/__next-runtime-snapshot/`, and any custom base. Devframe's design
52+
principle: SPAs own their base path at runtime, discovered from
53+
`document.baseURI`.
54+
- **`trailingSlash: true`** — emits `foo/index.html` rather than `foo.html`,
55+
which composes cleanly with devframe's static handler directory-with-index
56+
resolution.
57+
58+
The `dist/client/` artifact is a copy of `src/client/out/` (Next.js's
59+
default export directory) — the `build` script just renames it so the
60+
example matches the layout used by the other examples in this repo.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env node
2+
import process from 'node:process'
3+
import { createCli } from 'devframe/adapters/cli'
4+
import devframe from './src/devframe.ts'
5+
6+
async function main() {
7+
const cli = createCli(devframe)
8+
await cli.parse()
9+
}
10+
11+
main().catch((error) => {
12+
console.error(error)
13+
process.exit(1)
14+
})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "next-runtime-snapshot-example",
3+
"type": "module",
4+
"version": "0.4.1",
5+
"private": true,
6+
"description": "End-to-end devframe demo — Next.js App Router SPA over RPC, exposing the host Node runtime snapshot (system info, memory, env).",
7+
"main": "src/devframe.ts",
8+
"bin": {
9+
"next-runtime-snapshot": "./bin.mjs"
10+
},
11+
"scripts": {
12+
"build": "next build src/client && rm -rf dist/client && mkdir -p dist && cp -r src/client/out dist/client",
13+
"cli:build": "node bin.mjs build --out-dir dist/static",
14+
"dev": "node bin.mjs",
15+
"next:dev": "next dev src/client",
16+
"test": "vitest run"
17+
},
18+
"dependencies": {
19+
"devframe": "workspace:*",
20+
"next": "catalog:frontend",
21+
"react": "catalog:frontend",
22+
"react-dom": "catalog:frontend"
23+
},
24+
"devDependencies": {
25+
"@types/react": "catalog:types",
26+
"@types/react-dom": "catalog:types",
27+
"get-port-please": "catalog:deps",
28+
"h3": "catalog:deps",
29+
"vitest": "catalog:testing",
30+
"ws": "catalog:deps"
31+
}
32+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use client'
2+
3+
import type { DevToolsRpcClient } from 'devframe/client'
4+
import type { ReactNode } from 'react'
5+
import { connectDevframe } from 'devframe/client'
6+
import { createContext, useContext, useEffect, useState } from 'react'
7+
8+
interface ConnectionState {
9+
rpc: DevToolsRpcClient | null
10+
error: string | null
11+
}
12+
13+
const RpcContext = createContext<ConnectionState>({ rpc: null, error: null })
14+
15+
export function useRpc(): ConnectionState {
16+
return useContext(RpcContext)
17+
}
18+
19+
export function RpcProvider({ children }: { children: ReactNode }) {
20+
const [state, setState] = useState<ConnectionState>({ rpc: null, error: null })
21+
22+
useEffect(() => {
23+
let cancelled = false
24+
connectDevframe().then(
25+
(rpc) => {
26+
if (!cancelled)
27+
setState({ rpc, error: null })
28+
},
29+
(err: unknown) => {
30+
if (cancelled)
31+
return
32+
const message = err instanceof Error ? err.message : String(err)
33+
setState({ rpc: null, error: message })
34+
},
35+
)
36+
return () => {
37+
cancelled = true
38+
}
39+
}, [])
40+
41+
return <RpcContext.Provider value={state}>{children}</RpcContext.Provider>
42+
}

0 commit comments

Comments
 (0)