every-plugin 3.0.0-rc.3 → 3.0.0-rc.5

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.
Files changed (56) hide show
  1. package/dist/build/ui/generated-config.cjs +4 -0
  2. package/dist/build/ui/generated-config.cjs.map +1 -1
  3. package/dist/build/ui/generated-config.d.cts.map +1 -1
  4. package/dist/build/ui/generated-config.d.mts.map +1 -1
  5. package/dist/build/ui/generated-config.mjs +4 -1
  6. package/dist/build/ui/generated-config.mjs.map +1 -1
  7. package/dist/client.cjs +69 -0
  8. package/dist/client.cjs.map +1 -0
  9. package/dist/client.d.cts +15 -0
  10. package/dist/client.d.cts.map +1 -0
  11. package/dist/client.d.mts +15 -0
  12. package/dist/client.d.mts.map +1 -0
  13. package/dist/client.mjs +68 -0
  14. package/dist/client.mjs.map +1 -0
  15. package/dist/dev/host.cjs +149 -0
  16. package/dist/dev/host.cjs.map +1 -0
  17. package/dist/dev/host.d.cts +32 -0
  18. package/dist/dev/host.d.cts.map +1 -0
  19. package/dist/dev/host.d.mts +32 -0
  20. package/dist/dev/host.d.mts.map +1 -0
  21. package/dist/dev/host.mjs +148 -0
  22. package/dist/dev/host.mjs.map +1 -0
  23. package/dist/dev/serve.cjs +16 -111
  24. package/dist/dev/serve.cjs.map +1 -1
  25. package/dist/dev/serve.d.cts.map +1 -1
  26. package/dist/dev/serve.d.mts.map +1 -1
  27. package/dist/dev/serve.mjs +17 -112
  28. package/dist/dev/serve.mjs.map +1 -1
  29. package/dist/index.cjs +2 -0
  30. package/dist/index.d.cts +3 -2
  31. package/dist/index.d.mts +3 -2
  32. package/dist/index.mjs +2 -1
  33. package/dist/plugin.d.cts +1 -1
  34. package/dist/remote-entry.cjs +0 -1
  35. package/dist/remote-entry.cjs.map +1 -1
  36. package/dist/remote-entry.d.cts +1 -2
  37. package/dist/remote-entry.d.cts.map +1 -1
  38. package/dist/remote-entry.d.mts +1 -2
  39. package/dist/remote-entry.d.mts.map +1 -1
  40. package/dist/remote-entry.mjs +1 -1
  41. package/dist/remote-entry.mjs.map +1 -1
  42. package/dist/runtime/mf-config.cjs +1 -1
  43. package/dist/runtime/mf-config.mjs +1 -1
  44. package/dist/runtime/services/plugin-loader.service.d.cts +2 -2
  45. package/dist/types.d.cts +23 -2
  46. package/dist/types.d.cts.map +1 -1
  47. package/dist/types.d.mts +22 -1
  48. package/dist/types.d.mts.map +1 -1
  49. package/package.json +19 -2
  50. package/src/build/ui/generated-config.ts +4 -0
  51. package/src/client.ts +111 -0
  52. package/src/dev/host.ts +235 -0
  53. package/src/dev/serve.ts +22 -140
  54. package/src/index.ts +9 -0
  55. package/src/remote-entry.ts +1 -2
  56. package/src/types.ts +36 -2
