@devframes/next 0.8.2 → 0.9.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { DevframeConnectionStatus, DevframeRpcClient } from "devframe/client";
2
2
  import { ReactNode } from "react";
3
- import { ConnectionMeta } from "devframe/types";
3
+ import { ConnectionMeta } from "devframe";
4
4
  //#region src/client.d.ts
5
5
  interface DevframeRpcState {
6
6
  /** The connected client, or `null` while the initial connect is in flight. */
@@ -0,0 +1,29 @@
1
+ import { DevframeClientHost, DevframeClientHost as DevframeClientHost$1, DevframeClientHostOptions, DevframeClientHostOptions as DevframeClientHostOptions$1 } from "@devframes/hub/client";
2
+ //#region src/hub-client.d.ts
3
+ interface UseDevframeHubClientOptions extends DevframeClientHostOptions$1 {
4
+ /**
5
+ * Hub mount base to connect to. Forwarded as `connect.baseURL` when no
6
+ * `rpc` / `connect.baseURL` is supplied.
7
+ *
8
+ * @default '/__devframes/'
9
+ */
10
+ base?: string;
11
+ }
12
+ /**
13
+ * Boot the devframes-hub **client runtime** inside a React (Next.js) page —
14
+ * the browser half of {@link import('./hub').nextDevframeHub}. Connects RPC to
15
+ * the hub (defaulting `base` to `/__devframes/`), assembles the shared
16
+ * `DevframeClientContext`, imports each dock's client script into the page,
17
+ * and disposes on unmount. Returns the {@link DevframeClientHost} once ready
18
+ * (`null` while connecting).
19
+ *
20
+ * Only needed when you render your own dock UI (or override the hub's `ui`).
21
+ * With the default `@devframes/hub-ui`, its injected `embedded.js` boots the
22
+ * client for you, so the page needs no client code.
23
+ *
24
+ * `renderers` is read once on mount; memoize it at the call site if it isn't a
25
+ * stable reference.
26
+ */
27
+ declare function useDevframeHubClient(options?: UseDevframeHubClientOptions): DevframeClientHost$1 | null;
28
+ //#endregion
29
+ export { type DevframeClientHost, type DevframeClientHostOptions, UseDevframeHubClientOptions, useDevframeHubClient };
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ import { useEffect, useState } from "react";
3
+ import { createDevframeClientHost } from "@devframes/hub/client";
4
+ import { DEVFRAMES_HUB_BASE } from "@devframes/hub/constants";
5
+ //#region src/hub-client.tsx
6
+ /**
7
+ * Boot the devframes-hub **client runtime** inside a React (Next.js) page —
8
+ * the browser half of {@link import('./hub').nextDevframeHub}. Connects RPC to
9
+ * the hub (defaulting `base` to `/__devframes/`), assembles the shared
10
+ * `DevframeClientContext`, imports each dock's client script into the page,
11
+ * and disposes on unmount. Returns the {@link DevframeClientHost} once ready
12
+ * (`null` while connecting).
13
+ *
14
+ * Only needed when you render your own dock UI (or override the hub's `ui`).
15
+ * With the default `@devframes/hub-ui`, its injected `embedded.js` boots the
16
+ * client for you, so the page needs no client code.
17
+ *
18
+ * `renderers` is read once on mount; memoize it at the call site if it isn't a
19
+ * stable reference.
20
+ */
21
+ function useDevframeHubClient(options = {}) {
22
+ const { base = DEVFRAMES_HUB_BASE, rpc, connect } = options;
23
+ const [host, setHost] = useState(null);
24
+ useEffect(() => {
25
+ let disposed = false;
26
+ let created;
27
+ createDevframeClientHost({
28
+ ...options,
29
+ ...rpc ? { rpc } : { connect: {
30
+ baseURL: base,
31
+ ...connect
32
+ } }
33
+ }).then((next) => {
34
+ if (disposed) {
35
+ next.dispose();
36
+ return;
37
+ }
38
+ created = next;
39
+ setHost(next);
40
+ });
41
+ return () => {
42
+ disposed = true;
43
+ created?.dispose();
44
+ setHost(null);
45
+ };
46
+ }, [base, rpc]);
47
+ return host;
48
+ }
49
+ //#endregion
50
+ export { useDevframeHubClient };
package/dist/hub.d.mts ADDED
@@ -0,0 +1,197 @@
1
+ import { DevframeHubUi, HubInstance, InitHubOptions } from "@devframes/hub/initiate";
2
+ import { ConnectionMeta, DevframeHost, DevframeNodeContext, DevframeStorageScope } from "devframe";
3
+ //#region src/host.d.ts
4
+ interface CreateDevframeNextHostOptions {
5
+ /**
6
+ * Public origin the Next app is reachable at, e.g. `http://localhost:3000`.
7
+ * Surfaced through {@link DevframeHost.resolveOrigin} for docks that need an
8
+ * absolute iframe URL.
9
+ */
10
+ resolveOrigin: () => string;
11
+ /**
12
+ * Resolve a directory the host owns for persisted devframe state, per
13
+ * {@link DevframeHost.getStorageDir}.
14
+ */
15
+ getStorageDir: (scope: DevframeStorageScope) => string;
16
+ /**
17
+ * Initial connection meta served at every base registered via
18
+ * {@link DevframeHost.mountConnectionMeta}. Usually unknown until the
19
+ * side-car RPC/WS server has started — publish it later with
20
+ * {@link DevframeNextHost.setConnectionMeta}.
21
+ */
22
+ connectionMeta?: ConnectionMeta;
23
+ }
24
+ interface DevframeNextHostMcpOptions {
25
+ /** Name reported in the MCP handshake. Default: `'devframe (next)'`. */
26
+ serverName?: string;
27
+ /** Version reported in the MCP handshake. Default: `'0.0.0'`. */
28
+ serverVersion?: string;
29
+ /** Expose shared-state keys as MCP resources / `devframe:state:read`. Default: `true`. */
30
+ exposeSharedState?: boolean | ((key: string) => boolean);
31
+ /**
32
+ * Origin allow-list beyond the loopback default. `false` disables the
33
+ * origin gate entirely. Note the MCP route rejects `Origin`-less requests
34
+ * (see `createMcpFetchHandler`).
35
+ */
36
+ allowedOrigins?: readonly string[] | false;
37
+ }
38
+ interface DevframeNextHost {
39
+ /**
40
+ * The {@link DevframeHost} to hand to `createHubContext` / `createHostContext`.
41
+ * Its `mountStatic` / `mountConnectionMeta` calls accumulate into the
42
+ * {@link DevframeNextHost.fetch} handler below.
43
+ */
44
+ host: DevframeHost;
45
+ /**
46
+ * A WHATWG-`fetch` handler that serves every mounted SPA (with SPA
47
+ * fallback, correct content types, and path-traversal guarding — all from
48
+ * devframe's own `serveStaticHandler`) and answers `<base>/__connection.json`
49
+ * for each base registered via `mountConnectionMeta`. Delegate a Next App
50
+ * Router route handler straight to it:
51
+ *
52
+ * ```ts
53
+ * export async function GET(request: Request) {
54
+ * return (await ensureHub()).fetch(request)
55
+ * }
56
+ * ```
57
+ */
58
+ fetch: (request: Request) => Promise<Response>;
59
+ /**
60
+ * Publish the live connection meta once the RPC/WS server is up. Until this
61
+ * is called (and without an initial `connectionMeta`), meta requests answer
62
+ * `503` so a racing client retries rather than caching a wrong endpoint.
63
+ */
64
+ setConnectionMeta: (meta: ConnectionMeta) => void;
65
+ /**
66
+ * Serve an MCP Streamable-HTTP endpoint at `path` **in-process** — on the
67
+ * Next app's own origin, through the same catch-all route as the SPAs (the
68
+ * `/_next/mcp` shape). Built on `createMcpFetchHandler` from
69
+ * `devframe/adapters/mcp` (imported lazily: `@modelcontextprotocol/server`
70
+ * stays an optional peer). Advertise the path in the connection meta
71
+ * (`mcp: { path }` — same origin, no port) and register the instance via
72
+ * `registerDevframeInstance` so `devframe connect` can discover it.
73
+ */
74
+ mountMcp: (ctx: DevframeNodeContext, path: string, options?: DevframeNextHostMcpOptions) => Promise<{
75
+ dispose: () => Promise<void>;
76
+ }>;
77
+ }
78
+ /**
79
+ * Build a Node-runtime {@link DevframeHost} for a Next.js App Router app that
80
+ * hosts one or more devframes, plus the single `fetch` handler its catch-all
81
+ * route delegates to.
82
+ *
83
+ * This is the hosted-adapter counterpart to `devframeViteBridge` for the Next
84
+ * runtime, which — being webpack/Turbopack rather than Vite — can't reuse the
85
+ * Vite middleware path. Instead of hand-rolling static serving in a route
86
+ * handler, static mounts are registered on an internal h3 app and served
87
+ * through devframe's shared `serveStaticHandler` (`app.fetch` makes h3 a
88
+ * WHATWG-`fetch` handler, exactly what an App Router route returns).
89
+ *
90
+ * Pins Node runtime (`export const runtime = 'nodejs'` in the route) because
91
+ * the static handler streams from the filesystem.
92
+ */
93
+ declare function createDevframeNextHost(options: CreateDevframeNextHostOptions): DevframeNextHost;
94
+ //#endregion
95
+ //#region src/hub.d.ts
96
+ interface NextDevframeHubOptions {
97
+ /**
98
+ * Mount base the hub answers under — every frame lives at `<base><id>/`,
99
+ * so the App Router needs one catch-all route under it. Default:
100
+ * `/__devframes/`.
101
+ */
102
+ base?: string;
103
+ /**
104
+ * Pin the side-car RPC/WS port. Next route handlers can't accept WebSocket
105
+ * upgrades, so the socket always runs on a side-car server; default walks a
106
+ * free port near 9777.
107
+ */
108
+ port?: number;
109
+ /** Bind host for the side-car server. Default: `localhost`. */
110
+ host?: string;
111
+ /** Working directory for the hub context. Default: `process.cwd()`. */
112
+ cwd?: string;
113
+ /**
114
+ * Devframes to mount. Load built-in plugin packages through a
115
+ * bundler-ignored dynamic `import()` (their node code + `import.meta.url`
116
+ * dist lookups don't survive static Next bundling) and pass them here —
117
+ * `initHub` resolves async/factory entries.
118
+ */
119
+ devframes?: InitHubOptions['devframes'];
120
+ /** Prebuilt dock-renderer modules forwarded to `initHub({ renderers })`. */
121
+ renderers?: InitHubOptions['renderers'];
122
+ /** Extra RPC declarations registered at context creation. */
123
+ rpcDeclarations?: InitHubOptions['rpcDeclarations'];
124
+ /** Runs once the context exists and every devframe is mounted. */
125
+ configure?: (ctx: Parameters<NonNullable<InitHubOptions['configure']>>[0]) => void | Promise<void>;
126
+ /**
127
+ * The hub's UI slot. Defaults to `@devframes/hub-ui`'s `createUi()` (loaded
128
+ * through a bundler-ignored dynamic `import()` so its `import.meta.url` asset
129
+ * lookups resolve at request time). Pass your own {@link DevframeHubUi} to
130
+ * swap the viewer, or `false` for a headless hub.
131
+ */
132
+ ui?: DevframeHubUi | false;
133
+ /** The hub's single auth gate. Gates by default; `false` opts out. */
134
+ auth?: InitHubOptions['auth'];
135
+ /**
136
+ * Expose the aggregate MCP endpoint at `<base>__mcp`. Default: `true`
137
+ * (the Next hub's agent surface rides the same catch-all route).
138
+ */
139
+ mcp?: InitHubOptions['mcp'];
140
+ /** Public origin the Next app is reachable at. Default: derived from `PORT`. */
141
+ origin?: InitHubOptions['origin'];
142
+ /** Publish this hub in the global instance registry. Default: off. */
143
+ register?: InitHubOptions['register'];
144
+ /** Override where persisted devframe state lives. */
145
+ getStorageDir?: InitHubOptions['getStorageDir'];
146
+ /** Name for the hub instance (logs, diagnostics, MCP server). */
147
+ name?: string;
148
+ /** Version for the hub instance (logs, diagnostics, MCP server). */
149
+ version?: string;
150
+ }
151
+ /**
152
+ * Build a devframes-hub for a Next.js App Router app: one `initHub()` call
153
+ * mounting every devframe under `<base><id>/` behind one web-standard
154
+ * `handler`, with the RPC socket on a side-car (Next routes can't accept WS
155
+ * upgrades) and the aggregate MCP route on by default. The UI defaults to
156
+ * `@devframes/hub-ui`'s `createUi()`, loaded lazily via a bundler-ignored
157
+ * dynamic `import()` so its asset lookups resolve at request time; pass `ui`
158
+ * to swap it or `ui: false` for a headless hub.
159
+ *
160
+ * Prefer {@link nextDevframeHub} at a route module — it memoizes this on
161
+ * `globalThis` so Next's dev-time route re-evaluation reuses one instance
162
+ * instead of leaking a side-car per reload.
163
+ */
164
+ declare function createNextDevframeHub(options?: NextDevframeHubOptions): Promise<HubInstance>;
165
+ /** A route-facing hub handle whose instance is memoized on `globalThis`. */
166
+ interface NextDevframeHubHandle {
167
+ /** The normalized mount base this hub answers under. */
168
+ base: string;
169
+ /** Delegate an App Router catch-all route straight to this. */
170
+ handler: (request: Request) => Promise<Response>;
171
+ /** Await the underlying {@link HubInstance} (building it on first access). */
172
+ ready: () => Promise<HubInstance>;
173
+ /** Tear down the memoized instance (side-car, MCP sessions) and forget it. */
174
+ close: () => Promise<void>;
175
+ }
176
+ /**
177
+ * Route-facing devframes-hub for a Next.js App Router app, memoized on
178
+ * `globalThis` by mount base so Next's dev-time route re-evaluation reuses
179
+ * one instance instead of leaking a side-car per reload. Build it once at the
180
+ * catch-all route module and delegate the verbs to it:
181
+ *
182
+ * ```ts
183
+ * // app/__devframes/[[...path]]/route.ts
184
+ * export const runtime = 'nodejs'
185
+ * export const dynamic = 'force-dynamic'
186
+ *
187
+ * import { nextDevframeHub } from '@devframes/next/hub'
188
+ *
189
+ * const hub = nextDevframeHub({ devframes: [] })
190
+ * export const GET = (req: Request) => hub.handler(req)
191
+ * export const POST = (req: Request) => hub.handler(req)
192
+ * export const DELETE = (req: Request) => hub.handler(req)
193
+ * ```
194
+ */
195
+ declare function nextDevframeHub(options?: NextDevframeHubOptions): NextDevframeHubHandle;
196
+ //#endregion
197
+ export { type CreateDevframeNextHostOptions, type DevframeNextHost, type DevframeNextHostMcpOptions, NextDevframeHubHandle, NextDevframeHubOptions, createDevframeNextHost, createNextDevframeHub, nextDevframeHub };
package/dist/hub.mjs ADDED
@@ -0,0 +1,182 @@
1
+ import { DEVFRAMES_HUB_BASE, normalizeHubBase } from "@devframes/hub/constants";
2
+ import { initHub } from "@devframes/hub/initiate";
3
+ import { DEVFRAME_CONNECTION_META_FILENAME } from "devframe/constants";
4
+ import { serveStaticHandler } from "devframe/utils/serve-static";
5
+ import { H3 } from "h3";
6
+ //#region src/host.ts
7
+ const META_SUFFIX = `/${DEVFRAME_CONNECTION_META_FILENAME}`;
8
+ /** Drop trailing slashes from a mount base (`/__git/` → `/__git`). */
9
+ function stripTrailingSlash(base) {
10
+ return base.replace(/\/+$/, "");
11
+ }
12
+ /**
13
+ * Build a Node-runtime {@link DevframeHost} for a Next.js App Router app that
14
+ * hosts one or more devframes, plus the single `fetch` handler its catch-all
15
+ * route delegates to.
16
+ *
17
+ * This is the hosted-adapter counterpart to `devframeViteBridge` for the Next
18
+ * runtime, which — being webpack/Turbopack rather than Vite — can't reuse the
19
+ * Vite middleware path. Instead of hand-rolling static serving in a route
20
+ * handler, static mounts are registered on an internal h3 app and served
21
+ * through devframe's shared `serveStaticHandler` (`app.fetch` makes h3 a
22
+ * WHATWG-`fetch` handler, exactly what an App Router route returns).
23
+ *
24
+ * Pins Node runtime (`export const runtime = 'nodejs'` in the route) because
25
+ * the static handler streams from the filesystem.
26
+ */
27
+ function createDevframeNextHost(options) {
28
+ const app = new H3();
29
+ const metaBases = /* @__PURE__ */ new Set();
30
+ const mcpMounts = /* @__PURE__ */ new Map();
31
+ let connectionMeta = options.connectionMeta;
32
+ const host = {
33
+ mountStatic(base, distDir) {
34
+ const staticApp = new H3();
35
+ staticApp.use(serveStaticHandler(distDir));
36
+ app.mount(stripTrailingSlash(base), staticApp);
37
+ },
38
+ mountConnectionMeta(base) {
39
+ metaBases.add(stripTrailingSlash(base));
40
+ },
41
+ resolveOrigin: options.resolveOrigin,
42
+ getStorageDir: options.getStorageDir
43
+ };
44
+ async function fetch(request) {
45
+ const { pathname } = new URL(request.url);
46
+ const mcp = mcpMounts.get(stripTrailingSlash(pathname));
47
+ if (mcp) return mcp.fetch(request);
48
+ if (pathname.endsWith(META_SUFFIX) && metaBases.has(pathname.slice(0, -META_SUFFIX.length))) {
49
+ if (!connectionMeta) return new Response(null, { status: 503 });
50
+ return Response.json(connectionMeta);
51
+ }
52
+ const response = await app.fetch(request);
53
+ if (response.status === 404) return new Response(null, { status: 404 });
54
+ return response;
55
+ }
56
+ return {
57
+ host,
58
+ fetch,
59
+ setConnectionMeta(meta) {
60
+ connectionMeta = meta;
61
+ },
62
+ async mountMcp(ctx, path, mcpOptions = {}) {
63
+ const { createMcpFetchHandler } = await import("devframe/adapters/mcp");
64
+ const handler = createMcpFetchHandler(ctx, {
65
+ serverName: mcpOptions.serverName ?? "devframe (next)",
66
+ serverVersion: mcpOptions.serverVersion ?? "0.0.0",
67
+ exposeSharedState: mcpOptions.exposeSharedState ?? true,
68
+ allowedOrigins: mcpOptions.allowedOrigins
69
+ });
70
+ const key = stripTrailingSlash(path);
71
+ mcpMounts.set(key, handler);
72
+ return { dispose: async () => {
73
+ mcpMounts.delete(key);
74
+ await handler.dispose();
75
+ } };
76
+ }
77
+ };
78
+ }
79
+ //#endregion
80
+ //#region src/hub.ts
81
+ /**
82
+ * Build a devframes-hub for a Next.js App Router app: one `initHub()` call
83
+ * mounting every devframe under `<base><id>/` behind one web-standard
84
+ * `handler`, with the RPC socket on a side-car (Next routes can't accept WS
85
+ * upgrades) and the aggregate MCP route on by default. The UI defaults to
86
+ * `@devframes/hub-ui`'s `createUi()`, loaded lazily via a bundler-ignored
87
+ * dynamic `import()` so its asset lookups resolve at request time; pass `ui`
88
+ * to swap it or `ui: false` for a headless hub.
89
+ *
90
+ * Prefer {@link nextDevframeHub} at a route module — it memoizes this on
91
+ * `globalThis` so Next's dev-time route re-evaluation reuses one instance
92
+ * instead of leaking a side-car per reload.
93
+ */
94
+ async function createNextDevframeHub(options = {}) {
95
+ const base = normalizeHubBase(options.base ?? DEVFRAMES_HUB_BASE);
96
+ const ui = options.ui === false ? void 0 : options.ui ?? await loadDefaultUi();
97
+ return initHub({
98
+ base,
99
+ ...options.cwd != null ? { cwd: options.cwd } : {},
100
+ ...options.host != null ? { host: options.host } : {},
101
+ ...options.origin != null ? { origin: options.origin } : {},
102
+ auth: options.auth,
103
+ ws: options.port != null ? { port: options.port } : { sidecar: true },
104
+ mcp: options.mcp ?? true,
105
+ ...ui ? { ui } : {},
106
+ ...options.renderers ? { renderers: options.renderers } : {},
107
+ ...options.rpcDeclarations ? { rpcDeclarations: options.rpcDeclarations } : {},
108
+ ...options.register != null ? { register: options.register } : {},
109
+ ...options.getStorageDir ? { getStorageDir: options.getStorageDir } : {},
110
+ ...options.name != null ? { name: options.name } : {},
111
+ ...options.version != null ? { version: options.version } : {},
112
+ ...options.devframes ? { devframes: options.devframes } : {},
113
+ ...options.configure ? { configure: options.configure } : {}
114
+ });
115
+ }
116
+ function hubRegistry() {
117
+ const g = globalThis;
118
+ return g.__devframesNextHubs ??= /* @__PURE__ */ new Map();
119
+ }
120
+ /**
121
+ * Route-facing devframes-hub for a Next.js App Router app, memoized on
122
+ * `globalThis` by mount base so Next's dev-time route re-evaluation reuses
123
+ * one instance instead of leaking a side-car per reload. Build it once at the
124
+ * catch-all route module and delegate the verbs to it:
125
+ *
126
+ * ```ts
127
+ * // app/__devframes/[[...path]]/route.ts
128
+ * export const runtime = 'nodejs'
129
+ * export const dynamic = 'force-dynamic'
130
+ *
131
+ * import { nextDevframeHub } from '@devframes/next/hub'
132
+ *
133
+ * const hub = nextDevframeHub({ devframes: [] })
134
+ * export const GET = (req: Request) => hub.handler(req)
135
+ * export const POST = (req: Request) => hub.handler(req)
136
+ * export const DELETE = (req: Request) => hub.handler(req)
137
+ * ```
138
+ */
139
+ function nextDevframeHub(options = {}) {
140
+ const base = normalizeHubBase(options.base ?? DEVFRAMES_HUB_BASE);
141
+ const ready = () => {
142
+ const registry = hubRegistry();
143
+ let instance = registry.get(base);
144
+ if (!instance) {
145
+ instance = createNextDevframeHub({
146
+ ...options,
147
+ base
148
+ });
149
+ registry.set(base, instance);
150
+ }
151
+ return instance;
152
+ };
153
+ return {
154
+ base,
155
+ async handler(request) {
156
+ return (await ready()).handler(request);
157
+ },
158
+ ready,
159
+ async close() {
160
+ const registry = hubRegistry();
161
+ const instance = registry.get(base);
162
+ if (!instance) return;
163
+ registry.delete(base);
164
+ await (await instance).close();
165
+ }
166
+ };
167
+ }
168
+ /**
169
+ * Load `@devframes/hub-ui`'s default UI through a bundler-ignored dynamic
170
+ * `import()`: `createUi()` resolves its prebuilt assets via `import.meta.url`,
171
+ * which only points at the published `dist` when Node loads the package at
172
+ * request time — a static import would be rewritten into a Next server chunk.
173
+ */
174
+ async function loadDefaultUi() {
175
+ return (await import(
176
+ /* webpackIgnore: true */
177
+ /* turbopackIgnore: true */
178
+ "@devframes/hub-ui"
179
+ )).createUi();
180
+ }
181
+ //#endregion
182
+ export { createDevframeNextHost, createNextDevframeHub, nextDevframeHub };
package/dist/index.d.mts CHANGED
@@ -1,203 +1 @@
1
- import { CreateDevServerOptions } from "devframe/adapters/dev";
2
- import { ConnectionMeta, DevframeDefinition, DevframeHost, DevframeNodeContext, DevframeStorageScope } from "devframe/types";
3
- //#region src/config.d.ts
4
- /**
5
- * Minimal shape of the bits of `next.config` this helper reads/writes, so the
6
- * package doesn't pull Next's full config type graph into its `.d.ts` bundle.
7
- * Any real `NextConfig` structurally satisfies it.
8
- */
9
- interface DevframeNextConfig {
10
- skipTrailingSlashRedirect?: boolean;
11
- [key: string]: unknown;
12
- }
13
- /**
14
- * Apply the Next config settings a devframe **host** requires, preserving
15
- * everything else.
16
- *
17
- * Sets `skipTrailingSlashRedirect: true`: mounted devframe SPAs are served at
18
- * `/__<id>/` and reference their assets relatively (`./_next/…`). Next's
19
- * default trailing-slash redirect (`/__git/` → `/__git`) would re-root those
20
- * relative paths and 404 every asset, leaving the panel unstyled and unable to
21
- * connect. Serving the base path verbatim keeps relative resolution intact.
22
- *
23
- * ```js [next.config.mjs]
24
- * import { withDevframe } from '@devframes/next'
25
- *
26
- * export default withDevframe({
27
- * // ...your own Next config
28
- * })
29
- * ```
30
- */
31
- declare function withDevframe<T extends DevframeNextConfig>(nextConfig?: T): T;
32
- //#endregion
33
- //#region src/handler.d.ts
34
- interface CreateDevframeNextHandlerOptions {
35
- /**
36
- * Mount base for the SPA. Defaults to `def.basePath ?? '/__<id>/'` — the
37
- * hosted-adapter default, so the devframe shares the Next app's origin
38
- * without colliding with its routes.
39
- */
40
- base?: string;
41
- /** Bind host for the side-car RPC/WS server. Default: `def.cli?.host ?? 'localhost'`. */
42
- host?: string;
43
- /** Pin the side-car port. Default: resolved from `def.cli?.port` via `get-port-please`. */
44
- port?: number;
45
- /** Flag bag forwarded to `def.setup(ctx, { flags })`. */
46
- flags?: Record<string, unknown>;
47
- /**
48
- * Whether the side-car runs its own auth gate. **Gates by default** (defers
49
- * to `createDevServer` — devframe's interactive OTP unless the definition's
50
- * `cli.auth` opts out), so the side-car socket isn't silently reachable by
51
- * anything that can open it. Pass `false` to opt out for a single-user
52
- * localhost host, or a handler for a custom scheme.
53
- */
54
- auth?: CreateDevServerOptions['auth'];
55
- /** Origin the Next app is reachable at, for docks needing an absolute URL. */
56
- resolveOrigin?: () => string;
57
- /** Override where persisted devframe state lives (defaults under the cwd / home). */
58
- getStorageDir?: (scope: DevframeStorageScope) => string;
59
- /**
60
- * Expose the side-car's route-based MCP server (Streamable-HTTP) and
61
- * advertise it in the handler's `__connection.json`. Forwarded to
62
- * `createDevServer`: overrides `def.cli?.mcp`, `undefined` falls through to
63
- * it, `false` disables the route regardless. The endpoint lives on the
64
- * side-car's own port, so the advertised meta carries `{ port, path }`.
65
- *
66
- * @experimental
67
- */
68
- mcp?: CreateDevServerOptions['mcp'];
69
- }
70
- interface DevframeNextHandler {
71
- /**
72
- * WHATWG-`fetch` handler for the catch-all App Router route. Serves the
73
- * plugin's built SPA at `base` and answers `<base>/__connection.json` with
74
- * the side-car WS endpoint. Awaits {@link DevframeNextHandler.ready} so the
75
- * first request doesn't race the server boot.
76
- */
77
- fetch: (request: Request) => Promise<Response>;
78
- /** Resolves once the side-car RPC/WS server is listening. */
79
- ready: Promise<void>;
80
- /** Shut the side-car server down (call from an app-lifecycle hook / test). */
81
- close: () => Promise<void>;
82
- }
83
- /**
84
- * Host a **single** devframe from a Next.js App Router app — the convenience
85
- * wrapper over {@link createDevframeNextHost} for the common case of mounting
86
- * one plugin (the Next counterpart to `viteDevBridge`'s bridge mode).
87
- *
88
- * It statically serves `def.cli.distDir` at `base` through the Next route and
89
- * starts a side-car RPC/WS dev server (via `createDevServer` in bridge mode) on
90
- * its own port, advertising that endpoint at `<base>/__connection.json` so the
91
- * SPA's `connectDevframe()` can dial back in.
92
- *
93
- * ```ts [app/__my-tool/[[...path]]/route.ts]
94
- * import myDevframe from '@/devframe'
95
- * import { createDevframeNextHandler } from '@devframes/next'
96
- *
97
- * export const runtime = 'nodejs'
98
- * export const dynamic = 'force-dynamic'
99
- *
100
- * const handler = createDevframeNextHandler(myDevframe)
101
- * export const GET = handler.fetch
102
- * ```
103
- *
104
- * For a hub hosting many devframes at once, use {@link createDevframeNextHost}
105
- * directly with `@devframes/hub`.
106
- */
107
- declare function createDevframeNextHandler(def: DevframeDefinition, options?: CreateDevframeNextHandlerOptions): DevframeNextHandler;
108
- //#endregion
109
- //#region src/host.d.ts
110
- interface CreateDevframeNextHostOptions {
111
- /**
112
- * Public origin the Next app is reachable at, e.g. `http://localhost:3000`.
113
- * Surfaced through {@link DevframeHost.resolveOrigin} for docks that need an
114
- * absolute iframe URL.
115
- */
116
- resolveOrigin: () => string;
117
- /**
118
- * Resolve a directory the host owns for persisted devframe state, per
119
- * {@link DevframeHost.getStorageDir}.
120
- */
121
- getStorageDir: (scope: DevframeStorageScope) => string;
122
- /**
123
- * Initial connection meta served at every base registered via
124
- * {@link DevframeHost.mountConnectionMeta}. Usually unknown until the
125
- * side-car RPC/WS server has started — publish it later with
126
- * {@link DevframeNextHost.setConnectionMeta}.
127
- */
128
- connectionMeta?: ConnectionMeta;
129
- }
130
- interface DevframeNextHostMcpOptions {
131
- /** Name reported in the MCP handshake. Default: `'devframe (next)'`. */
132
- serverName?: string;
133
- /** Version reported in the MCP handshake. Default: `'0.0.0'`. */
134
- serverVersion?: string;
135
- /** Expose shared-state keys as MCP resources / `devframe:state:read`. Default: `true`. */
136
- exposeSharedState?: boolean | ((key: string) => boolean);
137
- /**
138
- * Origin allow-list beyond the loopback default. `false` disables the
139
- * origin gate entirely. Note the MCP route rejects `Origin`-less requests
140
- * (see `createMcpFetchHandler`).
141
- */
142
- allowedOrigins?: readonly string[] | false;
143
- }
144
- interface DevframeNextHost {
145
- /**
146
- * The {@link DevframeHost} to hand to `createHubContext` / `createHostContext`.
147
- * Its `mountStatic` / `mountConnectionMeta` calls accumulate into the
148
- * {@link DevframeNextHost.fetch} handler below.
149
- */
150
- host: DevframeHost;
151
- /**
152
- * A WHATWG-`fetch` handler that serves every mounted SPA (with SPA
153
- * fallback, correct content types, and path-traversal guarding — all from
154
- * devframe's own `serveStaticHandler`) and answers `<base>/__connection.json`
155
- * for each base registered via `mountConnectionMeta`. Delegate a Next App
156
- * Router route handler straight to it:
157
- *
158
- * ```ts
159
- * export async function GET(request: Request) {
160
- * return (await ensureHub()).fetch(request)
161
- * }
162
- * ```
163
- */
164
- fetch: (request: Request) => Promise<Response>;
165
- /**
166
- * Publish the live connection meta once the RPC/WS server is up. Until this
167
- * is called (and without an initial `connectionMeta`), meta requests answer
168
- * `503` so a racing client retries rather than caching a wrong endpoint.
169
- */
170
- setConnectionMeta: (meta: ConnectionMeta) => void;
171
- /**
172
- * Serve an MCP Streamable-HTTP endpoint at `path` **in-process** — on the
173
- * Next app's own origin, through the same catch-all route as the SPAs (the
174
- * `/_next/mcp` shape). Built on `createMcpFetchHandler` from
175
- * `devframe/adapters/mcp` (imported lazily: `@modelcontextprotocol/server`
176
- * stays an optional peer). Advertise the path in the connection meta
177
- * (`mcp: { path }` — same origin, no port) and register the instance via
178
- * `registerDevframeInstance` so `devframe connect` can discover it.
179
- *
180
- * @experimental
181
- */
182
- mountMcp: (ctx: DevframeNodeContext, path: string, options?: DevframeNextHostMcpOptions) => Promise<{
183
- dispose: () => Promise<void>;
184
- }>;
185
- }
186
- /**
187
- * Build a Node-runtime {@link DevframeHost} for a Next.js App Router app that
188
- * hosts one or more devframes, plus the single `fetch` handler its catch-all
189
- * route delegates to.
190
- *
191
- * This is the hosted-adapter counterpart to `viteDevBridge` for the Next
192
- * runtime, which — being webpack/Turbopack rather than Vite — can't reuse the
193
- * Vite middleware path. Instead of hand-rolling static serving in a route
194
- * handler, static mounts are registered on an internal h3 app and served
195
- * through devframe's shared `serveStaticHandler` (`app.fetch` makes h3 a
196
- * WHATWG-`fetch` handler, exactly what an App Router route returns).
197
- *
198
- * Pins Node runtime (`export const runtime = 'nodejs'` in the route) because
199
- * the static handler streams from the filesystem.
200
- */
201
- declare function createDevframeNextHost(options: CreateDevframeNextHostOptions): DevframeNextHost;
202
- //#endregion
203
- export { type CreateDevframeNextHandlerOptions, type CreateDevframeNextHostOptions, type DevframeNextConfig, type DevframeNextHandler, type DevframeNextHost, type DevframeNextHostMcpOptions, createDevframeNextHandler, createDevframeNextHost, withDevframe };
1
+ export {}
package/dist/index.mjs CHANGED
@@ -1,188 +1,4 @@
1
- import { homedir } from "node:os";
2
- import { join } from "node:path";
3
- import process from "node:process";
4
- import { createDevServer, resolveDevServerPort, resolveMcpConnectionMeta } from "devframe/adapters/dev";
5
- import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_WS_ROUTE } from "devframe/constants";
6
- import { serveStaticHandler } from "devframe/utils/serve-static";
7
- import { H3 } from "h3";
8
- //#region src/config.ts
9
- /**
10
- * Apply the Next config settings a devframe **host** requires, preserving
11
- * everything else.
12
- *
13
- * Sets `skipTrailingSlashRedirect: true`: mounted devframe SPAs are served at
14
- * `/__<id>/` and reference their assets relatively (`./_next/…`). Next's
15
- * default trailing-slash redirect (`/__git/` → `/__git`) would re-root those
16
- * relative paths and 404 every asset, leaving the panel unstyled and unable to
17
- * connect. Serving the base path verbatim keeps relative resolution intact.
18
- *
19
- * ```js [next.config.mjs]
20
- * import { withDevframe } from '@devframes/next'
21
- *
22
- * export default withDevframe({
23
- * // ...your own Next config
24
- * })
25
- * ```
26
- */
27
- function withDevframe(nextConfig = {}) {
28
- return {
29
- ...nextConfig,
30
- skipTrailingSlashRedirect: nextConfig.skipTrailingSlashRedirect ?? true
31
- };
32
- }
1
+ //#region src/index.ts
2
+ throw new Error("[@devframes/next] has no root export. Import from a scoped subpath instead:\n • \"@devframes/next/single\" (+ \"/single/client\") — host one devframe's SPA\n • \"@devframes/next/hub\" (+ \"/hub/client\") — mount a devframes-hub\n");
33
3
  //#endregion
