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.
- package/dist/build/ui/generated-config.cjs +4 -0
- package/dist/build/ui/generated-config.cjs.map +1 -1
- package/dist/build/ui/generated-config.d.cts.map +1 -1
- package/dist/build/ui/generated-config.d.mts.map +1 -1
- package/dist/build/ui/generated-config.mjs +4 -1
- package/dist/build/ui/generated-config.mjs.map +1 -1
- package/dist/client.cjs +69 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +15 -0
- package/dist/client.d.cts.map +1 -0
- package/dist/client.d.mts +15 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +68 -0
- package/dist/client.mjs.map +1 -0
- package/dist/dev/host.cjs +149 -0
- package/dist/dev/host.cjs.map +1 -0
- package/dist/dev/host.d.cts +32 -0
- package/dist/dev/host.d.cts.map +1 -0
- package/dist/dev/host.d.mts +32 -0
- package/dist/dev/host.d.mts.map +1 -0
- package/dist/dev/host.mjs +148 -0
- package/dist/dev/host.mjs.map +1 -0
- package/dist/dev/serve.cjs +16 -111
- package/dist/dev/serve.cjs.map +1 -1
- package/dist/dev/serve.d.cts.map +1 -1
- package/dist/dev/serve.d.mts.map +1 -1
- package/dist/dev/serve.mjs +17 -112
- package/dist/dev/serve.mjs.map +1 -1
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +3 -2
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +2 -1
- package/dist/plugin.d.cts +1 -1
- package/dist/remote-entry.cjs +0 -1
- package/dist/remote-entry.cjs.map +1 -1
- package/dist/remote-entry.d.cts +1 -2
- package/dist/remote-entry.d.cts.map +1 -1
- package/dist/remote-entry.d.mts +1 -2
- package/dist/remote-entry.d.mts.map +1 -1
- package/dist/remote-entry.mjs +1 -1
- package/dist/remote-entry.mjs.map +1 -1
- package/dist/runtime/mf-config.cjs +1 -1
- package/dist/runtime/mf-config.mjs +1 -1
- package/dist/runtime/services/plugin-loader.service.d.cts +2 -2
- package/dist/types.d.cts +23 -2
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.mts +22 -1
- package/dist/types.d.mts.map +1 -1
- package/package.json +19 -2
- package/src/build/ui/generated-config.ts +4 -0
- package/src/client.ts +111 -0
- package/src/dev/host.ts +235 -0
- package/src/dev/serve.ts +22 -140
- package/src/index.ts +9 -0
- package/src/remote-entry.ts +1 -2
- 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
|
+
}
|
package/src/dev/host.ts
ADDED
|
@@ -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 {
|
|
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
|
-
|
|
181
|
-
`❌ UI dist is missing at ${uiDistDir}
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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,
|
package/src/remote-entry.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
| {
|
|
50
|
-
|
|
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
|