@devframes/next 0.7.14
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/LICENSE.md +21 -0
- package/dist/client.d.mts +62 -0
- package/dist/client.mjs +95 -0
- package/dist/index.d.mts +173 -0
- package/dist/index.mjs +170 -0
- package/package.json +65 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-PRESENT Anthony Fu <https://github.com/antfu>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { DevframeConnectionStatus, DevframeRpcClient } from "devframe/client";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
import { ConnectionMeta } from "devframe/types";
|
|
4
|
+
//#region src/client.d.ts
|
|
5
|
+
interface DevframeRpcState {
|
|
6
|
+
/** The connected client, or `null` while the initial connect is in flight. */
|
|
7
|
+
rpc: DevframeRpcClient | null;
|
|
8
|
+
/** Live connection status — `'connecting'` until the client resolves. */
|
|
9
|
+
status: DevframeConnectionStatus;
|
|
10
|
+
/** The latest connection error, or `null`. */
|
|
11
|
+
error: Error | null;
|
|
12
|
+
}
|
|
13
|
+
interface RpcProviderProps {
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* Base URL the SPA discovers `__connection.json` under, relative to the
|
|
17
|
+
* running page. Defaults to `'./'` (resolved against `document.baseURI`), so
|
|
18
|
+
* a devframe mounted at `/__<id>/` connects without extra configuration.
|
|
19
|
+
*/
|
|
20
|
+
baseURL?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Pre-resolved connection meta. Pass this to skip the `__connection.json`
|
|
23
|
+
* fetch entirely (e.g. when the host already knows the WS endpoint).
|
|
24
|
+
*/
|
|
25
|
+
connectionMeta?: ConnectionMeta;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Connect to the devframe RPC backend once and provide the client — plus live
|
|
29
|
+
* connection status — to the tree. The React counterpart to `@devframes/nuxt`'s
|
|
30
|
+
* client plugin.
|
|
31
|
+
*
|
|
32
|
+
* Children render immediately (before the connection resolves), so your shell
|
|
33
|
+
* and a connection indicator stay visible throughout; read the client with
|
|
34
|
+
* {@link useRpc} and the status with {@link useRpcStatus}.
|
|
35
|
+
*
|
|
36
|
+
* ```tsx
|
|
37
|
+
* 'use client'
|
|
38
|
+
* import { RpcProvider } from '@devframes/next/client'
|
|
39
|
+
*
|
|
40
|
+
* export function Providers({ children }: { children: React.ReactNode }) {
|
|
41
|
+
* return <RpcProvider baseURL="/__my-tool/">{children}</RpcProvider>
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
declare function RpcProvider({ children, baseURL, connectionMeta }: RpcProviderProps): ReactNode;
|
|
46
|
+
/**
|
|
47
|
+
* Read the connected {@link DevframeRpcClient}, or `null` while connecting.
|
|
48
|
+
* Scope it to your tool's RPC namespace with `useRpc()?.scope('my-tool:')`.
|
|
49
|
+
*
|
|
50
|
+
* Throws when called outside a `<RpcProvider>`.
|
|
51
|
+
*/
|
|
52
|
+
declare function useRpc(): DevframeRpcClient | null;
|
|
53
|
+
/**
|
|
54
|
+
* Read the live connection `status` and latest `error`, for a connection
|
|
55
|
+
* indicator. Throws when called outside a `<RpcProvider>`.
|
|
56
|
+
*/
|
|
57
|
+
declare function useRpcStatus(): {
|
|
58
|
+
status: DevframeConnectionStatus;
|
|
59
|
+
error: Error | null;
|
|
60
|
+
};
|
|
61
|
+
//#endregion
|
|
62
|
+
export { DevframeRpcState, RpcProvider, RpcProviderProps, useRpc, useRpcStatus };
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { connectDevframe } from "devframe/client";
|
|
3
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
//#region src/client.tsx
|
|
6
|
+
const RpcContext = createContext(null);
|
|
7
|
+
const INITIAL = {
|
|
8
|
+
rpc: null,
|
|
9
|
+
status: "connecting",
|
|
10
|
+
error: null
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Connect to the devframe RPC backend once and provide the client — plus live
|
|
14
|
+
* connection status — to the tree. The React counterpart to `@devframes/nuxt`'s
|
|
15
|
+
* client plugin.
|
|
16
|
+
*
|
|
17
|
+
* Children render immediately (before the connection resolves), so your shell
|
|
18
|
+
* and a connection indicator stay visible throughout; read the client with
|
|
19
|
+
* {@link useRpc} and the status with {@link useRpcStatus}.
|
|
20
|
+
*
|
|
21
|
+
* ```tsx
|
|
22
|
+
* 'use client'
|
|
23
|
+
* import { RpcProvider } from '@devframes/next/client'
|
|
24
|
+
*
|
|
25
|
+
* export function Providers({ children }: { children: React.ReactNode }) {
|
|
26
|
+
* return <RpcProvider baseURL="/__my-tool/">{children}</RpcProvider>
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
function RpcProvider({ children, baseURL = "./", connectionMeta }) {
|
|
31
|
+
const [state, setState] = useState(INITIAL);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
let active = true;
|
|
34
|
+
let client;
|
|
35
|
+
const sync = () => {
|
|
36
|
+
if (active && client) setState({
|
|
37
|
+
rpc: client,
|
|
38
|
+
status: client.status,
|
|
39
|
+
error: client.connectionError
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
const offs = [];
|
|
43
|
+
connectDevframe({
|
|
44
|
+
baseURL,
|
|
45
|
+
connectionMeta
|
|
46
|
+
}).then((c) => {
|
|
47
|
+
if (!active) return;
|
|
48
|
+
client = c;
|
|
49
|
+
sync();
|
|
50
|
+
offs.push(c.events.on("connection:status", sync));
|
|
51
|
+
offs.push(c.events.on("connection:error", sync));
|
|
52
|
+
}, (err) => {
|
|
53
|
+
if (active) setState({
|
|
54
|
+
rpc: null,
|
|
55
|
+
status: "error",
|
|
56
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
return () => {
|
|
60
|
+
active = false;
|
|
61
|
+
for (const off of offs) off();
|
|
62
|
+
};
|
|
63
|
+
}, [baseURL]);
|
|
64
|
+
return /* @__PURE__ */ jsx(RpcContext.Provider, {
|
|
65
|
+
value: state,
|
|
66
|
+
children
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function useDevframeState() {
|
|
70
|
+
const state = useContext(RpcContext);
|
|
71
|
+
if (!state) throw new Error("[@devframes/next] useRpc()/useRpcStatus() must be called inside a <RpcProvider>.");
|
|
72
|
+
return state;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Read the connected {@link DevframeRpcClient}, or `null` while connecting.
|
|
76
|
+
* Scope it to your tool's RPC namespace with `useRpc()?.scope('my-tool:')`.
|
|
77
|
+
*
|
|
78
|
+
* Throws when called outside a `<RpcProvider>`.
|
|
79
|
+
*/
|
|
80
|
+
function useRpc() {
|
|
81
|
+
return useDevframeState().rpc;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Read the live connection `status` and latest `error`, for a connection
|
|
85
|
+
* indicator. Throws when called outside a `<RpcProvider>`.
|
|
86
|
+
*/
|
|
87
|
+
function useRpcStatus() {
|
|
88
|
+
const { status, error } = useDevframeState();
|
|
89
|
+
return {
|
|
90
|
+
status,
|
|
91
|
+
error
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
export { RpcProvider, useRpc, useRpcStatus };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { CreateDevServerOptions } from "devframe/adapters/dev";
|
|
2
|
+
import { ConnectionMeta, DevframeDefinition, DevframeHost, DevframeStorageScope } from "devframe/types";
|
|
3
|
+
//#region src/config.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Minimal shape of the bits of `next.config` this helper reads/writes, so the
|
|
6
|
+
* package doesn't pull Next's full config type graph into its `.d.ts` bundle.
|
|
7
|
+
* Any real `NextConfig` structurally satisfies it.
|
|
8
|
+
*/
|
|
9
|
+
interface DevframeNextConfig {
|
|
10
|
+
skipTrailingSlashRedirect?: boolean;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Apply the Next config settings a devframe **host** requires, preserving
|
|
15
|
+
* everything else.
|
|
16
|
+
*
|
|
17
|
+
* Sets `skipTrailingSlashRedirect: true`: mounted devframe SPAs are served at
|
|
18
|
+
* `/__<id>/` and reference their assets relatively (`./_next/…`). Next's
|
|
19
|
+
* default trailing-slash redirect (`/__git/` → `/__git`) would re-root those
|
|
20
|
+
* relative paths and 404 every asset, leaving the panel unstyled and unable to
|
|
21
|
+
* connect. Serving the base path verbatim keeps relative resolution intact.
|
|
22
|
+
*
|
|
23
|
+
* ```js [next.config.mjs]
|
|
24
|
+
* import { withDevframe } from '@devframes/next'
|
|
25
|
+
*
|
|
26
|
+
* export default withDevframe({
|
|
27
|
+
* // ...your own Next config
|
|
28
|
+
* })
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
declare function withDevframe<T extends DevframeNextConfig>(nextConfig?: T): T;
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/handler.d.ts
|
|
34
|
+
interface CreateDevframeNextHandlerOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Mount base for the SPA. Defaults to `def.basePath ?? '/__<id>/'` — the
|
|
37
|
+
* hosted-adapter default, so the devframe shares the Next app's origin
|
|
38
|
+
* without colliding with its routes.
|
|
39
|
+
*/
|
|
40
|
+
base?: string;
|
|
41
|
+
/** Bind host for the side-car RPC/WS server. Default: `def.cli?.host ?? 'localhost'`. */
|
|
42
|
+
host?: string;
|
|
43
|
+
/** Pin the side-car port. Default: resolved from `def.cli?.port` via `get-port-please`. */
|
|
44
|
+
port?: number;
|
|
45
|
+
/** Flag bag forwarded to `def.setup(ctx, { flags })`. */
|
|
46
|
+
flags?: Record<string, unknown>;
|
|
47
|
+
/**
|
|
48
|
+
* Whether the side-car runs its own auth gate. Defaults to `false`: this is a
|
|
49
|
+
* hosted adapter and the Next app owns authentication. Pass `true` for
|
|
50
|
+
* devframe's interactive gate or a handler for a custom scheme.
|
|
51
|
+
*/
|
|
52
|
+
auth?: CreateDevServerOptions['auth'];
|
|
53
|
+
/** Origin the Next app is reachable at, for docks needing an absolute URL. */
|
|
54
|
+
resolveOrigin?: () => string;
|
|
55
|
+
/** Override where persisted devframe state lives (defaults under the cwd / home). */
|
|
56
|
+
getStorageDir?: (scope: DevframeStorageScope) => string;
|
|
57
|
+
/**
|
|
58
|
+
* Expose the side-car's route-based MCP server (Streamable-HTTP) and
|
|
59
|
+
* advertise it in the handler's `__connection.json`. Forwarded to
|
|
60
|
+
* `createDevServer`: overrides `def.cli?.mcp`, `undefined` falls through to
|
|
61
|
+
* it, `false` disables the route regardless. The endpoint lives on the
|
|
62
|
+
* side-car's own port, so the advertised meta carries `{ port, path }`.
|
|
63
|
+
*
|
|
64
|
+
* @experimental
|
|
65
|
+
*/
|
|
66
|
+
mcp?: CreateDevServerOptions['mcp'];
|
|
67
|
+
}
|
|
68
|
+
interface DevframeNextHandler {
|
|
69
|
+
/**
|
|
70
|
+
* WHATWG-`fetch` handler for the catch-all App Router route. Serves the
|
|
71
|
+
* plugin's built SPA at `base` and answers `<base>/__connection.json` with
|
|
72
|
+
* the side-car WS endpoint. Awaits {@link DevframeNextHandler.ready} so the
|
|
73
|
+
* first request doesn't race the server boot.
|
|
74
|
+
*/
|
|
75
|
+
fetch: (request: Request) => Promise<Response>;
|
|
76
|
+
/** Resolves once the side-car RPC/WS server is listening. */
|
|
77
|
+
ready: Promise<void>;
|
|
78
|
+
/** Shut the side-car server down (call from an app-lifecycle hook / test). */
|
|
79
|
+
close: () => Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Host a **single** devframe from a Next.js App Router app — the convenience
|
|
83
|
+
* wrapper over {@link createDevframeNextHost} for the common case of mounting
|
|
84
|
+
* one plugin (the Next counterpart to `viteDevBridge`'s bridge mode).
|
|
85
|
+
*
|
|
86
|
+
* It statically serves `def.cli.distDir` at `base` through the Next route and
|
|
87
|
+
* starts a side-car RPC/WS dev server (via `createDevServer` in bridge mode) on
|
|
88
|
+
* its own port, advertising that endpoint at `<base>/__connection.json` so the
|
|
89
|
+
* SPA's `connectDevframe()` can dial back in.
|
|
90
|
+
*
|
|
91
|
+
* ```ts [app/__my-tool/[[...path]]/route.ts]
|
|
92
|
+
* import myDevframe from '@/devframe'
|
|
93
|
+
* import { createDevframeNextHandler } from '@devframes/next'
|
|
94
|
+
*
|
|
95
|
+
* export const runtime = 'nodejs'
|
|
96
|
+
* export const dynamic = 'force-dynamic'
|
|
97
|
+
*
|
|
98
|
+
* const handler = createDevframeNextHandler(myDevframe)
|
|
99
|
+
* export const GET = handler.fetch
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* For a hub hosting many devframes at once, use {@link createDevframeNextHost}
|
|
103
|
+
* directly with `@devframes/hub`.
|
|
104
|
+
*/
|
|
105
|
+
declare function createDevframeNextHandler(def: DevframeDefinition, options?: CreateDevframeNextHandlerOptions): DevframeNextHandler;
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/host.d.ts
|
|
108
|
+
interface CreateDevframeNextHostOptions {
|
|
109
|
+
/**
|
|
110
|
+
* Public origin the Next app is reachable at, e.g. `http://localhost:3000`.
|
|
111
|
+
* Surfaced through {@link DevframeHost.resolveOrigin} for docks that need an
|
|
112
|
+
* absolute iframe URL.
|
|
113
|
+
*/
|
|
114
|
+
resolveOrigin: () => string;
|
|
115
|
+
/**
|
|
116
|
+
* Resolve a directory the host owns for persisted devframe state, per
|
|
117
|
+
* {@link DevframeHost.getStorageDir}.
|
|
118
|
+
*/
|
|
119
|
+
getStorageDir: (scope: DevframeStorageScope) => string;
|
|
120
|
+
/**
|
|
121
|
+
* Initial connection meta served at every base registered via
|
|
122
|
+
* {@link DevframeHost.mountConnectionMeta}. Usually unknown until the
|
|
123
|
+
* side-car RPC/WS server has started — publish it later with
|
|
124
|
+
* {@link DevframeNextHost.setConnectionMeta}.
|
|
125
|
+
*/
|
|
126
|
+
connectionMeta?: ConnectionMeta;
|
|
127
|
+
}
|
|
128
|
+
interface DevframeNextHost {
|
|
129
|
+
/**
|
|
130
|
+
* The {@link DevframeHost} to hand to `createHubContext` / `createHostContext`.
|
|
131
|
+
* Its `mountStatic` / `mountConnectionMeta` calls accumulate into the
|
|
132
|
+
* {@link DevframeNextHost.fetch} handler below.
|
|
133
|
+
*/
|
|
134
|
+
host: DevframeHost;
|
|
135
|
+
/**
|
|
136
|
+
* A WHATWG-`fetch` handler that serves every mounted SPA (with SPA
|
|
137
|
+
* fallback, correct content types, and path-traversal guarding — all from
|
|
138
|
+
* devframe's own `serveStaticHandler`) and answers `<base>/__connection.json`
|
|
139
|
+
* for each base registered via `mountConnectionMeta`. Delegate a Next App
|
|
140
|
+
* Router route handler straight to it:
|
|
141
|
+
*
|
|
142
|
+
* ```ts
|
|
143
|
+
* export async function GET(request: Request) {
|
|
144
|
+
* return (await ensureHub()).fetch(request)
|
|
145
|
+
* }
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
fetch: (request: Request) => Promise<Response>;
|
|
149
|
+
/**
|
|
150
|
+
* Publish the live connection meta once the RPC/WS server is up. Until this
|
|
151
|
+
* is called (and without an initial `connectionMeta`), meta requests answer
|
|
152
|
+
* `503` so a racing client retries rather than caching a wrong endpoint.
|
|
153
|
+
*/
|
|
154
|
+
setConnectionMeta: (meta: ConnectionMeta) => void;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Build a Node-runtime {@link DevframeHost} for a Next.js App Router app that
|
|
158
|
+
* hosts one or more devframes, plus the single `fetch` handler its catch-all
|
|
159
|
+
* route delegates to.
|
|
160
|
+
*
|
|
161
|
+
* This is the hosted-adapter counterpart to `viteDevBridge` for the Next
|
|
162
|
+
* runtime, which — being webpack/Turbopack rather than Vite — can't reuse the
|
|
163
|
+
* Vite middleware path. Instead of hand-rolling static serving in a route
|
|
164
|
+
* handler, static mounts are registered on an internal h3 app and served
|
|
165
|
+
* through devframe's shared `serveStaticHandler` (`app.fetch` makes h3 a
|
|
166
|
+
* WHATWG-`fetch` handler, exactly what an App Router route returns).
|
|
167
|
+
*
|
|
168
|
+
* Pins Node runtime (`export const runtime = 'nodejs'` in the route) because
|
|
169
|
+
* the static handler streams from the filesystem.
|
|
170
|
+
*/
|
|
171
|
+
declare function createDevframeNextHost(options: CreateDevframeNextHostOptions): DevframeNextHost;
|
|
172
|
+
//#endregion
|
|
173
|
+
export { type CreateDevframeNextHandlerOptions, type CreateDevframeNextHostOptions, type DevframeNextConfig, type DevframeNextHandler, type DevframeNextHost, createDevframeNextHandler, createDevframeNextHost, withDevframe };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { createDevServer, resolveDevServerPort, resolveMcpConnectionMeta } from "devframe/adapters/dev";
|
|
5
|
+
import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_WS_ROUTE } from "devframe/constants";
|
|
6
|
+
import { serveStaticHandler } from "devframe/utils/serve-static";
|
|
7
|
+
import { H3 } from "h3";
|
|
8
|
+
//#region src/config.ts
|
|
9
|
+
/**
|
|
10
|
+
* Apply the Next config settings a devframe **host** requires, preserving
|
|
11
|
+
* everything else.
|
|
12
|
+
*
|
|
13
|
+
* Sets `skipTrailingSlashRedirect: true`: mounted devframe SPAs are served at
|
|
14
|
+
* `/__<id>/` and reference their assets relatively (`./_next/…`). Next's
|
|
15
|
+
* default trailing-slash redirect (`/__git/` → `/__git`) would re-root those
|
|
16
|
+
* relative paths and 404 every asset, leaving the panel unstyled and unable to
|
|
17
|
+
* connect. Serving the base path verbatim keeps relative resolution intact.
|
|
18
|
+
*
|
|
19
|
+
* ```js [next.config.mjs]
|
|
20
|
+
* import { withDevframe } from '@devframes/next'
|
|
21
|
+
*
|
|
22
|
+
* export default withDevframe({
|
|
23
|
+
* // ...your own Next config
|
|
24
|
+
* })
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
function withDevframe(nextConfig = {}) {
|
|
28
|
+
return {
|
|
29
|
+
...nextConfig,
|
|
30
|
+
skipTrailingSlashRedirect: nextConfig.skipTrailingSlashRedirect ?? true
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/host.ts
|
|
35
|
+
const META_SUFFIX = `/${DEVFRAME_CONNECTION_META_FILENAME}`;
|
|
36
|
+
/** Drop trailing slashes from a mount base (`/__git/` → `/__git`). */
|
|
37
|
+
function stripTrailingSlash(base) {
|
|
38
|
+
return base.replace(/\/+$/, "");
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build a Node-runtime {@link DevframeHost} for a Next.js App Router app that
|
|
42
|
+
* hosts one or more devframes, plus the single `fetch` handler its catch-all
|
|
43
|
+
* route delegates to.
|
|
44
|
+
*
|
|
45
|
+
* This is the hosted-adapter counterpart to `viteDevBridge` for the Next
|
|
46
|
+
* runtime, which — being webpack/Turbopack rather than Vite — can't reuse the
|
|
47
|
+
* Vite middleware path. Instead of hand-rolling static serving in a route
|
|
48
|
+
* handler, static mounts are registered on an internal h3 app and served
|
|
49
|
+
* through devframe's shared `serveStaticHandler` (`app.fetch` makes h3 a
|
|
50
|
+
* WHATWG-`fetch` handler, exactly what an App Router route returns).
|
|
51
|
+
*
|
|
52
|
+
* Pins Node runtime (`export const runtime = 'nodejs'` in the route) because
|
|
53
|
+
* the static handler streams from the filesystem.
|
|
54
|
+
*/
|
|
55
|
+
function createDevframeNextHost(options) {
|
|
56
|
+
const app = new H3();
|
|
57
|
+
const metaBases = /* @__PURE__ */ new Set();
|
|
58
|
+
let connectionMeta = options.connectionMeta;
|
|
59
|
+
const host = {
|
|
60
|
+
mountStatic(base, distDir) {
|
|
61
|
+
const staticApp = new H3();
|
|
62
|
+
staticApp.use(serveStaticHandler(distDir));
|
|
63
|
+
app.mount(stripTrailingSlash(base), staticApp);
|
|
64
|
+
},
|
|
65
|
+
mountConnectionMeta(base) {
|
|
66
|
+
metaBases.add(stripTrailingSlash(base));
|
|
67
|
+
},
|
|
68
|
+
resolveOrigin: options.resolveOrigin,
|
|
69
|
+
getStorageDir: options.getStorageDir
|
|
70
|
+
};
|
|
71
|
+
async function fetch(request) {
|
|
72
|
+
const { pathname } = new URL(request.url);
|
|
73
|
+
if (pathname.endsWith(META_SUFFIX) && metaBases.has(pathname.slice(0, -META_SUFFIX.length))) {
|
|
74
|
+
if (!connectionMeta) return new Response(null, { status: 503 });
|
|
75
|
+
return Response.json(connectionMeta);
|
|
76
|
+
}
|
|
77
|
+
const response = await app.fetch(request);
|
|
78
|
+
if (response.status === 404) return new Response(null, { status: 404 });
|
|
79
|
+
return response;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
host,
|
|
83
|
+
fetch,
|
|
84
|
+
setConnectionMeta(meta) {
|
|
85
|
+
connectionMeta = meta;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/handler.ts
|
|
91
|
+
/** Ensure a mount base has a single leading and trailing slash. */
|
|
92
|
+
function normalizeBase(base) {
|
|
93
|
+
return `/${base}/`.replace(/\/{2,}/g, "/");
|
|
94
|
+
}
|
|
95
|
+
function defaultGetStorageDir(scope) {
|
|
96
|
+
const cwd = process.cwd();
|
|
97
|
+
if (scope === "workspace") return join(cwd, ".devframe");
|
|
98
|
+
if (scope === "project") return join(cwd, "node_modules/.devframe");
|
|
99
|
+
return join(homedir(), ".devframe");
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Host a **single** devframe from a Next.js App Router app — the convenience
|
|
103
|
+
* wrapper over {@link createDevframeNextHost} for the common case of mounting
|
|
104
|
+
* one plugin (the Next counterpart to `viteDevBridge`'s bridge mode).
|
|
105
|
+
*
|
|
106
|
+
* It statically serves `def.cli.distDir` at `base` through the Next route and
|
|
107
|
+
* starts a side-car RPC/WS dev server (via `createDevServer` in bridge mode) on
|
|
108
|
+
* its own port, advertising that endpoint at `<base>/__connection.json` so the
|
|
109
|
+
* SPA's `connectDevframe()` can dial back in.
|
|
110
|
+
*
|
|
111
|
+
* ```ts [app/__my-tool/[[...path]]/route.ts]
|
|
112
|
+
* import myDevframe from '@/devframe'
|
|
113
|
+
* import { createDevframeNextHandler } from '@devframes/next'
|
|
114
|
+
*
|
|
115
|
+
* export const runtime = 'nodejs'
|
|
116
|
+
* export const dynamic = 'force-dynamic'
|
|
117
|
+
*
|
|
118
|
+
* const handler = createDevframeNextHandler(myDevframe)
|
|
119
|
+
* export const GET = handler.fetch
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* For a hub hosting many devframes at once, use {@link createDevframeNextHost}
|
|
123
|
+
* directly with `@devframes/hub`.
|
|
124
|
+
*/
|
|
125
|
+
function createDevframeNextHandler(def, options = {}) {
|
|
126
|
+
const distDir = def.cli?.distDir;
|
|
127
|
+
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.`);
|
|
128
|
+
const base = normalizeBase(options.base ?? def.basePath ?? `/__${def.id}/`);
|
|
129
|
+
const hostName = options.host ?? def.cli?.host;
|
|
130
|
+
const nextHost = createDevframeNextHost({
|
|
131
|
+
resolveOrigin: options.resolveOrigin ?? (() => ""),
|
|
132
|
+
getStorageDir: options.getStorageDir ?? defaultGetStorageDir
|
|
133
|
+
});
|
|
134
|
+
nextHost.host.mountStatic(base, distDir);
|
|
135
|
+
nextHost.host.mountConnectionMeta?.(base);
|
|
136
|
+
let started;
|
|
137
|
+
const ready = (async () => {
|
|
138
|
+
const port = options.port ?? await resolveDevServerPort(def, { host: hostName });
|
|
139
|
+
started = await createDevServer(def, {
|
|
140
|
+
host: hostName,
|
|
141
|
+
port,
|
|
142
|
+
flags: options.flags,
|
|
143
|
+
openBrowser: false,
|
|
144
|
+
auth: options.auth ?? false,
|
|
145
|
+
mcp: options.mcp
|
|
146
|
+
});
|
|
147
|
+
const mcpMeta = resolveMcpConnectionMeta(def, options.mcp, port);
|
|
148
|
+
nextHost.setConnectionMeta({
|
|
149
|
+
backend: "websocket",
|
|
150
|
+
websocket: {
|
|
151
|
+
port,
|
|
152
|
+
path: `/${DEVFRAME_WS_ROUTE}`
|
|
153
|
+
},
|
|
154
|
+
...mcpMeta ? { mcp: mcpMeta } : {}
|
|
155
|
+
});
|
|
156
|
+
})();
|
|
157
|
+
return {
|
|
158
|
+
async fetch(request) {
|
|
159
|
+
await ready;
|
|
160
|
+
return nextHost.fetch(request);
|
|
161
|
+
},
|
|
162
|
+
ready,
|
|
163
|
+
async close() {
|
|
164
|
+
await ready.catch(() => {});
|
|
165
|
+
await started?.close();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
//#endregion
|
|
170
|
+
export { createDevframeNextHandler, createDevframeNextHost, withDevframe };
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devframes/next",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.7.14",
|
|
5
|
+
"description": "Next.js host integration for Devframe — serve mounted devframe SPAs and connection meta from an App Router route (experimental)",
|
|
6
|
+
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/devframes/devframe#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"directory": "packages/next",
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/devframes/devframe.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": "https://github.com/devframes/devframe/issues",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"devframe",
|
|
17
|
+
"next",
|
|
18
|
+
"nextjs",
|
|
19
|
+
"devtools"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.mts",
|
|
25
|
+
"default": "./dist/index.mjs"
|
|
26
|
+
},
|
|
27
|
+
"./client": {
|
|
28
|
+
"types": "./dist/client.d.mts",
|
|
29
|
+
"default": "./dist/client.mjs"
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"types": "./dist/index.d.mts",
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
39
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
40
|
+
"devframe": "0.7.14"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"next": {
|
|
44
|
+
"optional": true
|
|
45
|
+
},
|
|
46
|
+
"react": {
|
|
47
|
+
"optional": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"h3": "^2.0.1-rc.22"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^26.1.1",
|
|
55
|
+
"@types/react": "^19.2.17",
|
|
56
|
+
"react": "^19.2.8",
|
|
57
|
+
"tsdown": "^0.22.14",
|
|
58
|
+
"devframe": "0.7.14"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsdown",
|
|
62
|
+
"watch": "tsdown --watch",
|
|
63
|
+
"typecheck": "tsc --noEmit"
|
|
64
|
+
}
|
|
65
|
+
}
|