every-plugin 3.0.0-rc.0 → 3.0.0-rc.4
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.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 +14 -109
- 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 +14 -109
- package/dist/dev/serve.mjs.map +1 -1
- package/dist/errors.cjs +1 -0
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.cts +2 -2
- package/dist/errors.d.mts +2 -2
- package/dist/errors.mjs +2 -2
- package/dist/errors.mjs.map +1 -1
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +5 -4
- package/dist/index.d.mts +5 -4
- package/dist/index.mjs +3 -2
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +17 -3
- package/dist/plugin.d.cts.map +1 -1
- package/dist/plugin.d.mts +16 -2
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/runtime/errors.cjs +20 -0
- package/dist/runtime/errors.cjs.map +1 -1
- package/dist/runtime/errors.d.cts +23 -1
- package/dist/runtime/errors.d.cts.map +1 -1
- package/dist/runtime/errors.d.mts +23 -1
- package/dist/runtime/errors.d.mts.map +1 -1
- package/dist/runtime/errors.mjs +20 -1
- package/dist/runtime/errors.mjs.map +1 -1
- package/dist/runtime/mf-config.cjs +1 -1
- package/dist/runtime/mf-config.mjs +1 -1
- package/dist/runtime/services/module-federation.service.cjs +1 -7
- package/dist/runtime/services/module-federation.service.cjs.map +1 -1
- package/dist/runtime/services/module-federation.service.mjs +1 -7
- package/dist/runtime/services/module-federation.service.mjs.map +1 -1
- package/dist/runtime/services/plugin-loader.service.cjs +26 -1
- package/dist/runtime/services/plugin-loader.service.cjs.map +1 -1
- package/dist/runtime/services/plugin-loader.service.d.cts +2 -2
- package/dist/runtime/services/plugin-loader.service.mjs +26 -1
- package/dist/runtime/services/plugin-loader.service.mjs.map +1 -1
- package/dist/runtime/services/shared-identity.cjs +1 -1
- package/dist/runtime/services/shared-identity.cjs.map +1 -1
- package/dist/runtime/services/shared-identity.mjs +1 -1
- package/dist/runtime/services/shared-identity.mjs.map +1 -1
- package/dist/types.d.cts +26 -2
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.mts +25 -1
- package/dist/types.d.mts.map +1 -1
- package/dist/version-manifest.cjs +1 -1
- package/dist/version-manifest.cjs.map +1 -1
- package/dist/version-manifest.mjs +1 -1
- package/dist/version-manifest.mjs.map +1 -1
- package/package.json +19 -2
- package/src/client.ts +111 -0
- package/src/dev/host.ts +235 -0
- package/src/dev/serve.ts +14 -135
- package/src/errors.ts +2 -1
- package/src/index.ts +9 -0
- package/src/plugin.ts +20 -1
- package/src/runtime/errors.ts +44 -0
- package/src/runtime/services/module-federation.service.ts +4 -7
- package/src/runtime/services/plugin-loader.service.ts +32 -0
- package/src/runtime/services/shared-identity.ts +1 -1
- package/src/types.ts +39 -2
- package/src/version-manifest.ts +1 -1
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
|
@@ -10,6 +10,7 @@ import { getPluginInfo, loadDevConfig } from "../build/rspack/utils";
|
|
|
10
10
|
import { ensureGeneratedUiRsbuildConfig, pluginLayoutKey } from "../build/ui/generated-config";
|
|
11
11
|
import { PLUGIN_ERROR_STATUS_MAP } from "../errors";
|
|
12
12
|
import { loadRemoteWithRetry } from "../remote-entry";
|
|
13
|
+
import { startDirectModuleHost } from "./host";
|
|
13
14
|
import { killChildEscalating, watchParentDeath } from "./watch-kill";
|
|
14
15
|
|
|
15
16
|
const corsHeaders = {
|
|
@@ -81,12 +82,6 @@ const loadPluginWithRetry = (
|
|
|
81
82
|
}),
|
|
82
83
|
);
|
|
83
84
|
|
|
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
85
|
const sendText = (res: http.ServerResponse, status: number, text: string) => {
|
|
91
86
|
res.statusCode = status;
|
|
92
87
|
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
@@ -125,7 +120,6 @@ export async function startPluginDevServer(
|
|
|
125
120
|
const compositionKey = pluginLayoutKey(cwd) ?? pluginId;
|
|
126
121
|
const port = options.port ?? (Number(process.env.PORT) || devConfig?.port || 3999);
|
|
127
122
|
const rpcPrefix = normalizePrefix(devConfig?.prefix);
|
|
128
|
-
const rpcBase = `/api/rpc${rpcPrefix}`;
|
|
129
123
|
|
|
130
124
|
const distDir = path.join(cwd, "dist");
|
|
131
125
|
const serveStatic = sirv(distDir, { dev: true });
|
|
@@ -208,134 +202,19 @@ export async function startPluginDevServer(
|
|
|
208
202
|
const handlers: { rpc: any; api: any } = { rpc: null, api: null };
|
|
209
203
|
const effectContextHolder: { context: unknown } = { context: null };
|
|
210
204
|
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());
|
|
205
|
+
const host = await startDirectModuleHost({
|
|
206
|
+
name: pluginInfo.normalizedName,
|
|
207
|
+
version: pluginInfo.version,
|
|
208
|
+
handlers,
|
|
209
|
+
effectContext: () => effectContextHolder.context,
|
|
210
|
+
port,
|
|
211
|
+
staticHandler: serveStatic,
|
|
333
212
|
});
|
|
334
213
|
|
|
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}/`);
|
|
214
|
+
console.log(`🚀 ${pluginInfo.name} dev server listening on port ${host.port}`);
|
|
215
|
+
console.log(`├─ 📡 RPC: http://localhost:${host.port}/api/rpc${rpcPrefix}`);
|
|
216
|
+
console.log(`├─ 📖 Docs: http://localhost:${host.port}/api`);
|
|
217
|
+
console.log(`└─ 💚 Health: http://localhost:${host.port}/`);
|
|
339
218
|
|
|
340
219
|
const load = async () => {
|
|
341
220
|
try {
|
|
@@ -360,7 +239,7 @@ export async function startPluginDevServer(
|
|
|
360
239
|
|
|
361
240
|
const registry: Record<string, { remote: string }> = {
|
|
362
241
|
[pluginId]: {
|
|
363
|
-
remote: `http://localhost:${port}/${DEV_ENTRY_FILENAME}`,
|
|
242
|
+
remote: `http://localhost:${host.port}/${DEV_ENTRY_FILENAME}`,
|
|
364
243
|
},
|
|
365
244
|
...siblings,
|
|
366
245
|
};
|
|
@@ -440,8 +319,8 @@ export async function startPluginDevServer(
|
|
|
440
319
|
if (uiStaticServer) {
|
|
441
320
|
await new Promise<void>((resolve) => uiStaticServer.close(() => resolve()));
|
|
442
321
|
}
|
|
322
|
+
await host.close();
|
|
443
323
|
if (runtime) await runtime.shutdown().catch(() => {});
|
|
444
|
-
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
445
324
|
};
|
|
446
325
|
|
|
447
326
|
process.once("SIGINT", async () => {
|
package/src/errors.ts
CHANGED
|
@@ -125,9 +125,10 @@ export const PLUGIN_ERROR_STATUS_MAP: Record<string, number> = {
|
|
|
125
125
|
CONNECTION_ERROR: 502,
|
|
126
126
|
} as const;
|
|
127
127
|
|
|
128
|
-
export type { PluginLoadFailureInfo } from "./runtime/errors";
|
|
128
|
+
export type { PluginLoadFailureInfo, PluginWarningInfo } from "./runtime/errors";
|
|
129
129
|
export {
|
|
130
130
|
classifyPluginFailure,
|
|
131
|
+
defaultMissingSecretWarnings,
|
|
131
132
|
extractFromFiberFailure,
|
|
132
133
|
formatORPCError,
|
|
133
134
|
ModuleFederationError,
|
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/plugin.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { ContractedRouter, Implementer } from "@orpc/server";
|
|
|
5
5
|
import { implement } from "@orpc/server";
|
|
6
6
|
import type { Scope } from "effect";
|
|
7
7
|
import { Context, Effect, Layer } from "effect";
|
|
8
|
+
import type { PluginWarningInfo } from "./runtime/errors";
|
|
8
9
|
import { type PluginRuntimeError, toPluginRuntimeError } from "./runtime/errors";
|
|
9
10
|
|
|
10
11
|
type ContextOutput<T> = T extends AnySchema ? InferSchemaOutput<T> : Record<string, never>;
|
|
@@ -19,11 +20,20 @@ export type PluginConfigFor<
|
|
|
19
20
|
context: TRequestContext;
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
type PluginInitializeInput<V extends AnySchema, S extends AnySchema> = {
|
|
23
|
+
export type PluginInitializeInput<V extends AnySchema, S extends AnySchema> = {
|
|
23
24
|
variables: InferSchemaOutput<V>;
|
|
24
25
|
secrets: InferSchemaOutput<S>;
|
|
25
26
|
};
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* A plugin's own config-derived warnings, evaluated once with the validated
|
|
30
|
+
* config after initialization. Declaring `warnings` opts the plugin out of
|
|
31
|
+
* the loader's default missing-secret warning — these messages replace it.
|
|
32
|
+
*/
|
|
33
|
+
export type PluginWarningsFn<V extends AnySchema, S extends AnySchema> = (
|
|
34
|
+
config: PluginInitializeInput<V, S>,
|
|
35
|
+
) => PluginWarningInfo[] | string[];
|
|
36
|
+
|
|
27
37
|
export class PluginIdTag extends Context.Service<PluginIdTag, string>()("PluginId") {}
|
|
28
38
|
|
|
29
39
|
/**
|
|
@@ -50,6 +60,13 @@ type PluginDefinition<
|
|
|
50
60
|
* module statically.
|
|
51
61
|
*/
|
|
52
62
|
servicesTag?: Context.Key<any, any>;
|
|
63
|
+
/**
|
|
64
|
+
* Optional warnings function evaluated once with the validated config after
|
|
65
|
+
* initialization. Returns structured non-fatal warnings (e.g. an optional
|
|
66
|
+
* secret was not set, so the feature depending on it is disabled).
|
|
67
|
+
* Declaring it opts out of the loader's default missing-secret warning.
|
|
68
|
+
*/
|
|
69
|
+
warnings?: PluginWarningsFn<V, S>;
|
|
53
70
|
/**
|
|
54
71
|
* Initialize the plugin by returning an Effect `Layer`.
|
|
55
72
|
*
|
|
@@ -107,6 +124,7 @@ export interface Plugin<
|
|
|
107
124
|
readonly contract: TContract;
|
|
108
125
|
readonly configSchema: PluginConfigFor<TVariables, TSecrets, TRequestContext>;
|
|
109
126
|
readonly servicesTag?: Context.Key<any, any>;
|
|
127
|
+
readonly warnings?: PluginWarningsFn<TVariables, TSecrets>;
|
|
110
128
|
|
|
111
129
|
initialize(
|
|
112
130
|
config: PluginInitializeInput<TVariables, TSecrets>,
|
|
@@ -156,6 +174,7 @@ export const createPlugin: CreatePluginFn = function createPlugin<
|
|
|
156
174
|
readonly contract = config.contract;
|
|
157
175
|
readonly configSchema = configSchema;
|
|
158
176
|
readonly servicesTag = config.servicesTag;
|
|
177
|
+
readonly warnings = config.warnings;
|
|
159
178
|
|
|
160
179
|
initialize(
|
|
161
180
|
pluginConfig: PluginInitializeInput<V, S>,
|
package/src/runtime/errors.ts
CHANGED
|
@@ -282,6 +282,50 @@ export interface PluginLoadFailureInfo {
|
|
|
282
282
|
dbUrlMasked?: string;
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Structured, serializable warning surfaced alongside plugin failures in
|
|
287
|
+
* health endpoints and dashboards. Warnings are non-fatal: the plugin loaded
|
|
288
|
+
* and initialized successfully, but runs with reduced functionality (e.g. an
|
|
289
|
+
* optional secret was not set, so the feature depending on it is disabled).
|
|
290
|
+
*/
|
|
291
|
+
export interface PluginWarningInfo {
|
|
292
|
+
/** loader-side key (host entry key, dev plugin id) */
|
|
293
|
+
pluginKey: string;
|
|
294
|
+
message: string;
|
|
295
|
+
hint?: string;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The loader's default warning policy for declared-but-unset secrets: a
|
|
300
|
+
* successfully initialized plugin whose declared secret names resolved to
|
|
301
|
+
* nothing (missing or empty in the environment) gets one consolidated
|
|
302
|
+
* warning. `*_DATABASE_URL` keys are excluded — they have the shared-API-DB
|
|
303
|
+
* fallback, and a genuinely-missing DB URL fails initialization (a `failure`,
|
|
304
|
+
* not a warning). A plugin that declares its own `warnings` function opts out
|
|
305
|
+
* of this default.
|
|
306
|
+
*/
|
|
307
|
+
export const defaultMissingSecretWarnings = (
|
|
308
|
+
pluginKey: string,
|
|
309
|
+
declaredKeys: readonly string[],
|
|
310
|
+
resolvedKeys: readonly string[],
|
|
311
|
+
): PluginWarningInfo[] => {
|
|
312
|
+
const resolved = new Set(resolvedKeys);
|
|
313
|
+
const missing = declaredKeys.filter(
|
|
314
|
+
(key) => !resolved.has(key) && !key.endsWith("_DATABASE_URL"),
|
|
315
|
+
);
|
|
316
|
+
if (missing.length === 0) return [];
|
|
317
|
+
return [
|
|
318
|
+
{
|
|
319
|
+
pluginKey,
|
|
320
|
+
message:
|
|
321
|
+
missing.length === 1
|
|
322
|
+
? `Declared secret ${missing[0]} is not set — features depending on it are disabled`
|
|
323
|
+
: `${missing.length} declared secrets are not set: ${missing.join(", ")} — features depending on them are disabled`,
|
|
324
|
+
hint: "Set the secret(s) in your environment (.env in development); see the plugin's config schema for what each enables",
|
|
325
|
+
},
|
|
326
|
+
];
|
|
327
|
+
};
|
|
328
|
+
|
|
285
329
|
const NETWORK_RETRYABLE_PATTERNS = ["ETIMEDOUT", "ECONNRESET", "timeout", "503", "429"] as const;
|
|
286
330
|
const NETWORK_DEAD_PATTERNS = ["ECONNREFUSED", "ENOTFOUND", "EHOSTUNREACH"] as const;
|
|
287
331
|
const RETRYABLE_ORPC_CODES = new Set([
|
|
@@ -111,13 +111,10 @@ const resolveRemoteSelfName = (pluginId: string, url: string) =>
|
|
|
111
111
|
const memo = remoteSelfNames.get(memoKey);
|
|
112
112
|
if (memo) return memo;
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
} catch {
|
|
119
|
-
selfName = null;
|
|
120
|
-
}
|
|
114
|
+
const manifest = yield* Effect.tryPromise(() => fetchRemoteIdentityManifest(url)).pipe(
|
|
115
|
+
Effect.orElseSucceed(() => null),
|
|
116
|
+
);
|
|
117
|
+
const selfName = manifest?.metaData?.name?.trim() || null;
|
|
121
118
|
|
|
122
119
|
const resolved = selfName ?? getNormalizedRemoteName(pluginId);
|
|
123
120
|
remoteSelfNames.set(memoKey, resolved);
|
|
@@ -11,11 +11,39 @@ import type {
|
|
|
11
11
|
PluginMetadata,
|
|
12
12
|
PluginRegistry,
|
|
13
13
|
} from "../../types";
|
|
14
|
+
import type { PluginWarningInfo } from "../errors";
|
|
14
15
|
import { PluginRuntimeError, toPluginRuntimeError } from "../errors";
|
|
15
16
|
import { validate } from "../validation";
|
|
16
17
|
import { ModuleFederationService } from "./module-federation.service";
|
|
17
18
|
import { SecretsService } from "./secrets.service";
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Evaluate a plugin's `warnings` function against the validated config.
|
|
22
|
+
* Fail-safe: a throwing warnings function becomes a warning itself rather
|
|
23
|
+
* than failing initialization. Strings are normalized to `PluginWarningInfo`
|
|
24
|
+
* with the plugin's registry key.
|
|
25
|
+
*/
|
|
26
|
+
const collectPluginWarnings = (
|
|
27
|
+
plugin: AnyPlugin,
|
|
28
|
+
config: { variables: unknown; secrets: unknown },
|
|
29
|
+
): PluginWarningInfo[] => {
|
|
30
|
+
const warningsFn = plugin.warnings;
|
|
31
|
+
if (!warningsFn) return [];
|
|
32
|
+
try {
|
|
33
|
+
const raw = warningsFn(config as never);
|
|
34
|
+
return raw.map((warning) =>
|
|
35
|
+
typeof warning === "string" ? { pluginKey: plugin.id, message: warning } : warning,
|
|
36
|
+
);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
pluginKey: plugin.id,
|
|
41
|
+
message: `Plugin warnings check failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
19
47
|
export class PluginRegistryTag extends Context.Service<PluginRegistryTag, PluginRegistry>()(
|
|
20
48
|
"PluginRegistry",
|
|
21
49
|
) {}
|
|
@@ -313,6 +341,10 @@ export const PluginLoaderServiceDefault = Layer.effect(
|
|
|
313
341
|
config: { variables: _variables, secrets: hydratedConfig.secrets },
|
|
314
342
|
effectContext,
|
|
315
343
|
scope,
|
|
344
|
+
warnings: collectPluginWarnings(plugin, {
|
|
345
|
+
variables: _variables,
|
|
346
|
+
secrets: hydratedConfig.secrets,
|
|
347
|
+
}),
|
|
316
348
|
} satisfies InitializedPlugin<T>;
|
|
317
349
|
}),
|
|
318
350
|
};
|
|
@@ -19,7 +19,7 @@ function manifestUrl(remoteUrl: string): string {
|
|
|
19
19
|
|
|
20
20
|
function normalizeBaseUrl(remoteUrl: string): string {
|
|
21
21
|
const withoutHash = remoteUrl.split("#")[0] ?? remoteUrl;
|
|
22
|
-
return withoutHash.replace(/\/(mf-manifest\.json|remoteEntry
|
|
22
|
+
return withoutHash.replace(/\/(mf-manifest\.json|remoteEntry(?:\.[^/]+)?\.js)$/i, "");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function compareSharedIdentity(
|