@devframes/next 0.8.1 → 0.9.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { DevframeConnectionStatus, DevframeRpcClient } from "devframe/client";
2
2
  import { ReactNode } from "react";
3
- import { ConnectionMeta } from "devframe/types";
3
+ import { ConnectionMeta } from "devframe";
4
4
  //#region src/client.d.ts
5
5
  interface DevframeRpcState {
6
6
  /** The connected client, or `null` while the initial connect is in flight. */
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { CreateDevServerOptions } from "devframe/adapters/dev";
2
- import { ConnectionMeta, DevframeDefinition, DevframeHost, DevframeNodeContext, DevframeStorageScope } from "devframe/types";
1
+ import { InitDevframeOptions } from "devframe/initiate";
2
+ import { ConnectionMeta, DevframeDefinition, DevframeHost, DevframeNodeContext, DevframeStorageScope } from "devframe";
3
3
  //#region src/config.d.ts
4
4
  /**
5
5
  * Minimal shape of the bits of `next.config` this helper reads/writes, so the
@@ -45,34 +45,40 @@ interface CreateDevframeNextHandlerOptions {
45
45
  /** Flag bag forwarded to `def.setup(ctx, { flags })`. */
46
46
  flags?: Record<string, unknown>;
47
47
  /**
48
- * Whether the side-car runs its own auth gate. **Gates by default** (defers
49
- * to `createDevServer` — devframe's interactive OTP unless the definition's
50
- * `cli.auth` opts out), so the side-car socket isn't silently reachable by
51
- * anything that can open it. Pass `false` to opt out for a single-user
52
- * localhost host, or a handler for a custom scheme.
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
53
  */
54
- auth?: CreateDevServerOptions['auth'];
54
+ auth?: InitDevframeOptions['auth'];
55
55
  /** Origin the Next app is reachable at, for docks needing an absolute URL. */
56
56
  resolveOrigin?: () => string;
57
57
  /** Override where persisted devframe state lives (defaults under the cwd / home). */
58
58
  getStorageDir?: (scope: DevframeStorageScope) => string;
59
59
  /**
60
- * Expose the side-car's route-based MCP server (Streamable-HTTP) and
61
- * advertise it in the handler's `__connection.json`. Forwarded to
62
- * `createDevServer`: overrides `def.cli?.mcp`, `undefined` falls through to
63
- * it, `false` disables the route regardless. The endpoint lives on the
64
- * side-car's own port, so the advertised meta carries `{ port, path }`.
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
65
  *
66
66
  * @experimental
67
67
  */
68
- mcp?: CreateDevServerOptions['mcp'];
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;
69
75
  }
70
76
  interface DevframeNextHandler {
71
77
  /**
72
78
  * WHATWG-`fetch` handler for the catch-all App Router route. Serves the
73
- * plugin's built SPA at `base` and answers `<base>/__connection.json` with
74
- * the side-car WS endpoint. Awaits {@link DevframeNextHandler.ready} so the
75
- * first request doesn't race the server boot.
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.
76
82
  */
77
83
  fetch: (request: Request) => Promise<Response>;
78
84
  /** Resolves once the side-car RPC/WS server is listening. */
@@ -81,16 +87,13 @@ interface DevframeNextHandler {
81
87
  close: () => Promise<void>;
82
88
  }
83
89
  /**
84
- * Host a **single** devframe from a Next.js App Router app — the convenience
85
- * wrapper over {@link createDevframeNextHost} for the common case of mounting
86
- * one plugin (the Next counterpart to `viteDevBridge`'s bridge mode).
87
- *
88
- * It statically serves `def.cli.distDir` at `base` through the Next route and
89
- * starts a side-car RPC/WS dev server (via `createDevServer` in bridge mode) on
90
- * its own port, advertising that endpoint at `<base>/__connection.json` so the
91
- * SPA's `connectDevframe()` can dial back in.
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`).
92
95
  *
93
- * ```ts [app/__my-tool/[[...path]]/route.ts]
96
+ * ```ts [app/%5F_my-tool/[[...path]]/route.ts]
94
97
  * import myDevframe from '@/devframe'
95
98
  * import { createDevframeNextHandler } from '@devframes/next'
96
99
  *
@@ -101,7 +104,7 @@ interface DevframeNextHandler {
101
104
  * export const GET = handler.fetch
102
105
  * ```
103
106
  *
104
- * For a hub hosting many devframes at once, use {@link createDevframeNextHost}
107
+ * For a hub hosting many devframes at once, use `createDevframeNextHost`
105
108
  * directly with `@devframes/hub`.
106
109
  */
107
110
  declare function createDevframeNextHandler(def: DevframeDefinition, options?: CreateDevframeNextHandlerOptions): DevframeNextHandler;
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
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";
4
+ import { initDevframe } from "devframe/initiate";
5
+ import { DEVFRAME_CONNECTION_META_FILENAME } from "devframe/constants";
6
6
  import { serveStaticHandler } from "devframe/utils/serve-static";
7
7
  import { H3 } from "h3";
8
8
  //#region src/config.ts
@@ -31,6 +31,80 @@ function withDevframe(nextConfig = {}) {
31
31
  };
32
32
  }
33
33
  //#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
34
108
  //#region src/host.ts
