@devframes/next 0.9.0-beta.1 → 0.9.0-beta.3

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.
@@ -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/dev-spa'
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/dev-spa'
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/dev-spa'
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/dev-spa'
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 };
@@ -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,51 @@
1
+ "use client";
2
+ import { useEffect, useState } from "react";
3
+ import { createDevframeClientHost } from "@devframes/hub/client";
4
+ //#region src/hub-client.tsx
5
+ /** Default hub mount base — mirrors `@devframes/hub`'s `DEVFRAMES_HUB_BASE`. */
6
+ const DEVFRAMES_HUB_BASE = "/__devframes/";
7
+ /**
8
+ * Boot the devframes-hub **client runtime** inside a React (Next.js) page —
9
+ * the browser half of {@link import('./hub').nextDevframeHub}. Connects RPC to
10
+ * the hub (defaulting `base` to `/__devframes/`), assembles the shared
11
+ * `DevframeClientContext`, imports each dock's client script into the page,
12
+ * and disposes on unmount. Returns the {@link DevframeClientHost} once ready
13
+ * (`null` while connecting).
14
+ *
15
+ * Only needed when you render your own dock UI (or override the hub's `ui`).
16
+ * With the default `@devframes/hub-ui`, its injected `embedded.js` boots the
17
+ * client for you, so the page needs no client code.
18
+ *
19
+ * `renderers` is read once on mount; memoize it at the call site if it isn't a
20
+ * stable reference.
21
+ */
22
+ function useDevframeHubClient(options = {}) {
23
+ const { base = DEVFRAMES_HUB_BASE, rpc, connect } = options;
24
+ const [host, setHost] = useState(null);
25
+ useEffect(() => {
26
+ let disposed = false;
27
+ let created;
28
+ createDevframeClientHost({
29
+ ...options,
30
+ ...rpc ? { rpc } : { connect: {
31
+ baseURL: base,
32
+ ...connect
33
+ } }
34
+ }).then((next) => {
35
+ if (disposed) {
36
+ next.dispose();
37
+ return;
38
+ }
39
+ created = next;
40
+ setHost(next);
41
+ });
42
+ return () => {
43
+ disposed = true;
44
+ created?.dispose();
45
+ setHost(null);
46
+ };
47
+ }, [base, rpc]);
48
+ return host;
49
+ }
50
+ //#endregion
51
+ 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,185 @@
1
+ import { DEVFRAMES_HUB_BASE, initHub } from "@devframes/hub/initiate";
2
+ import { cleanDoubleSlashes, withLeadingSlash, withTrailingSlash } from "ufo";
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 = normalizeBase(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 = normalizeBase(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
+ function normalizeBase(base) {
182
+ return cleanDoubleSlashes(withTrailingSlash(withLeadingSlash(base)));
183
+ }
184
+ //#endregion
185
+ export { createDevframeNextHost, createNextDevframeHub, nextDevframeHub };
package/dist/index.d.mts CHANGED
@@ -1,206 +1 @@
1
- import { InitDevframeOptions } from "devframe/initiate";
2
- import { ConnectionMeta, DevframeDefinition, DevframeHost, DevframeNodeContext, 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'
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
- * @experimental
67
- */
68
- mcp?: InitDevframeOptions['mcp'];
69
- /**
70
- * Memoization key for the handler. Next re-runs route modules across
71
- * dev-time reloads; the key makes a re-run return the live handler instead
72
- * of leaking side-car servers. Default: `@devframes/next:<def.id>:<base>`.
73
- */
74
- key?: string;
75
- }
76
- interface DevframeNextHandler {
77
- /**
78
- * WHATWG-`fetch` handler for the catch-all App Router route. Serves the
79
- * plugin's built SPA at `base` and answers `<base>__connection.json` with
80
- * the RPC endpoint. Awaits {@link DevframeNextHandler.ready} so the first
81
- * request doesn't race the server boot.
82
- */
83
- fetch: (request: Request) => Promise<Response>;
84
- /** Resolves once the side-car RPC/WS server is listening. */
85
- ready: Promise<void>;
86
- /** Shut the side-car server down (call from an app-lifecycle hook / test). */
87
- close: () => Promise<void>;
88
- }
89
- /**
90
- * Host a **single** devframe from a Next.js App Router app — the Next
91
- * counterpart to `viteDevBridge`, reduced to memoization + defaults over
92
- * `initDevframe` (Next's route handlers can't accept WS upgrades, so the
93
- * RPC socket lives on the instance's side-car port, advertised at
94
- * `<base>__connection.json`).
95
- *
96
- * ```ts [app/%5F_my-tool/[[...path]]/route.ts]
97
- * import myDevframe from '@/devframe'
98
- * import { createDevframeNextHandler } from '@devframes/next'
99
- *
100
- * export const runtime = 'nodejs'
101
- * export const dynamic = 'force-dynamic'
102
- *
103
- * const handler = createDevframeNextHandler(myDevframe)
104
- * export const GET = handler.fetch
105
- * ```
106
- *
107
- * For a hub hosting many devframes at once, use `createDevframeNextHost`
108
- * directly with `@devframes/hub`.
109
- */
110
- declare function createDevframeNextHandler(def: DevframeDefinition, options?: CreateDevframeNextHandlerOptions): DevframeNextHandler;
111
- //#endregion
112
- //#region src/host.d.ts
113
- interface CreateDevframeNextHostOptions {
114
- /**
115
- * Public origin the Next app is reachable at, e.g. `http://localhost:3000`.
116
- * Surfaced through {@link DevframeHost.resolveOrigin} for docks that need an
117
- * absolute iframe URL.
118
- */
119
- resolveOrigin: () => string;
120
- /**
121
- * Resolve a directory the host owns for persisted devframe state, per
122
- * {@link DevframeHost.getStorageDir}.
123
- */
124
- getStorageDir: (scope: DevframeStorageScope) => string;
125
- /**
126
- * Initial connection meta served at every base registered via
127
- * {@link DevframeHost.mountConnectionMeta}. Usually unknown until the
128
- * side-car RPC/WS server has started — publish it later with
129
- * {@link DevframeNextHost.setConnectionMeta}.
130
- */
131
- connectionMeta?: ConnectionMeta;
132
- }
133
- interface DevframeNextHostMcpOptions {
134
- /** Name reported in the MCP handshake. Default: `'devframe (next)'`. */
135
- serverName?: string;
136
- /** Version reported in the MCP handshake. Default: `'0.0.0'`. */
137
- serverVersion?: string;
138
- /** Expose shared-state keys as MCP resources / `devframe:state:read`. Default: `true`. */
139
- exposeSharedState?: boolean | ((key: string) => boolean);
140
- /**
141
- * Origin allow-list beyond the loopback default. `false` disables the
142
- * origin gate entirely. Note the MCP route rejects `Origin`-less requests
143
- * (see `createMcpFetchHandler`).
144
- */
145
- allowedOrigins?: readonly string[] | false;
146
- }
147
- interface DevframeNextHost {
148
- /**
149
- * The {@link DevframeHost} to hand to `createHubContext` / `createHostContext`.
150
- * Its `mountStatic` / `mountConnectionMeta` calls accumulate into the
151
- * {@link DevframeNextHost.fetch} handler below.
152
- */
153
- host: DevframeHost;
154
- /**
155
- * A WHATWG-`fetch` handler that serves every mounted SPA (with SPA
156
- * fallback, correct content types, and path-traversal guarding — all from
157
- * devframe's own `serveStaticHandler`) and answers `<base>/__connection.json`
158
- * for each base registered via `mountConnectionMeta`. Delegate a Next App
159
- * Router route handler straight to it:
160
- *
161
- * ```ts
162
- * export async function GET(request: Request) {
163
- * return (await ensureHub()).fetch(request)
164
- * }
165
- * ```
166
- */
167
- fetch: (request: Request) => Promise<Response>;
168
- /**
169
- * Publish the live connection meta once the RPC/WS server is up. Until this
170
- * is called (and without an initial `connectionMeta`), meta requests answer
171
- * `503` so a racing client retries rather than caching a wrong endpoint.
172
- */
173
- setConnectionMeta: (meta: ConnectionMeta) => void;
174
- /**
175
- * Serve an MCP Streamable-HTTP endpoint at `path` **in-process** — on the
176
- * Next app's own origin, through the same catch-all route as the SPAs (the
177
- * `/_next/mcp` shape). Built on `createMcpFetchHandler` from
178
- * `devframe/adapters/mcp` (imported lazily: `@modelcontextprotocol/server`
179
- * stays an optional peer). Advertise the path in the connection meta
180
- * (`mcp: { path }` — same origin, no port) and register the instance via
181
- * `registerDevframeInstance` so `devframe connect` can discover it.
182
- *
183
- * @experimental
184
- */
185
- mountMcp: (ctx: DevframeNodeContext, path: string, options?: DevframeNextHostMcpOptions) => Promise<{
186
- dispose: () => Promise<void>;
187
- }>;
188
- }
189
- /**
190
- * Build a Node-runtime {@link DevframeHost} for a Next.js App Router app that
191
- * hosts one or more devframes, plus the single `fetch` handler its catch-all
192
- * route delegates to.
193
- *
194
- * This is the hosted-adapter counterpart to `viteDevBridge` for the Next
195
- * runtime, which — being webpack/Turbopack rather than Vite — can't reuse the
196
- * Vite middleware path. Instead of hand-rolling static serving in a route
197
- * handler, static mounts are registered on an internal h3 app and served
198
- * through devframe's shared `serveStaticHandler` (`app.fetch` makes h3 a
199
- * WHATWG-`fetch` handler, exactly what an App Router route returns).
200
- *
201
- * Pins Node runtime (`export const runtime = 'nodejs'` in the route) because
202
- * the static handler streams from the filesystem.
203
- */
204
- declare function createDevframeNextHost(options: CreateDevframeNextHostOptions): DevframeNextHost;
205
- //#endregion
206
- export { type CreateDevframeNextHandlerOptions, type CreateDevframeNextHostOptions, type DevframeNextConfig, type DevframeNextHandler, type DevframeNextHost, type DevframeNextHostMcpOptions, createDevframeNextHandler, createDevframeNextHost, withDevframe };
1
+ export {}
package/dist/index.mjs CHANGED
@@ -1,182 +1,4 @@
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
- import { DEVFRAME_CONNECTION_META_FILENAME } 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/dev-spa\" (+ \"/dev-spa/client\") — host one devframe's SPA\n • \"@devframes/next/hub\" (+ \"/hub/client\") — mount a devframes-hub\n");
33
3
  //#endregion
34
- //#region src/handler.ts
35
- /** Ensure a mount base has a single leading and trailing slash. */
36
- function normalizeBase(base) {
37
- return `/${base}/`.replace(/\/{2,}/g, "/");
38
- }
39
- const REGISTRY_KEY = Symbol.for("@devframes/next:handler-registry");
40
- /**
41
- * Handlers memoized per key on `globalThis`. Next re-evaluates route modules
42
- * on every dev-time reload, so without this each reload would start a fresh
43
- * side-car WebSocket server and leak the previous one.
44
- */
45
- function handlerRegistry() {
46
- const holder = globalThis;
47
- holder[REGISTRY_KEY] ??= /* @__PURE__ */ new Map();
48
- return holder[REGISTRY_KEY];
49
- }
50
- function defaultGetStorageDir(scope) {
51
- const cwd = process.cwd();
52
- if (scope === "workspace") return join(cwd, ".devframe");
53
- if (scope === "project") return join(cwd, "node_modules/.devframe");
54
- return join(homedir(), ".devframe");
55
- }
56
- /**
57
- * Host a **single** devframe from a Next.js App Router app — the Next
58
- * counterpart to `viteDevBridge`, reduced to memoization + defaults over
59
- * `initDevframe` (Next's route handlers can't accept WS upgrades, so the
60
- * RPC socket lives on the instance's side-car port, advertised at
61
- * `<base>__connection.json`).
62
- *
63
- * ```ts [app/%5F_my-tool/[[...path]]/route.ts]
64
- * import myDevframe from '@/devframe'
65
- * import { createDevframeNextHandler } from '@devframes/next'
66
- *
67
- * export const runtime = 'nodejs'
68
- * export const dynamic = 'force-dynamic'
69
- *
70
- * const handler = createDevframeNextHandler(myDevframe)
71
- * export const GET = handler.fetch
72
- * ```
73
- *
74
- * For a hub hosting many devframes at once, use `createDevframeNextHost`
75
- * directly with `@devframes/hub`.
76
- */
77
- function createDevframeNextHandler(def, options = {}) {
78
- const distDir = def.cli?.distDir;
79
- 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.`);
80
- const base = normalizeBase(options.base ?? def.basePath ?? `/__${def.id}/`);
81
- const key = options.key ?? `@devframes/next:${def.id}:${base}`;
82
- const registry = handlerRegistry();
83
- const memoized = registry.get(key);
84
- if (memoized) return memoized;
85
- const instance = initDevframe(def, {
86
- base,
87
- distDir,
88
- host: options.host,
89
- flags: options.flags,
90
- auth: options.auth,
91
- mcp: options.mcp,
92
- ws: options.port != null ? { port: options.port } : { sidecar: true },
93
- ...options.resolveOrigin ? { origin: options.resolveOrigin } : {},
94
- getStorageDir: options.getStorageDir ?? defaultGetStorageDir
95
- });
96
- const handler = {
97
- fetch: (request) => instance.handler(request),
98
- ready: instance.ready,
99
- close: async () => {
100
- if (registry.get(key) === handler) registry.delete(key);
101
- await instance.close();
102
- }
103
- };
104
- registry.set(key, handler);
105
- return handler;
106
- }
107
- //#endregion
108
- //#region src/host.ts
109
- const META_SUFFIX = `/${DEVFRAME_CONNECTION_META_FILENAME}`;
110
- /** Drop trailing slashes from a mount base (`/__git/` → `/__git`). */
111
- function stripTrailingSlash(base) {
112
- return base.replace(/\/+$/, "");
113
- }
114
- /**
115
- * Build a Node-runtime {@link DevframeHost} for a Next.js App Router app that
116
- * hosts one or more devframes, plus the single `fetch` handler its catch-all
117
- * route delegates to.
118
- *
119
- * This is the hosted-adapter counterpart to `viteDevBridge` for the Next
120
- * runtime, which — being webpack/Turbopack rather than Vite — can't reuse the
121
- * Vite middleware path. Instead of hand-rolling static serving in a route
122
- * handler, static mounts are registered on an internal h3 app and served
123
- * through devframe's shared `serveStaticHandler` (`app.fetch` makes h3 a
124
- * WHATWG-`fetch` handler, exactly what an App Router route returns).
125
- *
126
- * Pins Node runtime (`export const runtime = 'nodejs'` in the route) because
127
- * the static handler streams from the filesystem.
128
- */
129
- function createDevframeNextHost(options) {
130
- const app = new H3();
131
- const metaBases = /* @__PURE__ */ new Set();
132
- const mcpMounts = /* @__PURE__ */ new Map();
133
- let connectionMeta = options.connectionMeta;
134
- const host = {
135
- mountStatic(base, distDir) {
136
- const staticApp = new H3();
137
- staticApp.use(serveStaticHandler(distDir));
138
- app.mount(stripTrailingSlash(base), staticApp);
139
- },
140
- mountConnectionMeta(base) {
141
- metaBases.add(stripTrailingSlash(base));
142
- },
143
- resolveOrigin: options.resolveOrigin,
144
- getStorageDir: options.getStorageDir
145
- };
146
- async function fetch(request) {
147
- const { pathname } = new URL(request.url);
148
- const mcp = mcpMounts.get(stripTrailingSlash(pathname));
149
- if (mcp) return mcp.fetch(request);
150
- if (pathname.endsWith(META_SUFFIX) && metaBases.has(pathname.slice(0, -META_SUFFIX.length))) {
151
- if (!connectionMeta) return new Response(null, { status: 503 });
152
- return Response.json(connectionMeta);
153
- }
154
- const response = await app.fetch(request);
155
- if (response.status === 404) return new Response(null, { status: 404 });
156
- return response;
157
- }
158
- return {
159
- host,
160
- fetch,
161
- setConnectionMeta(meta) {
162
- connectionMeta = meta;
163
- },
164
- async mountMcp(ctx, path, mcpOptions = {}) {
165
- const { createMcpFetchHandler } = await import("devframe/adapters/mcp");
166
- const handler = createMcpFetchHandler(ctx, {
167
- serverName: mcpOptions.serverName ?? "devframe (next)",
168
- serverVersion: mcpOptions.serverVersion ?? "0.0.0",
169
- exposeSharedState: mcpOptions.exposeSharedState ?? true,
170
- allowedOrigins: mcpOptions.allowedOrigins
171
- });
172
- const key = stripTrailingSlash(path);
173
- mcpMounts.set(key, handler);
174
- return { dispose: async () => {
175
- mcpMounts.delete(key);
176
- await handler.dispose();
177
- } };
178
- }
179
- };
180
- }
181
- //#endregion
182
- export { createDevframeNextHandler, createDevframeNextHost, withDevframe };
4
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@devframes/next",
3
3
  "type": "module",
4
- "version": "0.9.0-beta.1",
4
+ "version": "0.9.0-beta.3",
5
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",
@@ -24,10 +24,22 @@
24
24
  "types": "./dist/index.d.mts",
25
25
  "default": "./dist/index.mjs"
26
26
  },
27
- "./client": {
27
+ "./dev-spa": {
28
+ "types": "./dist/dev-spa.d.mts",
29
+ "default": "./dist/dev-spa.mjs"
30
+ },
31
+ "./dev-spa/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.9.0-beta.1"
52
+ "@devframes/hub": "0.9.0-beta.3",
53
+ "@devframes/hub-ui": "0.9.0-beta.3",
54
+ "devframe": "0.9.0-beta.3"
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
  },
@@ -48,14 +68,17 @@
48
68
  }
49
69
  },
50
70
  "dependencies": {
51
- "h3": "^2.0.1-rc.26"
71
+ "h3": "^2.0.1-rc.26",
72
+ "ufo": "^1.6.4"
52
73
  },
53
74
  "devDependencies": {
54
75
  "@types/node": "^26.2.0",
55
76
  "@types/react": "^19.2.18",
56
77
  "react": "^19.2.8",
57
78
  "tsdown": "^0.22.14",
58
- "devframe": "0.9.0-beta.1"
79
+ "@devframes/hub": "0.9.0-beta.3",
80
+ "devframe": "0.9.0-beta.3",
81
+ "@devframes/hub-ui": "0.9.0-beta.3"
59
82
  },
60
83
  "scripts": {
61
84
  "build": "tsdown",