@devframes/next 0.9.0-beta.2 → 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.
- package/dist/dev-spa.d.mts +110 -0
- package/dist/dev-spa.mjs +105 -0
- package/dist/hub-client.d.mts +29 -0
- package/dist/hub-client.mjs +51 -0
- package/dist/hub.d.mts +197 -0
- package/dist/hub.mjs +185 -0
- package/dist/index.d.mts +1 -206
- package/dist/index.mjs +3 -181
- package/package.json +28 -5
|
@@ -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 };
|
package/dist/dev-spa.mjs
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
"./
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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",
|