35
109
  const META_SUFFIX = `/${DEVFRAME_CONNECTION_META_FILENAME}`;
36
110
  /** Drop trailing slashes from a mount base (`/__git/` → `/__git`). */
@@ -105,84 +179,4 @@ function createDevframeNextHost(options) {
105
179
  };
106
180
  }
107
181
  //#endregion
108
- //#region src/handler.ts
109
- /** Ensure a mount base has a single leading and trailing slash. */
110
- function normalizeBase(base) {
111
- return `/${base}/`.replace(/\/{2,}/g, "/");
112
- }
113
- function defaultGetStorageDir(scope) {
114
- const cwd = process.cwd();
115
- if (scope === "workspace") return join(cwd, ".devframe");
116
- if (scope === "project") return join(cwd, "node_modules/.devframe");
117
- return join(homedir(), ".devframe");
118
- }
119
- /**
120
- * Host a **single** devframe from a Next.js App Router app — the convenience
121
- * wrapper over {@link createDevframeNextHost} for the common case of mounting
122
- * one plugin (the Next counterpart to `viteDevBridge`'s bridge mode).
123
- *
124
- * It statically serves `def.cli.distDir` at `base` through the Next route and
125
- * starts a side-car RPC/WS dev server (via `createDevServer` in bridge mode) on
126
- * its own port, advertising that endpoint at `<base>/__connection.json` so the
127
- * SPA's `connectDevframe()` can dial back in.
128
- *
129
- * ```ts [app/__my-tool/[[...path]]/route.ts]
130
- * import myDevframe from '@/devframe'
131
- * import { createDevframeNextHandler } from '@devframes/next'
132
- *
133
- * export const runtime = 'nodejs'
134
- * export const dynamic = 'force-dynamic'
135
- *
136
- * const handler = createDevframeNextHandler(myDevframe)
137
- * export const GET = handler.fetch
138
- * ```
139
- *
140
- * For a hub hosting many devframes at once, use {@link createDevframeNextHost}
141
- * directly with `@devframes/hub`.
142
- */
143
- function createDevframeNextHandler(def, options = {}) {
144
- const distDir = def.cli?.distDir;
145
- if (!distDir) throw new Error(`[@devframes/next] createDevframeNextHandler("${def.id}") needs a built SPA to serve, but "cli.distDir" is not set on the devframe definition.`);
146
- const base = normalizeBase(options.base ?? def.basePath ?? `/__${def.id}/`);
147
- const hostName = options.host ?? def.cli?.host;
148
- const nextHost = createDevframeNextHost({
149
- resolveOrigin: options.resolveOrigin ?? (() => ""),
150
- getStorageDir: options.getStorageDir ?? defaultGetStorageDir
151
- });
152
- nextHost.host.mountStatic(base, distDir);
153
- nextHost.host.mountConnectionMeta?.(base);
154
- let started;
155
- const ready = (async () => {
156
- const port = options.port ?? await resolveDevServerPort(def, { host: hostName });
157
- started = await createDevServer(def, {
158
- host: hostName,
159
- port,
160
- flags: options.flags,
161
- openBrowser: false,
162
- auth: options.auth,
163
- mcp: options.mcp
164
- });
165
- const mcpMeta = resolveMcpConnectionMeta(def, options.mcp, port);
166
- nextHost.setConnectionMeta({
167
- backend: "websocket",
168
- websocket: {
169
- port,
170
- path: `/${DEVFRAME_WS_ROUTE}`
171
- },
172
- ...mcpMeta ? { mcp: mcpMeta } : {}
173
- });
174
- })();
175
- return {
176
- async fetch(request) {
177
- await ready;
178
- return nextHost.fetch(request);
179
- },
180
- ready,
181
- async close() {
182
- await ready.catch(() => {});
183
- await started?.close();
184
- }
185
- };
186
- }
187
- //#endregion
188
182
  export { createDevframeNextHandler, createDevframeNextHost, withDevframe };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@devframes/next",
3
3
  "type": "module",
4
- "version": "0.8.1",
5
- "description": "Next.js host integration for Devframe — serve mounted devframe SPAs and connection meta from an App Router route (experimental)",
4
+ "version": "0.9.0-beta.1",
5
+ "description": "Next.js host integration that serves mounted devframes from an App Router route.",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
8
8
  "homepage": "https://github.com/devframes/devframe#readme",
@@ -37,7 +37,7 @@
37
37
  "peerDependencies": {
38
38
  "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
39
39
  "react": "^18.0.0 || ^19.0.0",
40
- "devframe": "0.8.1"
40
+ "devframe": "0.9.0-beta.1"
41
41
  },
42
42
  "peerDependenciesMeta": {
43
43
  "next": {
@@ -51,11 +51,11 @@
51
51
  "h3": "^2.0.1-rc.26"
52
52
  },
53
53
  "devDependencies": {
54
- "@types/node": "^26.1.2",
55
- "@types/react": "^19.2.17",
54
+ "@types/node": "^26.2.0",
55
+ "@types/react": "^19.2.18",
56
56
  "react": "^19.2.8",
57
57
  "tsdown": "^0.22.14",
58
- "devframe": "0.8.1"
58
+ "devframe": "0.9.0-beta.1"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "tsdown",