package/src/client.ts ADDED
@@ -0,0 +1,111 @@
1
+ import { createORPCClient, onError } from "@orpc/client";
2
+ import { RPCLink } from "@orpc/client/fetch";
3
+ import type { ContractRouterClient, RouterContract } from "@orpc/contract";
4
+
5
+ export type RpcConnectionErrorCopy = () => { title: string; description: string };
6
+
7
+ export interface RpcClientConfig {
8
+ hostUrl: string;
9
+ path: `/${string}`;
10
+ connectionError?: RpcConnectionErrorCopy;
11
+ }
12
+
13
+ const RATE_LIMIT_MAX_RETRIES = 3;
14
+ const RATE_LIMIT_RETRY_CAP_MS = 2_000;
15
+ const browserClients = new Map<string, unknown>();
16
+ type FetchInput = Parameters<typeof fetch>[0];
17
+
18
+ async function fetchWithRateLimitRetry(
19
+ fetchUrl: FetchInput,
20
+ options?: RequestInit,
21
+ ): Promise<Response> {
22
+ let response = await fetch(fetchUrl, options);
23
+ for (let attempt = 1; attempt <= RATE_LIMIT_MAX_RETRIES && response.status === 429; attempt++) {
24
+ const retryAfterSeconds = Number(response.headers.get("retry-after"));
25
+ const delay =
26
+ Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0
27
+ ? Math.min(retryAfterSeconds * 1000, RATE_LIMIT_RETRY_CAP_MS)
28
+ : 100 * attempt;
29
+ await new Promise((resolve) => setTimeout(resolve, delay));
30
+ response = await fetch(fetchUrl, options);
31
+ }
32
+
33
+ return response;
34
+ }
35
+
36
+ function createRpcLink(config: RpcClientConfig, headers?: Headers) {
37
+ return new RPCLink({
38
+ origin: config.hostUrl,
39
+ url: config.path,
40
+ interceptors: [
41
+ onError((error: unknown) => {
42
+ const browserWindow = (globalThis as { window?: unknown }).window;
43
+ if (typeof browserWindow === "undefined") {
44
+ return;
45
+ }
46
+
47
+ if (error && typeof error === "object" && "message" in error) {
48
+ const message = String(error.message).toLowerCase();
49
+ if (
50
+ message.includes("fetch") ||
51
+ message.includes("network") ||
52
+ message.includes("failed to fetch")
53
+ ) {
54
+ void import("sonner")
55
+ .then(({ toast }) => {
56
+ const message = config.connectionError?.() ?? {
57
+ title: "Unable to connect to API",
58
+ description: "The API is currently unavailable. Please try again later.",
59
+ };
60
+ toast.error(message.title, {
61
+ id: "api-connection-error",
62
+ description: message.description,
63
+ });
64
+ })
65
+ .catch(() => {});
66
+ }
67
+ }
68
+ }),
69
+ ],
70
+ fetch(fetchUrl: FetchInput, options?: RequestInit) {
71
+ return fetchWithRateLimitRetry(fetchUrl, {
72
+ ...options,
73
+ credentials: "include",
74
+ headers: headers
75
+ ? { ...Object.fromEntries(headers), ...(options?.headers as Record<string, string>) }
76
+ : options?.headers,
77
+ });
78
+ },
79
+ });
80
+ }
81
+
82
+ function buildRpcClient<T extends RouterContract>(
83
+ config: RpcClientConfig,
84
+ headers?: Headers,
85
+ ): ContractRouterClient<T> {
86
+ if (!config.hostUrl) {
87
+ throw new Error("Missing runtime host URL");
88
+ }
89
+
90
+ return createORPCClient(createRpcLink(config, headers)) as ContractRouterClient<T>;
91
+ }
92
+
93
+ export function createRpcClient<T extends RouterContract = RouterContract>(
94
+ config: RpcClientConfig,
95
+ headers?: Headers,
96
+ ): ContractRouterClient<T> {
97
+ const browserWindow = (globalThis as { window?: unknown }).window;
98
+ if (typeof browserWindow !== "undefined" && !headers) {
99
+ const key = `${config.hostUrl}${config.path}`;
100
+ const memoized = browserClients.get(key);
101
+ if (memoized) {
102
+ return memoized as ContractRouterClient<T>;
103
+ }
104
+
105
+ const client = buildRpcClient<T>(config, headers);
106
+ browserClients.set(key, client);
107
+ return client;
108
+ }
109
+
110
+ return buildRpcClient<T>(config, headers);
111
+ }
@@ -0,0 +1,235 @@
1
+ import type http from "node:http";
2
+ import { createServer } from "node:http";
3
+
4
+ const corsHeaders = {
5
+ "Access-Control-Allow-Origin": "*",
6
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
7
+ "Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization",
8
+ };
9
+
10
+ const applyCorsHeaders = (res: http.ServerResponse) => {
11
+ Object.entries(corsHeaders).forEach(([key, value]) => {
12
+ res.setHeader(key, value);
13
+ });
14
+ };
15
+
16
+ const sendJson = (res: http.ServerResponse, status: number, body: unknown) => {
17
+ res.statusCode = status;
18
+ res.setHeader("content-type", "application/json; charset=utf-8");
19
+ res.end(JSON.stringify(body));
20
+ };
21
+
22
+ const sendText = (res: http.ServerResponse, status: number, text: string) => {
23
+ res.statusCode = status;
24
+ res.setHeader("content-type", "text/plain; charset=utf-8");
25
+ res.end(text);
26
+ };
27
+
28
+ const pathnameOf = (req: http.IncomingMessage): string =>
29
+ ((req.url ?? "/").split("?")[0] ?? "/").replace(/\/+$/, "") || "/";
30
+
31
+ const toWebRequest = (req: http.IncomingMessage, port: number): Request => {
32
+ const url = `http://localhost:${port}${req.url}`;
33
+ return new Request(url, {
34
+ method: req.method,
35
+ headers: req.headers,
36
+ body: req.method !== "GET" && req.method !== "HEAD" ? req : undefined,
37
+ duplex: req.method !== "GET" && req.method !== "HEAD" ? "half" : undefined,
38
+ } as RequestInit);
39
+ };
40
+
41
+ const buildHostContext = (
42
+ effectContext: unknown,
43
+ webRequest: Request,
44
+ cachedRawBodyRef: { value: string | null },
45
+ ) => {
46
+ const rawClone =
47
+ webRequest.method === "GET" || webRequest.method === "HEAD" ? null : webRequest.clone();
48
+
49
+ return {
50
+ reqHeaders: webRequest.headers,
51
+ "effect/context": effectContext,
52
+ getRawBody: async (): Promise<string> => {
53
+ if (cachedRawBodyRef.value !== null) return cachedRawBodyRef.value;
54
+ if (!rawClone) {
55
+ cachedRawBodyRef.value = "";
56
+ return cachedRawBodyRef.value;
57
+ }
58
+ cachedRawBodyRef.value = await rawClone.text();
59
+ return cachedRawBodyRef.value;
60
+ },
61
+ };
62
+ };
63
+
64
+ export interface DirectModuleHostHandler {
65
+ handle(
66
+ request: Request,
67
+ options: { prefix: string; context: unknown },
68
+ ): Promise<{ response?: Response }>;
69
+ }
70
+
71
+ const handleOrpc = async (
72
+ req: http.IncomingMessage,
73
+ res: http.ServerResponse,
74
+ handler: DirectModuleHostHandler,
75
+ prefix: string,
76
+ errorLabel: string,
77
+ effectContext: unknown,
78
+ port: number,
79
+ ) => {
80
+ try {
81
+ const webRequest = toWebRequest(req, port);
82
+ const cachedRawBodyRef = { value: null as string | null };
83
+ const result = await handler.handle(webRequest, {
84
+ prefix,
85
+ context: buildHostContext(effectContext, webRequest, cachedRawBodyRef),
86
+ });
87
+
88
+ if (result.response) {
89
+ res.statusCode = result.response.status;
90
+ result.response.headers.forEach((value: string, key: string) => {
91
+ res.setHeader(key, value);
92
+ });
93
+ const text = await result.response.text();
94
+ if (!res.getHeader("content-length")) {
95
+ res.setHeader("content-length", Buffer.byteLength(text).toString());
96
+ }
97
+ res.end(text);
98
+ } else {
99
+ sendText(res, 404, "Not Found");
100
+ }
101
+ } catch (error) {
102
+ console.error(`${errorLabel} error:`, error);
103
+ sendJson(res, 500, { error: (error as Error).message });
104
+ }
105
+ };
106
+
107
+ export interface DirectModuleHostHandlers {
108
+ rpc: DirectModuleHostHandler | null;
109
+ api: DirectModuleHostHandler | null;
110
+ }
111
+
112
+ export interface DirectModuleHostOptions {
113
+ name: string;
114
+ handlers: DirectModuleHostHandlers;
115
+ effectContext: unknown;
116
+ port?: number;
117
+ version?: string;
118
+ rpcPrefix?: `/${string}`;
119
+ apiPrefix?: `/${string}`;
120
+ staticHandler?:
121
+ | ((req: http.IncomingMessage, res: http.ServerResponse, next: () => void) => void)
122
+ | null;
123
+ }
124
+
125
+ export interface DirectModuleHostHandle {
126
+ port: number;
127
+ close: () => Promise<void>;
128
+ }
129
+
130
+ export async function startDirectModuleHost(
131
+ options: DirectModuleHostOptions,
132
+ ): Promise<DirectModuleHostHandle> {
133
+ const port = options.port ?? 0;
134
+ let resolvedPort = port;
135
+ const rpcPrefix = options.rpcPrefix ?? "/api/rpc";
136
+ const apiPrefix = options.apiPrefix ?? "/api";
137
+ const staticHandler = options.staticHandler ?? null;
138
+ const resolveEffectContext: () => unknown =
139
+ typeof options.effectContext === "function"
140
+ ? (options.effectContext as () => unknown)
141
+ : () => options.effectContext;
142
+
143
+ const server = createServer((req, res) => {
144
+ applyCorsHeaders(res);
145
+ const pathname = pathnameOf(req);
146
+
147
+ if (req.method === "OPTIONS") {
148
+ res.statusCode = 200;
149
+ res.end();
150
+ return;
151
+ }
152
+
153
+ if (pathname === "/" && (req.method === "GET" || req.method === "HEAD")) {
154
+ sendJson(res, 200, {
155
+ ok: true,
156
+ module: options.name,
157
+ version: options.version,
158
+ status: options.handlers.rpc ? "ready" : "loading",
159
+ endpoints: {
160
+ health: "/health",
161
+ docs: apiPrefix,
162
+ rpc: rpcPrefix,
163
+ },
164
+ });
165
+ return;
166
+ }
167
+
168
+ if (pathname === "/health" && (req.method === "GET" || req.method === "HEAD")) {
169
+ sendText(res, 200, "OK");
170
+ return;
171
+ }
172
+
173
+ if (pathname.startsWith(`${rpcPrefix}/`)) {
174
+ if (!options.handlers.rpc) {
175
+ sendJson(res, 503, { error: "Plugin still loading..." });
176
+ return;
177
+ }
178
+ handleOrpc(
179
+ req,
180
+ res,
181
+ options.handlers.rpc,
182
+ rpcPrefix,
183
+ "RPC",
184
+ resolveEffectContext(),
185
+ resolvedPort,
186
+ ).catch(() => {});
187
+ return;
188
+ }
189
+
190
+ if (pathname === apiPrefix || pathname.startsWith(`${apiPrefix}/`)) {
191
+ if (!options.handlers.api) {
192
+ sendJson(res, 503, { error: "Plugin still loading..." });
193
+ return;
194
+ }
195
+ handleOrpc(
196
+ req,
197
+ res,
198
+ options.handlers.api,
199
+ apiPrefix,
200
+ "OpenAPI",
201
+ resolveEffectContext(),
202
+ resolvedPort,
203
+ ).catch(() => {});
204
+ return;
205
+ }
206
+
207
+ if (staticHandler) {
208
+ staticHandler(req, res, () => {
209
+ sendText(res, 404, "Not Found");
210
+ });
211
+ return;
212
+ }
213
+
214
+ sendText(res, 404, "Not Found");
215
+ });
216
+
217
+ await new Promise<void>((resolve, reject) => {
218
+ server.once("error", reject);
219
+ server.listen(port, () => resolve());
220
+ });
221
+
222
+ const address = server.address();
223
+ if (!address || typeof address === "string") {
224
+ await new Promise<void>((resolve) => server.close(() => resolve()));
225
+ throw new Error("Failed to resolve host port");
226
+ }
227
+ resolvedPort = address.port;
228
+
229
+ return {
230
+ port: resolvedPort,
231
+ close: async () => {
232
+ await new Promise<void>((resolve) => server.close(() => resolve()));
233
+ },
234
+ };
235
+ }
package/src/dev/serve.ts CHANGED
@@ -7,9 +7,14 @@ import sirv from "sirv";
7
7
  import { DEV_ENTRY_FILENAME } from "../build/artifact-names";