34
- //#region src/host.ts
35
- const META_SUFFIX = `/${DEVFRAME_CONNECTION_META_FILENAME}`;
36
- /** Drop trailing slashes from a mount base (`/__git/` → `/__git`). */
37
- function stripTrailingSlash(base) {
38
- return base.replace(/\/+$/, "");
39
- }
40
- /**
41
- * Build a Node-runtime {@link DevframeHost} for a Next.js App Router app that
42
- * hosts one or more devframes, plus the single `fetch` handler its catch-all
43
- * route delegates to.
44
- *
45
- * This is the hosted-adapter counterpart to `viteDevBridge` for the Next
46
- * runtime, which — being webpack/Turbopack rather than Vite — can't reuse the
47
- * Vite middleware path. Instead of hand-rolling static serving in a route
48
- * handler, static mounts are registered on an internal h3 app and served
49
- * through devframe's shared `serveStaticHandler` (`app.fetch` makes h3 a
50
- * WHATWG-`fetch` handler, exactly what an App Router route returns).
51
- *
52
- * Pins Node runtime (`export const runtime = 'nodejs'` in the route) because
53
- * the static handler streams from the filesystem.
54
- */
55
- function createDevframeNextHost(options) {
56
- const app = new H3();
57
- const metaBases = /* @__PURE__ */ new Set();
58
- const mcpMounts = /* @__PURE__ */ new Map();
59
- let connectionMeta = options.connectionMeta;
60
- const host = {
61
- mountStatic(base, distDir) {
62
- const staticApp = new H3();
63
- staticApp.use(serveStaticHandler(distDir));
64
- app.mount(stripTrailingSlash(base), staticApp);
65
- },
66
- mountConnectionMeta(base) {
67
- metaBases.add(stripTrailingSlash(base));
68
- },
69
- resolveOrigin: options.resolveOrigin,
70
- getStorageDir: options.getStorageDir
71
- };
72
- async function fetch(request) {
73
- const { pathname } = new URL(request.url);
74
- const mcp = mcpMounts.get(stripTrailingSlash(pathname));
75
- if (mcp) return mcp.fetch(request);
76
- if (pathname.endsWith(META_SUFFIX) && metaBases.has(pathname.slice(0, -META_SUFFIX.length))) {
77
- if (!connectionMeta) return new Response(null, { status: 503 });
78
- return Response.json(connectionMeta);
79
- }
80
- const response = await app.fetch(request);
81
- if (response.status === 404) return new Response(null, { status: 404 });
82
- return response;
83
- }
84
- return {
85
- host,
86
- fetch,
87
- setConnectionMeta(meta) {
88
- connectionMeta = meta;
89
- },
90
- async mountMcp(ctx, path, mcpOptions = {}) {
91
- const { createMcpFetchHandler } = await import("devframe/adapters/mcp");
92
- const handler = createMcpFetchHandler(ctx, {
93
- serverName: mcpOptions.serverName ?? "devframe (next)",
94
- serverVersion: mcpOptions.serverVersion ?? "0.0.0",
95
- exposeSharedState: mcpOptions.exposeSharedState ?? true,
96
- allowedOrigins: mcpOptions.allowedOrigins
97
- });
98
- const key = stripTrailingSlash(path);
99
- mcpMounts.set(key, handler);
100
- return { dispose: async () => {
101
- mcpMounts.delete(key);
102
- await handler.dispose();
103
- } };
104
- }
105
- };
106
- }
107
- //#endregion
108
- //#region src/handler.ts
109
- /** Ensure a mount base has a single leading and trailing slash. */
110
- function normalizeBase(base) {
111
- return `/${base}/`.replace(/\/{2,}/g, "/");
112
- }
113
- function defaultGetStorageDir(scope) {
114
- const cwd = process.cwd();
115
- if (scope === "workspace") return join(cwd, ".devframe");
116
- if (scope === "project") return join(cwd, "node_modules/.devframe");
117
- return join(homedir(), ".devframe");
118
- }
119
- /**
120
- * Host a **single** devframe from a Next.js App Router app — the convenience
121
- * wrapper over {@link createDevframeNextHost} for the common case of mounting
122
- * one plugin (the Next counterpart to `viteDevBridge`'s bridge mode).
123
- *
124
- * It statically serves `def.cli.distDir` at `base` through the Next route and
125
- * starts a side-car RPC/WS dev server (via `createDevServer` in bridge mode) on
126
- * its own port, advertising that endpoint at `<base>/__connection.json` so the
127
- * SPA's `connectDevframe()` can dial back in.
128
- *
129
- * ```ts [app/__my-tool/[[...path]]/route.ts]
130
- * import myDevframe from '@/devframe'
131
- * import { createDevframeNextHandler } from '@devframes/next'
132
- *
133
- * export const runtime = 'nodejs'
134
- * export const dynamic = 'force-dynamic'
135
- *
136
- * const handler = createDevframeNextHandler(myDevframe)
137
- * export const GET = handler.fetch
138
- * ```
139
- *
140
- * For a hub hosting many devframes at once, use {@link createDevframeNextHost}
141
- * directly with `@devframes/hub`.
142
- */
143
- function createDevframeNextHandler(def, options = {}) {
144
- const distDir = def.cli?.distDir;
145
- if (!distDir) throw new Error(`[@devframes/next] createDevframeNextHandler("${def.id}") needs a built SPA to serve, but "cli.distDir" is not set on the devframe definition.`);
146
- const base = normalizeBase(options.base ?? def.basePath ?? `/__${def.id}/`);
147
- const hostName = options.host ?? def.cli?.host;
148
- const nextHost = createDevframeNextHost({
149
- resolveOrigin: options.resolveOrigin ?? (() => ""),
150
- getStorageDir: options.getStorageDir ?? defaultGetStorageDir
151
- });
152
- nextHost.host.mountStatic(base, distDir);
153
- nextHost.host.mountConnectionMeta?.(base);
154
- let started;
155
- const ready = (async () => {
156
- const port = options.port ?? await resolveDevServerPort(def, { host: hostName });
157
- started = await createDevServer(def, {
158
- host: hostName,
159
- port,
160
- flags: options.flags,
161
- openBrowser: false,
162
- auth: options.auth,
163
- mcp: options.mcp
164
- });
165
- const mcpMeta = resolveMcpConnectionMeta(def, options.mcp, port);
166
- nextHost.setConnectionMeta({
167
- backend: "websocket",
168
- websocket: {
169
- port,
170
- path: `/${DEVFRAME_WS_ROUTE}`
171
- },
172
- ...mcpMeta ? { mcp: mcpMeta } : {}
173
- });
174
- })();
175
- return {
176
- async fetch(request) {
177
- await ready;
178
- return nextHost.fetch(request);
179
- },
180
- ready,
181
- async close() {
182
- await ready.catch(() => {});
183
- await started?.close();
184
- }
185
- };
186
- }
187
- //#endregion
188
- export { createDevframeNextHandler, createDevframeNextHost, withDevframe };
4
+ export {};
@@ -0,0 +1,110 @@
1
+ import { InitDevframeOptions } from "devframe/initiate";
2
+ import { DevframeDefinition, DevframeStorageScope } from "devframe";
3
+ //#region src/config.d.ts
4
+ /**
5
+ * Minimal shape of the bits of `next.config` this helper reads/writes, so the
6
+ * package doesn't pull Next's full config type graph into its `.d.ts` bundle.
7
+ * Any real `NextConfig` structurally satisfies it.
8
+ */
9
+ interface DevframeNextConfig {
10
+ skipTrailingSlashRedirect?: boolean;
11
+ [key: string]: unknown;
12
+ }
13
+ /**
14
+ * Apply the Next config settings a devframe **host** requires, preserving
15
+ * everything else.
16
+ *
17
+ * Sets `skipTrailingSlashRedirect: true`: mounted devframe SPAs are served at
18
+ * `/__<id>/` and reference their assets relatively (`./_next/…`). Next's
19
+ * default trailing-slash redirect (`/__git/` → `/__git`) would re-root those
20
+ * relative paths and 404 every asset, leaving the panel unstyled and unable to
21
+ * connect. Serving the base path verbatim keeps relative resolution intact.
22
+ *
23
+ * ```js [next.config.mjs]
24
+ * import { withDevframe } from '@devframes/next/single'
25
+ *
26
+ * export default withDevframe({
27
+ * // ...your own Next config
28
+ * })
29
+ * ```
30
+ */
31
+ declare function withDevframe<T extends DevframeNextConfig>(nextConfig?: T): T;
32
+ //#endregion
33
+ //#region src/handler.d.ts
34
+ interface CreateDevframeNextHandlerOptions {
35
+ /**
36
+ * Mount base for the SPA. Defaults to `def.basePath ?? '/__<id>/'` — the
37
+ * hosted-adapter default, so the devframe shares the Next app's origin
38
+ * without colliding with its routes.
39
+ */
40
+ base?: string;
41
+ /** Bind host for the side-car RPC/WS server. Default: `def.cli?.host ?? 'localhost'`. */
42
+ host?: string;
43
+ /** Pin the side-car port. Default: resolved from `def.cli?.port` via `get-port-please`. */
44
+ port?: number;
45
+ /** Flag bag forwarded to `def.setup(ctx, { flags })`. */
46
+ flags?: Record<string, unknown>;
47
+ /**
48
+ * Whether the side-car runs its own auth gate. **Gates by default**
49
+ * (devframe's interactive OTP unless the definition's `cli.auth` opts
50
+ * out), so the side-car socket isn't silently reachable by anything that
51
+ * can open it. Pass `false` to opt out for a single-user localhost host,
52
+ * or a handler for a custom scheme.
53
+ */
54
+ auth?: InitDevframeOptions['auth'];
55
+ /** Origin the Next app is reachable at, for docks needing an absolute URL. */
56
+ resolveOrigin?: () => string;
57
+ /** Override where persisted devframe state lives (defaults under the cwd / home). */
58
+ getStorageDir?: (scope: DevframeStorageScope) => string;
59
+ /**
60
+ * Expose the route-based MCP server (Streamable-HTTP) at `<base>__mcp` —
61
+ * on the Next app's own origin, through the same catch-all route as the
62
+ * SPA — and advertise it in the handler's `__connection.json`. Overrides
63
+ * `def.cli?.mcp`, `undefined` falls through to it, `false` disables the
64
+ * route regardless.
65
+ */
66
+ mcp?: InitDevframeOptions['mcp'];
67
+ /**
68
+ * Memoization key for the handler. Next re-runs route modules across
69
+ * dev-time reloads; the key makes a re-run return the live handler instead
70
+ * of leaking side-car servers. Default: `@devframes/next:<def.id>:<base>`.
71
+ */
72
+ key?: string;
73
+ }
74
+ interface DevframeNextHandler {
75
+ /**
76
+ * WHATWG-`fetch` handler for the catch-all App Router route. Serves the
77
+ * plugin's built SPA at `base` and answers `<base>__connection.json` with
78
+ * the RPC endpoint. Awaits {@link DevframeNextHandler.ready} so the first
79
+ * request doesn't race the server boot.
80
+ */
81
+ fetch: (request: Request) => Promise<Response>;
82
+ /** Resolves once the side-car RPC/WS server is listening. */
83
+ ready: Promise<void>;
84
+ /** Shut the side-car server down (call from an app-lifecycle hook / test). */
85
+ close: () => Promise<void>;
86
+ }
87
+ /**
88
+ * Host a **single** devframe from a Next.js App Router app — the Next
89
+ * counterpart to `devframeViteBridge`, reduced to memoization + defaults over
90
+ * `initDevframe` (Next's route handlers can't accept WS upgrades, so the
91
+ * RPC socket lives on the instance's side-car port, advertised at
92
+ * `<base>__connection.json`).
93
+ *
94
+ * ```ts [app/%5F_my-tool/[[...path]]/route.ts]
95
+ * import myDevframe from '@/devframe'
96
+ * import { createDevframeNextHandler } from '@devframes/next/single'
97
+ *
98
+ * export const runtime = 'nodejs'
99
+ * export const dynamic = 'force-dynamic'
100
+ *
101
+ * const handler = createDevframeNextHandler(myDevframe)
102
+ * export const GET = handler.fetch
103
+ * ```
104
+ *
105
+ * For a hub hosting many devframes at once, use `createDevframeNextHost`
106
+ * directly with `@devframes/hub`.
107
+ */
108
+ declare function createDevframeNextHandler(def: DevframeDefinition, options?: CreateDevframeNextHandlerOptions): DevframeNextHandler;
109
+ //#endregion
110
+ export { type CreateDevframeNextHandlerOptions, type DevframeNextConfig, type DevframeNextHandler, createDevframeNextHandler, withDevframe };
@@ -0,0 +1,105 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import process from "node:process";
4
+ import { initDevframe } from "devframe/initiate";
5
+ //#region src/config.ts
6
+ /**
7
+ * Apply the Next config settings a devframe **host** requires, preserving
8
+ * everything else.
9
+ *
10
+ * Sets `skipTrailingSlashRedirect: true`: mounted devframe SPAs are served at
11
+ * `/__<id>/` and reference their assets relatively (`./_next/…`). Next's
12
+ * default trailing-slash redirect (`/__git/` → `/__git`) would re-root those
13
+ * relative paths and 404 every asset, leaving the panel unstyled and unable to
14
+ * connect. Serving the base path verbatim keeps relative resolution intact.
15
+ *
16
+ * ```js [next.config.mjs]
17
+ * import { withDevframe } from '@devframes/next/single'
18
+ *
19
+ * export default withDevframe({
20
+ * // ...your own Next config
21
+ * })
22
+ * ```
23
+ */
24
+ function withDevframe(nextConfig = {}) {
25
+ return {
26
+ ...nextConfig,
27
+ skipTrailingSlashRedirect: nextConfig.skipTrailingSlashRedirect ?? true
28
+ };
29
+ }
30
+ //#endregion
31
+ //#region src/handler.ts
32
+ /** Ensure a mount base has a single leading and trailing slash. */
33
+ function normalizeBase(base) {
34
+ return `/${base}/`.replace(/\/{2,}/g, "/");
35
+ }
36
+ const REGISTRY_KEY = Symbol.for("@devframes/next:handler-registry");
37
+ /**
38
+ * Handlers memoized per key on `globalThis`. Next re-evaluates route modules
39
+ * on every dev-time reload, so without this each reload would start a fresh
40
+ * side-car WebSocket server and leak the previous one.
41
+ */
42
+ function handlerRegistry() {
43
+ const holder = globalThis;
44
+ holder[REGISTRY_KEY] ??= /* @__PURE__ */ new Map();
45
+ return holder[REGISTRY_KEY];
46
+ }
47
+ function defaultGetStorageDir(scope) {
48
+ const cwd = process.cwd();
49
+ if (scope === "workspace") return join(cwd, ".devframe");
50
+ if (scope === "project") return join(cwd, "node_modules/.devframe");
51
+ return join(homedir(), ".devframe");
52
+ }
53
+ /**
54
+ * Host a **single** devframe from a Next.js App Router app — the Next
55
+ * counterpart to `devframeViteBridge`, reduced to memoization + defaults over
56
+ * `initDevframe` (Next's route handlers can't accept WS upgrades, so the
57
+ * RPC socket lives on the instance's side-car port, advertised at
58
+ * `<base>__connection.json`).
59
+ *
60
+ * ```ts [app/%5F_my-tool/[[...path]]/route.ts]
61
+ * import myDevframe from '@/devframe'
62
+ * import { createDevframeNextHandler } from '@devframes/next/single'
63
+ *
64
+ * export const runtime = 'nodejs'
65
+ * export const dynamic = 'force-dynamic'
66
+ *
67
+ * const handler = createDevframeNextHandler(myDevframe)
68
+ * export const GET = handler.fetch
69
+ * ```
70
+ *
71
+ * For a hub hosting many devframes at once, use `createDevframeNextHost`
72
+ * directly with `@devframes/hub`.
73
+ */
74
+ function createDevframeNextHandler(def, options = {}) {
75
+ const distDir = def.cli?.distDir;
76
+ if (!distDir) throw new Error(`[@devframes/next] createDevframeNextHandler("${def.id}") needs a built SPA to serve, but "cli.distDir" is not set on the devframe definition.`);
77
+ const base = normalizeBase(options.base ?? def.basePath ?? `/__${def.id}/`);
78
+ const key = options.key ?? `@devframes/next:${def.id}:${base}`;
79
+ const registry = handlerRegistry();
80
+ const memoized = registry.get(key);
81
+ if (memoized) return memoized;
82
+ const instance = initDevframe(def, {
83
+ base,
84
+ distDir,
85
+ host: options.host,
86
+ flags: options.flags,
87
+ auth: options.auth,
88
+ mcp: options.mcp,
89
+ ws: options.port != null ? { port: options.port } : { sidecar: true },
90
+ ...options.resolveOrigin ? { origin: options.resolveOrigin } : {},
91
+ getStorageDir: options.getStorageDir ?? defaultGetStorageDir
92
+ });
93
+ const handler = {
94
+ fetch: (request) => instance.handler(request),
95
+ ready: instance.ready,
96
+ close: async () => {
97
+ if (registry.get(key) === handler) registry.delete(key);
98
+ await instance.close();
99
+ }
100
+ };
101
+ registry.set(key, handler);
102
+ return handler;
103
+ }
104
+ //#endregion
105
+ export { createDevframeNextHandler, withDevframe };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@devframes/next",
3
3
  "type": "module",