8
8
  import { ensureGeneratedRspackConfig } from "../build/rspack/generated-config";
9
9
  import { getPluginInfo, loadDevConfig } from "../build/rspack/utils";
10
- import { ensureGeneratedUiRsbuildConfig, pluginLayoutKey } from "../build/ui/generated-config";
10
+ import {
11
+ ensureGeneratedUiRsbuildConfig,
12
+ pluginLayoutKey,
13
+ uiDistDirFor,
14
+ } from "../build/ui/generated-config";
11
15
  import { PLUGIN_ERROR_STATUS_MAP } from "../errors";
12
16
  import { loadRemoteWithRetry } from "../remote-entry";
17
+ import { startDirectModuleHost } from "./host";
13
18
  import { killChildEscalating, watchParentDeath } from "./watch-kill";
14
19
 
15
20
  const corsHeaders = {
@@ -81,12 +86,6 @@ const loadPluginWithRetry = (
81
86
  }),
82
87
  );
83
88
 
84
- const sendJson = (res: http.ServerResponse, status: number, body: unknown) => {
85
- res.statusCode = status;
86
- res.setHeader("content-type", "application/json; charset=utf-8");
87
- res.end(JSON.stringify(body));
88
- };
89
-
90
89
  const sendText = (res: http.ServerResponse, status: number, text: string) => {
91
90
  res.statusCode = status;
92
91
  res.setHeader("content-type", "text/plain; charset=utf-8");
@@ -125,7 +124,6 @@ export async function startPluginDevServer(
125
124
  const compositionKey = pluginLayoutKey(cwd) ?? pluginId;
126
125
  const port = options.port ?? (Number(process.env.PORT) || devConfig?.port || 3999);
127
126
  const rpcPrefix = normalizePrefix(devConfig?.prefix);
128
- const rpcBase = `/api/rpc${rpcPrefix}`;
129
127
 
130
128
  const distDir = path.join(cwd, "dist");
131
129
  const serveStatic = sirv(distDir, { dev: true });
@@ -174,11 +172,10 @@ export async function startPluginDevServer(
174
172
  // Folder-form ui: one-shot build, then serve the ui source root's dist
175
173
  // (the factory's absolute distPath target) statically on the ui port.
176
174
  await runOnce("rsbuild", ["build", "--config", uiConfig]);
177
- const uiDistDir =
178
- path.basename(cwd) === "ui" ? path.join(cwd, "dist") : path.join(cwd, "ui", "dist");
175
+ const uiDistDir = uiDistDirFor(cwd);
179
176
  if (!fs.existsSync(path.join(uiDistDir, DEV_ENTRY_FILENAME))) {
180
- console.error(
181
- `❌ UI dist is missing at ${uiDistDir} — the ui static server would serve 404s for every asset (build output layout mismatch?)`,
177
+ throw new Error(
178
+ `❌ UI dist is missing at ${uiDistDir} after the rsbuild one-shot — build output layout mismatch?`,
182
179
  );
183
180
  }
184
181
  const serveUiStatic = sirv(uiDistDir, { dev: true });
@@ -208,134 +205,19 @@ export async function startPluginDevServer(
208
205
  const handlers: { rpc: any; api: any } = { rpc: null, api: null };
209
206
  const effectContextHolder: { context: unknown } = { context: null };
210
207
  let runtime: { shutdown: () => Promise<void> } | null = null;
211
-
212
- const pathnameOf = (req: http.IncomingMessage): string =>
213
- ((req.url ?? "/").split("?")[0] ?? "/").replace(/\/+$/, "") || "/";
214
-
215
- const buildDevContext = (_req: http.IncomingMessage, webRequest: Request) => {
216
- const rawClone =
217
- webRequest.method === "GET" || webRequest.method === "HEAD" ? null : webRequest.clone();
218
- let cachedRawBody: string | null = null;
219
- return {
220
- reqHeaders: webRequest.headers,
221
- "effect/context": effectContextHolder.context,
222
- getRawBody: async (): Promise<string> => {
223
- if (cachedRawBody !== null) return cachedRawBody;
224
- if (!rawClone) {
225
- cachedRawBody = "";
226
- return cachedRawBody;
227
- }
228
- cachedRawBody = await rawClone.text();
229
- return cachedRawBody;
230
- },
231
- };
232
- };
233
-
234
- const toWebRequest = (req: http.IncomingMessage): Request => {
235
- const url = `http://localhost:${port}${req.url}`;
236
- return new Request(url, {
237
- method: req.method,
238
- headers: req.headers,
239
- body: req.method !== "GET" && req.method !== "HEAD" ? req : undefined,
240
- duplex: req.method !== "GET" && req.method !== "HEAD" ? "half" : undefined,
241
- } as RequestInit);
242
- };
243
-
244
- const handleOrpc = async (
245
- req: http.IncomingMessage,
246
- res: http.ServerResponse,
247
- handler: any,
248
- prefix: string,
249
- errorLabel: string,
250
- ) => {
251
- try {
252
- const webRequest = toWebRequest(req);
253
- const result = await handler.handle(webRequest, {
254
- prefix,
255
- context: buildDevContext(req, webRequest),
256
- });
257
-
258
- if (result.response) {
259
- res.statusCode = result.response.status;
260
- result.response.headers.forEach((value: string, key: string) => {
261
- res.setHeader(key, value);
262
- });
263
- const text = await result.response.text();
264
- if (!res.getHeader("content-length")) {
265
- res.setHeader("content-length", Buffer.byteLength(text).toString());
266
- }
267
- res.end(text);
268
- } else {
269
- sendText(res, 404, "Not Found");
270
- }
271
- } catch (error) {
272
- console.error(`${errorLabel} error:`, error);
273
- sendJson(res, 500, { error: (error as Error).message });
274
- }
275
- };
276
-
277
- const server = http.createServer((req, res) => {
278
- applyCorsHeaders(res);
279
- const pathname = pathnameOf(req);
280
-
281
- if (req.method === "OPTIONS") {
282
- res.statusCode = 200;
283
- res.end();
284
- return;
285
- }
286
-
287
- if (pathname === "/" && (req.method === "GET" || req.method === "HEAD")) {
288
- sendJson(res, 200, {
289
- ok: true,
290
- plugin: pluginInfo.normalizedName,
291
- version: pluginInfo.version,
292
- status: handlers.rpc ? "ready" : "loading",
293
- endpoints: {
294
- health: "/",
295
- docs: "/api",
296
- rpc: rpcBase,
297
- },
298
- });
299
- return;
300
- }
301
-
302
- if (pathname === "/health" && (req.method === "GET" || req.method === "HEAD")) {
303
- sendText(res, 200, "OK");
304
- return;
305
- }
306
-
307
- if (pathname.startsWith(`${rpcBase}/`)) {
308
- if (!handlers.rpc) {
309
- sendJson(res, 503, { error: "Plugin still loading..." });
310
- return;
311
- }
312
- handleOrpc(req, res, handlers.rpc, rpcBase, "RPC").catch(() => {});
313
- return;
314
- }
315
-
316
- if (pathname === "/api" || pathname.startsWith("/api/")) {
317
- if (!handlers.api) {
318
- sendJson(res, 503, { error: "Plugin still loading..." });
319
- return;
320
- }
321
- handleOrpc(req, res, handlers.api, "/api", "OpenAPI").catch(() => {});
322
- return;
323
- }
324
-
325
- serveStatic(req, res, () => {
326
- sendText(res, 404, "Not Found");
327
- });
328
- });
329
-
330
- await new Promise<void>((resolve, reject) => {
331
- server.once("error", reject);
332
- server.listen(port, () => resolve());
208
+ const host = await startDirectModuleHost({
209
+ name: pluginInfo.normalizedName,
210
+ version: pluginInfo.version,
211
+ handlers,
212
+ effectContext: () => effectContextHolder.context,
213
+ port,
214
+ staticHandler: serveStatic,
333
215
  });
334
216
 
335
- console.log(`🚀 ${pluginInfo.name} dev server listening on port ${port}`);
336
- console.log(`├─ 📡 RPC: http://localhost:${port}/api/rpc${rpcPrefix}`);
337
- console.log(`├─ 📖 Docs: http://localhost:${port}/api`);
338
- console.log(`└─ 💚 Health: http://localhost:${port}/`);
217
+ console.log(`🚀 ${pluginInfo.name} dev server listening on port ${host.port}`);
218
+ console.log(`├─ 📡 RPC: http://localhost:${host.port}/api/rpc${rpcPrefix}`);
219
+ console.log(`├─ 📖 Docs: http://localhost:${host.port}/api`);
220
+ console.log(`└─ 💚 Health: http://localhost:${host.port}/`);
339
221
 
340
222
  const load = async () => {
341
223
  try {
@@ -360,7 +242,7 @@ export async function startPluginDevServer(
360
242
 
361
243
  const registry: Record<string, { remote: string }> = {
362
244
  [pluginId]: {
363
- remote: `http://localhost:${port}/${DEV_ENTRY_FILENAME}`,
245
+ remote: `http://localhost:${host.port}/${DEV_ENTRY_FILENAME}`,
364
246
  },
365
247
  ...siblings,
366
248
  };
@@ -440,8 +322,8 @@ export async function startPluginDevServer(
440
322
  if (uiStaticServer) {
441
323
  await new Promise<void>((resolve) => uiStaticServer.close(() => resolve()));
442
324
  }
325
+ await host.close();
443
326
  if (runtime) await runtime.shutdown().catch(() => {});
444
- await new Promise<void>((resolve) => server.close(() => resolve()));
445
327
  };
446
328
 
447
329
  process.once("SIGINT", async () => {
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import "@orpc/openapi/extensions/route";
2
2
 
3
+ export * from "./client";
3
4
  export * from "./effect-helpers";
4
5
  export * from "./errors";
5
6
  export * from "./plugin";
@@ -13,10 +14,16 @@ export type {
13
14
  EveryPlugin,
14
15
  InitializedPlugin,
15
16
  LoadedPlugin,
17
+ ModuleClientEntry,
18
+ ModuleEndpoint,
19
+ ModuleSurface,
20
+ ModuleTransport,
21
+ PluginClientEntry,
16
22
  PluginClientType,
17
23
  PluginConfigInput,
18
24
  PluginContext,
19
25
  PluginContract,
26
+ PluginEndpoint,
20
27
  PluginInstance,
21
28
  PluginMetadata,
22
29
  PluginRegistry,
@@ -24,6 +31,8 @@ export type {
24
31
  PluginRuntimeConfig,
25
32
  PluginSecrets,
26
33
  PluginServicesEntry,
34
+ PluginSurface,
35
+ PluginTransport,
27
36
  PluginVariables,
28
37
  RegisteredPlugins,
29
38
  SecretsConfig,
@@ -23,8 +23,7 @@ const DEFAULT_TIMEOUT_MS = 120_000;
23
23
  const RETRY_BASE_DELAY_MS = 500;
24
24
  const RETRY_MAX_DELAY_MS = 5_000;
25
25
 
26
- export const sleep = (ms: number): Promise<void> =>
27
- new Promise((resolve) => setTimeout(resolve, ms));
26
+ const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
28
27
 
29
28
  /**
30
29
  * Deletes the failed remote's keys from the process-global entry-load cache.
package/src/types.ts CHANGED
@@ -24,6 +24,24 @@ import type { PluginWarningInfo } from "./runtime/errors";
24
24
  // biome-ignore lint/suspicious/noEmptyInterface: required for module augmentation pattern
25
25
  export interface RegisteredPlugins {}
26
26
 
27
+ export type ModuleTransport = "inproc" | "rpc" | "mf";
28
+
29
+ export type ModuleSurface = "api" | "ui" | "jobs";
30
+
31
+ export interface ModuleEndpoint {
32
+ readonly transport: ModuleTransport;
33
+ readonly surface?: ModuleSurface;
34
+ readonly url: string;
35
+ readonly path?: `/${string}`;
36
+ readonly manifest?: string;
37
+ }
38
+
39
+ export type PluginTransport = ModuleTransport;
40
+
41
+ export type PluginSurface = ModuleSurface;
42
+
43
+ export type PluginEndpoint = ModuleEndpoint;
44
+
27
45
  /**
28
46
  * Base type for any plugin instance.
29
47
  */
@@ -46,8 +64,14 @@ export type AnyPluginConstructor = {
46
64
  * Registry entry that supports both direct module imports and remote URLs
47
65
  */
48
66
  export type PluginRegistryEntry =
49
- | { module: AnyPluginConstructor; remote?: string; version?: string; description?: string }
50
- | { remote: string; version?: string; description?: string };
67
+ | {
68
+ module: AnyPluginConstructor;
69
+ remote?: string;
70
+ endpoint?: PluginEndpoint;
71
+ version?: string;
72
+ description?: string;
73
+ }
74
+ | { remote: string; endpoint?: PluginEndpoint; version?: string; description?: string };
51
75
 
52
76
  /**
53
77
  * Extract plugin constructor type from registry entry
@@ -116,8 +140,18 @@ export type PluginClientType<T> = RouterClient<PluginRouterType<T>>;
116
140
  export type PluginServicesEntry<T = any> = {
117
141
  client: (context?: PluginContextInput<T>) => PluginClientType<T>;
118
142
  router: PluginRouterType<T>;
143
+ endpoint?: PluginEndpoint;
119
144
  };
120
145
 
146
+ export type ModuleClientEntry<C extends AnyContractRouter = AnyContractRouter> = {
147
+ client: (context?: Record<string, unknown>) => RouterClient<ContractedRouter<C, any>>;
148
+ router?: ContractedRouter<C, any>;
149
+ endpoint?: PluginEndpoint;
150
+ };
151
+
152
+ export type PluginClientEntry<C extends AnyContractRouter = AnyContractRouter> =
153
+ ModuleClientEntry<C>;
154
+
121
155
  /**
122
156
  * Extract plugin type from registered plugins by key
123
157
  * @param K - The plugin key