4
- "version": "0.8.2",
5
- "description": "Next.js host integration for Devframe — serve mounted devframe SPAs and connection meta from an App Router route (experimental)",
4
+ "version": "0.9.0-beta.10",
5
+ "description": "Next.js host integration that serves mounted devframes from an App Router route.",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
8
8
  "homepage": "https://github.com/devframes/devframe#readme",
@@ -24,10 +24,22 @@
24
24
  "types": "./dist/index.d.mts",
25
25
  "default": "./dist/index.mjs"
26
26
  },
27
- "./client": {
27
+ "./single": {
28
+ "types": "./dist/single.d.mts",
29
+ "default": "./dist/single.mjs"
30
+ },
31
+ "./single/client": {
28
32
  "types": "./dist/client.d.mts",
29
33
  "default": "./dist/client.mjs"
30
34
  },
35
+ "./hub": {
36
+ "types": "./dist/hub.d.mts",
37
+ "default": "./dist/hub.mjs"
38
+ },
39
+ "./hub/client": {
40
+ "types": "./dist/hub-client.d.mts",
41
+ "default": "./dist/hub-client.mjs"
42
+ },
31
43
  "./package.json": "./package.json"
32
44
  },
33
45
  "types": "./dist/index.d.mts",
@@ -37,9 +49,17 @@
37
49
  "peerDependencies": {
38
50
  "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
39
51
  "react": "^18.0.0 || ^19.0.0",
40
- "devframe": "0.8.2"
52
+ "@devframes/hub": "0.9.0-beta.10",
53
+ "@devframes/hub-ui": "0.9.0-beta.10",
54
+ "devframe": "0.9.0-beta.10"
41
55
  },
42
56
  "peerDependenciesMeta": {
57
+ "@devframes/hub": {
58
+ "optional": true
59
+ },
60
+ "@devframes/hub-ui": {
61
+ "optional": true
62
+ },
43
63
  "next": {
44
64
  "optional": true
45
65
  },
@@ -51,11 +71,13 @@
51
71
  "h3": "^2.0.1-rc.26"
52
72
  },
53
73
  "devDependencies": {
54
- "@types/node": "^26.1.2",
74
+ "@types/node": "^26.2.0",
55
75
  "@types/react": "^19.2.18",
56
76
  "react": "^19.2.8",
57
77
  "tsdown": "^0.22.14",
58
- "devframe": "0.8.2"
78
+ "@devframes/hub": "0.9.0-beta.10",
79
+ "@devframes/hub-ui": "0.9.0-beta.10",
80
+ "devframe": "0.9.0-beta.10"
59
81
  },
60
82
  "scripts": {
61
83
  "build": "tsdown",