env-runner 0.1.1 → 0.1.2
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/README.md +180 -8
- package/dist/_chunks/base-runner.d.mts +7 -62
- package/dist/_chunks/base-runner.mjs +65 -0
- package/dist/_chunks/libs/std-env.mjs +1 -1
- package/dist/_chunks/runner.d.mts +2 -1
- package/dist/_chunks/runner2.d.mts +51 -2
- package/dist/_chunks/runner2.mjs +403 -83
- package/dist/_chunks/server.mjs +48 -0
- package/dist/_chunks/types.d.mts +76 -0
- package/dist/_chunks/worker-utils.mjs +11 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +10 -3
- package/dist/runners/bun-process/runner.d.mts +2 -1
- package/dist/runners/bun-process/worker.mjs +26 -5
- package/dist/runners/deno-process/worker.mjs +26 -5
- package/dist/runners/miniflare/runner.d.mts +2 -2
- package/dist/runners/node-process/runner.d.mts +2 -1
- package/dist/runners/node-process/worker.mjs +26 -5
- package/dist/runners/node-worker/runner.d.mts +2 -1
- package/dist/runners/node-worker/worker.mjs +26 -5
- package/dist/runners/self/runner.d.mts +12 -1
- package/dist/runners/self/runner.mjs +13 -1
- package/dist/vite.d.mts +32 -0
- package/dist/vite.mjs +58 -0
- package/package.json +3 -2
package/dist/_chunks/runner2.mjs
CHANGED
|
@@ -1,25 +1,212 @@
|
|
|
1
1
|
import { t as BaseEnvRunner } from "./base-runner.mjs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { readFileSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
//#region src/runners/miniflare/wrapper.ts
|
|
7
|
+
const IPC_PATH$1 = "/__env_runner_ipc";
|
|
8
|
+
/**
|
|
9
|
+
* Generates a wrapper module that imports the user entry and adds IPC glue.
|
|
10
|
+
*
|
|
11
|
+
* The user module is expected to export `fetch` and optionally `ipc`.
|
|
12
|
+
* The wrapper uses a persistent WebSocket pair for bidirectional IPC:
|
|
13
|
+
* - Init: `fetch` with `upgrade: websocket` creates a WebSocketPair
|
|
14
|
+
* - Messages: JSON over the WebSocket (no per-message `dispatchFetch`)
|
|
15
|
+
* - Reload: `{ type: "reload" }` triggers cache-busted re-import
|
|
16
|
+
* - Shutdown: `{ type: "shutdown" }` calls `ipc.onClose()`
|
|
17
|
+
*
|
|
18
|
+
* Passed as an in-memory `script` to Miniflare (no temp files needed).
|
|
19
|
+
*/
|
|
20
|
+
function generateWrapper(entryPath, opts) {
|
|
21
|
+
const staticReExport = opts?.dynamicOnly ? "" : `export * from ${JSON.stringify(entryPath)};`;
|
|
22
|
+
const explicitExports = opts?.dynamicOnly && opts.exports?.length ? opts.exports.map((name) => `export { ${name} } from ${JSON.stringify(entryPath)};`).join("\n") : "";
|
|
23
|
+
const fetchBody = opts?.captureErrors ?? true ? `try {
|
|
24
|
+
return await entryFetch(request, env, ctx);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
27
|
+
const body = JSON.stringify({
|
|
28
|
+
error: error.message,
|
|
29
|
+
stack: error.stack,
|
|
30
|
+
name: error.constructor?.name || "Error",
|
|
31
|
+
});
|
|
32
|
+
return new Response(body, {
|
|
33
|
+
status: 500,
|
|
34
|
+
headers: { "Content-Type": "application/json", "X-Env-Runner-Error": "1" },
|
|
35
|
+
});
|
|
36
|
+
}` : `return entryFetch(request, env, ctx);`;
|
|
37
|
+
return `import __process from "node:process";
|
|
38
|
+
if (!globalThis.process) { globalThis.process = __process; }
|
|
39
|
+
${staticReExport}
|
|
40
|
+
${explicitExports}
|
|
41
|
+
|
|
42
|
+
const __IPC_PATH = "${IPC_PATH$1}";
|
|
43
|
+
const __entryPath = ${JSON.stringify(entryPath)};
|
|
44
|
+
let __userEntry;
|
|
45
|
+
let __ipcInitialized = false;
|
|
46
|
+
let __serverWs;
|
|
47
|
+
|
|
48
|
+
async function __loadEntry(env, path) {
|
|
49
|
+
globalThis.__ENV_RUNNER_UNSAFE_EVAL__ = env.__ENV_RUNNER_UNSAFE_EVAL__;
|
|
50
|
+
const importFn = env.__ENV_RUNNER_UNSAFE_EVAL__.newAsyncFunction(
|
|
51
|
+
"return await import(path)",
|
|
52
|
+
"loadEntry",
|
|
53
|
+
"path"
|
|
54
|
+
);
|
|
55
|
+
const mod = await importFn(path);
|
|
56
|
+
return mod.default || mod;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function __sendMessage(message) {
|
|
60
|
+
if (__serverWs) {
|
|
61
|
+
__serverWs.send(JSON.stringify(message));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function __handleWsMessage(env, data) {
|
|
66
|
+
let msg;
|
|
67
|
+
try { msg = JSON.parse(data); } catch { return; }
|
|
68
|
+
|
|
69
|
+
if (msg.type === "message") {
|
|
70
|
+
if (__userEntry?.ipc?.onMessage) {
|
|
71
|
+
__userEntry.ipc.onMessage(msg.data);
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (msg.type === "reload" && env.__ENV_RUNNER_UNSAFE_EVAL__) {
|
|
77
|
+
const version = msg.version || 0;
|
|
78
|
+
try {
|
|
79
|
+
const newEntry = await __loadEntry(env, __entryPath + "?t=" + version);
|
|
80
|
+
if (__userEntry?.ipc?.onClose) {
|
|
81
|
+
await __userEntry.ipc.onClose();
|
|
82
|
+
}
|
|
83
|
+
__userEntry = newEntry;
|
|
84
|
+
__ipcInitialized = false;
|
|
85
|
+
if (__userEntry.ipc?.onOpen) {
|
|
86
|
+
__ipcInitialized = true;
|
|
87
|
+
await __userEntry.ipc.onOpen({ sendMessage: __sendMessage });
|
|
88
|
+
}
|
|
89
|
+
__sendMessage({ event: "module-reloaded" });
|
|
90
|
+
} catch (e) {
|
|
91
|
+
__sendMessage({ event: "module-reloaded", error: String(e) });
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (msg.type === "shutdown") {
|
|
97
|
+
if (__userEntry?.ipc?.onClose) {
|
|
98
|
+
await __userEntry.ipc.onClose();
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default {
|
|
105
|
+
async fetch(request, env, ctx) {
|
|
106
|
+
const url = new URL(request.url);
|
|
107
|
+
|
|
108
|
+
// WebSocket IPC handshake
|
|
109
|
+
if (url.pathname === __IPC_PATH && request.headers.get("upgrade") === "websocket") {
|
|
110
|
+
try {
|
|
111
|
+
if (!__userEntry) {
|
|
112
|
+
__userEntry = await __loadEntry(env, __entryPath);
|
|
113
|
+
}
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return new Response("Failed to load entry: " + String(e), { status: 500 });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const pair = new WebSocketPair();
|
|
119
|
+
const client = pair[0];
|
|
120
|
+
const server = pair[1];
|
|
121
|
+
server.accept();
|
|
122
|
+
__serverWs = server;
|
|
123
|
+
|
|
124
|
+
server.addEventListener("message", (event) => {
|
|
125
|
+
__handleWsMessage(env, event.data);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Initialize IPC hooks
|
|
129
|
+
if (!__ipcInitialized && __userEntry.ipc) {
|
|
130
|
+
__ipcInitialized = true;
|
|
131
|
+
if (__userEntry.ipc.onOpen) {
|
|
132
|
+
await __userEntry.ipc.onOpen({ sendMessage: __sendMessage });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return new Response(null, { status: 101, webSocket: client });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!__userEntry) {
|
|
140
|
+
return new Response("Worker not initialized", { status: 503 });
|
|
141
|
+
}
|
|
142
|
+
const entryFetch = __userEntry.fetch;
|
|
143
|
+
if (!entryFetch) {
|
|
144
|
+
return new Response("No fetch handler exported", { status: 500 });
|
|
145
|
+
}
|
|
146
|
+
${fetchBody}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
3
152
|
//#region src/runners/miniflare/runner.ts
|
|
4
|
-
const
|
|
153
|
+
const IPC_PATH = "/__env_runner_ipc";
|
|
154
|
+
const _miniflareCache = /* @__PURE__ */ new Map();
|
|
5
155
|
var MiniflareEnvRunner = class extends BaseEnvRunner {
|
|
6
156
|
#miniflare;
|
|
7
157
|
#miniflareOptions;
|
|
158
|
+
#transformRequest;
|
|
159
|
+
#reloadCounter = 0;
|
|
160
|
+
#ws;
|
|
161
|
+
#persistent;
|
|
162
|
+
#cacheKey;
|
|
163
|
+
#exports;
|
|
164
|
+
#captureErrors;
|
|
8
165
|
constructor(opts) {
|
|
9
166
|
super({
|
|
10
167
|
...opts,
|
|
11
168
|
workerEntry: ""
|
|
12
169
|
});
|
|
13
170
|
this.#miniflareOptions = opts.miniflareOptions || {};
|
|
171
|
+
this.#transformRequest = opts.transformRequest;
|
|
172
|
+
this.#persistent = opts.persistent ?? false;
|
|
173
|
+
this.#exports = opts.exports ?? {};
|
|
174
|
+
this.#captureErrors = opts.captureErrors ?? true;
|
|
14
175
|
this.#init();
|
|
15
176
|
}
|
|
177
|
+
/** Dispose all persistent Miniflare instances from the cache. */
|
|
178
|
+
static async disposeAll() {
|
|
179
|
+
const entries = [..._miniflareCache.values()];
|
|
180
|
+
_miniflareCache.clear();
|
|
181
|
+
for (const entry of entries) await entry.mf.dispose().catch(() => {});
|
|
182
|
+
}
|
|
183
|
+
/** Fully dispose the Miniflare instance (even if persistent). */
|
|
184
|
+
async dispose() {
|
|
185
|
+
if (this.#miniflare) {
|
|
186
|
+
if (this.#ws) {
|
|
187
|
+
this.#ws.send(JSON.stringify({ type: "shutdown" }));
|
|
188
|
+
this.#ws.close();
|
|
189
|
+
this.#ws = void 0;
|
|
190
|
+
}
|
|
191
|
+
if (this.#cacheKey) _miniflareCache.delete(this.#cacheKey);
|
|
192
|
+
await this.#miniflare.dispose();
|
|
193
|
+
this.#miniflare = void 0;
|
|
194
|
+
}
|
|
195
|
+
if (!this.closed) await this.close();
|
|
196
|
+
}
|
|
16
197
|
async fetch(input, init) {
|
|
17
198
|
if (!this.#miniflare || this.closed) return new Response("miniflare env runner is unavailable", { status: 503 });
|
|
18
199
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
19
|
-
|
|
200
|
+
const res = await this.#miniflare.dispatchFetch(url, init);
|
|
201
|
+
if (res instanceof Response) return res;
|
|
202
|
+
return new Response(res.body, {
|
|
203
|
+
status: res.status,
|
|
204
|
+
statusText: res.statusText,
|
|
205
|
+
headers: res.headers
|
|
206
|
+
});
|
|
20
207
|
}
|
|
21
208
|
sendMessage(message) {
|
|
22
|
-
if (!this.#
|
|
209
|
+
if (!this.#ws) throw new Error("Miniflare env runner should be initialized before sending messages.");
|
|
23
210
|
if (message?.type === "ping") {
|
|
24
211
|
queueMicrotask(() => this._handleMessage({
|
|
25
212
|
type: "pong",
|
|
@@ -27,11 +214,45 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
|
|
|
27
214
|
}));
|
|
28
215
|
return;
|
|
29
216
|
}
|
|
30
|
-
this.#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
217
|
+
this.#ws.send(JSON.stringify({
|
|
218
|
+
type: "message",
|
|
219
|
+
data: message
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Hot-reload the user entry module without recreating the Miniflare instance.
|
|
224
|
+
*
|
|
225
|
+
* Sends `reload-module` event over the WebSocket. The worker wrapper uses
|
|
226
|
+
* `unsafeEvalBinding` to re-import the entry with a cache-busting query string
|
|
227
|
+
* and responds with `module-reloaded` when done.
|
|
228
|
+
*/
|
|
229
|
+
async reloadModule(timeout = 5e3) {
|
|
230
|
+
if (!this.#ws) throw new Error("Miniflare env runner should be initialized before reloading.");
|
|
231
|
+
if (!this._data?.entry) return;
|
|
232
|
+
this.#reloadCounter++;
|
|
233
|
+
const version = this.#reloadCounter;
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
const timer = setTimeout(() => {
|
|
236
|
+
cleanup();
|
|
237
|
+
reject(/* @__PURE__ */ new Error("Module reload timed out"));
|
|
238
|
+
}, timeout);
|
|
239
|
+
const listener = (msg) => {
|
|
240
|
+
if (msg?.event === "module-reloaded") {
|
|
241
|
+
cleanup();
|
|
242
|
+
if (msg.error) reject(typeof msg.error === "string" ? new Error(msg.error) : msg.error);
|
|
243
|
+
else resolve();
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
const cleanup = () => {
|
|
247
|
+
clearTimeout(timer);
|
|
248
|
+
this.offMessage(listener);
|
|
249
|
+
};
|
|
250
|
+
this.onMessage(listener);
|
|
251
|
+
this.#ws.send(JSON.stringify({
|
|
252
|
+
type: "reload",
|
|
253
|
+
version
|
|
254
|
+
}));
|
|
255
|
+
});
|
|
35
256
|
}
|
|
36
257
|
_hasRuntime() {
|
|
37
258
|
return Boolean(this.#miniflare);
|
|
@@ -41,11 +262,21 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
|
|
|
41
262
|
}
|
|
42
263
|
async _closeRuntime() {
|
|
43
264
|
if (!this.#miniflare) return;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
265
|
+
if (this.#ws) {
|
|
266
|
+
this.#ws.send(JSON.stringify({ type: "shutdown" }));
|
|
267
|
+
this.#ws.close();
|
|
268
|
+
this.#ws = void 0;
|
|
269
|
+
}
|
|
270
|
+
if (this.#persistent && this.#cacheKey) {
|
|
271
|
+
const cached = _miniflareCache.get(this.#cacheKey);
|
|
272
|
+
if (cached) {
|
|
273
|
+
cached.refCount--;
|
|
274
|
+
if (cached.refCount <= 0) {
|
|
275
|
+
_miniflareCache.delete(this.#cacheKey);
|
|
276
|
+
await this.#miniflare.dispose();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} else await this.#miniflare.dispose();
|
|
49
280
|
this.#miniflare = void 0;
|
|
50
281
|
}
|
|
51
282
|
#init() {
|
|
@@ -57,29 +288,150 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
|
|
|
57
288
|
async #initAsync() {
|
|
58
289
|
const { Miniflare } = await import("miniflare");
|
|
59
290
|
const entryPath = this._data?.entry;
|
|
291
|
+
const userFlags = this.#miniflareOptions.compatibilityFlags || [];
|
|
60
292
|
const options = {
|
|
61
293
|
compatibilityDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
62
294
|
modules: true,
|
|
63
|
-
...this.#miniflareOptions
|
|
295
|
+
...this.#miniflareOptions,
|
|
296
|
+
compatibilityFlags: [...new Set(["nodejs_compat", ...userFlags])]
|
|
64
297
|
};
|
|
65
|
-
options.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
298
|
+
if (entryPath && !options.script && !options.scriptPath) {
|
|
299
|
+
const resolvedEntry = resolve(entryPath);
|
|
300
|
+
const entryDir = dirname(resolvedEntry);
|
|
301
|
+
const detectedExports = this.#exports === false || this.#exports === void 0 ? [] : detectExportedClasses(resolvedEntry, typeof this.#exports === "object" ? this.#exports : {});
|
|
302
|
+
if (detectedExports.length > 0 && !options.durableObjects) {
|
|
303
|
+
const autoDOs = { ...this.#miniflareOptions.durableObjects || {} };
|
|
304
|
+
for (const name of detectedExports) {
|
|
305
|
+
const bindingName = toScreamingSnakeCase(name);
|
|
306
|
+
if (!autoDOs[bindingName]) autoDOs[bindingName] = name;
|
|
307
|
+
}
|
|
308
|
+
options.durableObjects = autoDOs;
|
|
71
309
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
310
|
+
options.script = generateWrapper(resolvedEntry, {
|
|
311
|
+
dynamicOnly: true,
|
|
312
|
+
captureErrors: this.#captureErrors,
|
|
313
|
+
exports: detectedExports
|
|
314
|
+
});
|
|
315
|
+
options.scriptPath = entryDir + "/__env_runner_wrapper.mjs";
|
|
316
|
+
if (!options.modulesRoot) options.modulesRoot = "/";
|
|
317
|
+
options.unsafeEvalBinding = "__ENV_RUNNER_UNSAFE_EVAL__";
|
|
318
|
+
if (this.#transformRequest && !options.modulesRules) options.modulesRules = [{
|
|
319
|
+
type: "ESModule",
|
|
320
|
+
include: [
|
|
321
|
+
"**/*.ts",
|
|
322
|
+
"**/*.tsx",
|
|
323
|
+
"**/*.jsx",
|
|
324
|
+
"**/*.mts"
|
|
325
|
+
]
|
|
326
|
+
}];
|
|
327
|
+
if (!options.unsafeModuleFallbackService) {
|
|
328
|
+
const _require = createRequire(resolvedEntry);
|
|
329
|
+
const _transformRequest = this.#transformRequest;
|
|
330
|
+
options.unsafeUseModuleFallbackService = true;
|
|
331
|
+
const modulePathMap = /* @__PURE__ */ new Map();
|
|
332
|
+
options.unsafeModuleFallbackService = async (request) => {
|
|
333
|
+
const url = new URL(request.url);
|
|
334
|
+
const specifier = url.searchParams.get("specifier");
|
|
335
|
+
const rawSpecifier = url.searchParams.get("rawSpecifier");
|
|
336
|
+
const referrer = url.searchParams.get("referrer") || "";
|
|
337
|
+
if (!specifier) return new Response(null, { status: 404 });
|
|
338
|
+
const cleanSpecifier = specifier.split("?")[0] || specifier;
|
|
339
|
+
const cleanRaw = rawSpecifier?.split("?")[0];
|
|
340
|
+
let resolvedPath;
|
|
341
|
+
const fileUrlRaw = cleanRaw || cleanSpecifier;
|
|
342
|
+
if (fileUrlRaw.startsWith("file://")) try {
|
|
343
|
+
resolvedPath = fileURLToPath(fileUrlRaw);
|
|
344
|
+
} catch {
|
|
345
|
+
return new Response(null, { status: 404 });
|
|
346
|
+
}
|
|
347
|
+
else if (cleanRaw && !cleanRaw.startsWith(".") && !cleanRaw.startsWith("/")) {
|
|
348
|
+
const referrerKey = referrer.startsWith("/") ? referrer.slice(1) : referrer;
|
|
349
|
+
const referrerReal = modulePathMap.get(referrerKey);
|
|
350
|
+
const contextRequire = referrerReal ? createRequire(referrerReal) : _require;
|
|
351
|
+
if (cleanRaw.startsWith("node:")) {
|
|
352
|
+
const nodeName = cleanRaw.slice(5);
|
|
353
|
+
try {
|
|
354
|
+
resolvedPath = contextRequire.resolve(`unenv/node/${nodeName}`);
|
|
355
|
+
} catch {
|
|
356
|
+
return new Response(null, { status: 404 });
|
|
357
|
+
}
|
|
358
|
+
} else try {
|
|
359
|
+
resolvedPath = contextRequire.resolve(cleanRaw);
|
|
360
|
+
} catch {
|
|
361
|
+
const name = cleanSpecifier.startsWith("/") ? cleanSpecifier.slice(1) : cleanSpecifier;
|
|
362
|
+
return Response.json({
|
|
363
|
+
name,
|
|
364
|
+
esModule: "export default undefined;"
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
const referrerKey = referrer.startsWith("/") ? referrer.slice(1) : referrer;
|
|
369
|
+
const referrerDir = dirname(modulePathMap.get(referrerKey) || (referrer.startsWith("/") ? referrer : "/" + referrer));
|
|
370
|
+
const raw = cleanRaw || cleanSpecifier;
|
|
371
|
+
if (raw.startsWith(".")) resolvedPath = resolve(referrerDir, raw);
|
|
372
|
+
else if (cleanSpecifier.startsWith("/")) resolvedPath = cleanSpecifier;
|
|
373
|
+
else try {
|
|
374
|
+
resolvedPath = _require.resolve(raw);
|
|
375
|
+
} catch {
|
|
376
|
+
return new Response(null, { status: 404 });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const rawQuery = specifier.includes("?") ? specifier.slice(specifier.indexOf("?")) : "";
|
|
380
|
+
const name = (cleanSpecifier.startsWith("/") ? cleanSpecifier.slice(1) : cleanSpecifier) + rawQuery;
|
|
381
|
+
if (_transformRequest) try {
|
|
382
|
+
const result = await _transformRequest(resolvedPath);
|
|
383
|
+
if (result?.code) {
|
|
384
|
+
modulePathMap.set(name, resolvedPath);
|
|
385
|
+
return Response.json({
|
|
386
|
+
name,
|
|
387
|
+
esModule: result.code
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
} catch {}
|
|
391
|
+
try {
|
|
392
|
+
const contents = readFileSync(resolvedPath, "utf8");
|
|
393
|
+
modulePathMap.set(name, resolvedPath);
|
|
394
|
+
const isESM = resolvedPath.endsWith(".mjs") || !resolvedPath.endsWith(".cjs") && /\b(import\s|import\(|export\s|export\{|import\.meta\b)/.test(contents);
|
|
395
|
+
return Response.json({
|
|
396
|
+
name,
|
|
397
|
+
...isESM ? { esModule: contents } : { commonJsModule: contents }
|
|
398
|
+
});
|
|
399
|
+
} catch {
|
|
400
|
+
return new Response(null, { status: 404 });
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (this.#persistent && entryPath) {
|
|
406
|
+
this.#cacheKey = computeCacheKey(entryPath, this.#miniflareOptions);
|
|
407
|
+
const cached = _miniflareCache.get(this.#cacheKey);
|
|
408
|
+
if (cached) {
|
|
409
|
+
this.#miniflare = cached.mf;
|
|
410
|
+
cached.refCount++;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (!this.#miniflare) {
|
|
414
|
+
this.#miniflare = new Miniflare(options);
|
|
415
|
+
await this.#miniflare.ready;
|
|
416
|
+
if (this.#persistent && this.#cacheKey) _miniflareCache.set(this.#cacheKey, {
|
|
417
|
+
mf: this.#miniflare,
|
|
418
|
+
refCount: 1
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
const initRes = await this.#miniflare.dispatchFetch("http://localhost" + IPC_PATH, { headers: { upgrade: "websocket" } });
|
|
422
|
+
const ws = initRes.webSocket;
|
|
423
|
+
if (!ws) {
|
|
424
|
+
const body = await initRes.text().catch(() => "");
|
|
425
|
+
throw new Error(`Failed to establish WebSocket IPC channel (${initRes.status}: ${body})`);
|
|
76
426
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
427
|
+
ws.accept();
|
|
428
|
+
this.#ws = ws;
|
|
429
|
+
ws.addEventListener("message", (event) => {
|
|
430
|
+
try {
|
|
431
|
+
const parsed = JSON.parse(event.data);
|
|
432
|
+
this._handleMessage(parsed);
|
|
433
|
+
} catch {}
|
|
434
|
+
});
|
|
83
435
|
this._handleMessage({ address: {
|
|
84
436
|
host: "127.0.0.1",
|
|
85
437
|
port: 0
|
|
@@ -87,60 +439,28 @@ var MiniflareEnvRunner = class extends BaseEnvRunner {
|
|
|
87
439
|
}
|
|
88
440
|
};
|
|
89
441
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* The user module is expected to `export default { fetch, ipc? }`.
|
|
93
|
-
* The wrapper renames the user's default export, intercepts IPC requests,
|
|
94
|
-
* and bridges the `ipc` hooks via `env.__ENV_RUNNER_IPC` service binding.
|
|
442
|
+
* Detect `export class` declarations in the entry file.
|
|
443
|
+
* Merges with explicitly declared exports from options.
|
|
95
444
|
*/
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
});
|
|
116
|
-
};
|
|
117
|
-
if (__userEntry.ipc.onOpen) {
|
|
118
|
-
await __userEntry.ipc.onOpen({ sendMessage });
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return new Response(null, { status: 204 });
|
|
122
|
-
}
|
|
123
|
-
if (ipcType === "message") {
|
|
124
|
-
const message = await request.json();
|
|
125
|
-
if (__userEntry.ipc?.onMessage) {
|
|
126
|
-
__userEntry.ipc.onMessage(message);
|
|
127
|
-
}
|
|
128
|
-
return new Response(null, { status: 204 });
|
|
129
|
-
}
|
|
130
|
-
if (ipcType === "shutdown") {
|
|
131
|
-
if (__userEntry.ipc?.onClose) {
|
|
132
|
-
await __userEntry.ipc.onClose();
|
|
133
|
-
}
|
|
134
|
-
return new Response(null, { status: 204 });
|
|
135
|
-
}
|
|
136
|
-
return new Response(null, { status: 400 });
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Delegate to user's fetch handler
|
|
140
|
-
return __userEntry.fetch(request, env, ctx);
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
`;
|
|
445
|
+
function detectExportedClasses(entryPath, explicit) {
|
|
446
|
+
const names = new Set(Object.keys(explicit));
|
|
447
|
+
try {
|
|
448
|
+
const source = readFileSync(entryPath, "utf8");
|
|
449
|
+
const re = /\bexport\s+class\s+(\w+)/g;
|
|
450
|
+
let match;
|
|
451
|
+
while (match = re.exec(source)) if (match[1]) names.add(match[1]);
|
|
452
|
+
} catch {}
|
|
453
|
+
return [...names];
|
|
454
|
+
}
|
|
455
|
+
/** Convert PascalCase/camelCase to SCREAMING_SNAKE_CASE (e.g. `Counter` → `COUNTER`, `MyDurableObject` → `MY_DURABLE_OBJECT`). */
|
|
456
|
+
function toScreamingSnakeCase(name) {
|
|
457
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
|
|
458
|
+
}
|
|
459
|
+
/** Compute a stable cache key for persistent Miniflare instances. */
|
|
460
|
+
function computeCacheKey(entryPath, opts) {
|
|
461
|
+
const serializableOpts = {};
|
|
462
|
+
for (const [k, v] of Object.entries(opts)) if (typeof v !== "function") serializableOpts[k] = v;
|
|
463
|
+
return `${resolve(entryPath)}::${JSON.stringify(serializableOpts)}`;
|
|
144
464
|
}
|
|
145
465
|
//#endregion
|
|
146
466
|
export { MiniflareEnvRunner as t };
|
package/dist/_chunks/server.mjs
CHANGED
|
@@ -51,6 +51,54 @@ var RunnerManager = class {
|
|
|
51
51
|
this._messageListeners.delete(listener);
|
|
52
52
|
this._runner?.offMessage(listener);
|
|
53
53
|
}
|
|
54
|
+
waitForReady(timeout = 5e3) {
|
|
55
|
+
if (this.ready) return Promise.resolve();
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
this._messageListeners.delete(listener);
|
|
59
|
+
reject(/* @__PURE__ */ new Error("Runner did not become ready in time"));
|
|
60
|
+
}, timeout);
|
|
61
|
+
const listener = (message) => {
|
|
62
|
+
if (message?.address || this.ready) {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
this._messageListeners.delete(listener);
|
|
65
|
+
resolve();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
this._messageListeners.add(listener);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
rpc(name, data, opts) {
|
|
72
|
+
const id = Math.random().toString(36).slice(2);
|
|
73
|
+
const timeout = opts?.timeout ?? 3e3;
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const timer = setTimeout(() => {
|
|
76
|
+
cleanup();
|
|
77
|
+
reject(/* @__PURE__ */ new Error(`RPC "${name}" timed out`));
|
|
78
|
+
}, timeout);
|
|
79
|
+
const listener = (msg) => {
|
|
80
|
+
if (msg?.__rpc_id === id) {
|
|
81
|
+
cleanup();
|
|
82
|
+
if (msg.error) reject(typeof msg.error === "string" ? new Error(msg.error) : msg.error);
|
|
83
|
+
else resolve(msg.data);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const cleanup = () => {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
this.offMessage(listener);
|
|
89
|
+
};
|
|
90
|
+
this.onMessage(listener);
|
|
91
|
+
this.sendMessage({
|
|
92
|
+
__rpc: name,
|
|
93
|
+
__rpc_id: id,
|
|
94
|
+
data
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async reloadModule(timeout) {
|
|
99
|
+
if (!this._runner?.reloadModule) throw new Error("Active runner does not support reloadModule()");
|
|
100
|
+
return this._runner.reloadModule(timeout);
|
|
101
|
+
}
|
|
54
102
|
async close() {
|
|
55
103
|
this._closed = true;
|
|
56
104
|
this._messageQueue.length = 0;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { IncomingMessage } from "node:http";
|
|
2
|
+
import { Socket } from "node:net";
|
|
3
|
+
|
|
4
|
+
//#region src/types.d.ts
|
|
5
|
+
/** Handler for proxying HTTP requests to the worker. */
|
|
6
|
+
type FetchHandler = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
7
|
+
/** Callback for receiving messages from the worker. */
|
|
8
|
+
type RunnerMessageListener = (data: unknown) => void;
|
|
9
|
+
/** Raw Node.js upgrade request context. */
|
|
10
|
+
interface NodeUpgradeContext {
|
|
11
|
+
req: IncomingMessage;
|
|
12
|
+
socket: Socket;
|
|
13
|
+
head: any;
|
|
14
|
+
}
|
|
15
|
+
/** Context passed to the upgrade handler for WebSocket upgrades. */
|
|
16
|
+
interface UpgradeContext {
|
|
17
|
+
node: NodeUpgradeContext;
|
|
18
|
+
}
|
|
19
|
+
/** Handler for proxying WebSocket upgrade requests to the worker. */
|
|
20
|
+
type UpgradeHandler = (context: UpgradeContext) => void;
|
|
21
|
+
/** Bidirectional RPC messaging interface between the runner and worker. */
|
|
22
|
+
interface RunnerRPCHooks {
|
|
23
|
+
/** Send a message to the worker. */
|
|
24
|
+
sendMessage: (message: unknown) => void;
|
|
25
|
+
/** Register a listener for messages from the worker. */
|
|
26
|
+
onMessage: (listener: RunnerMessageListener) => void;
|
|
27
|
+
/** Remove a previously registered message listener. */
|
|
28
|
+
offMessage: (listener: RunnerMessageListener) => void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Address reported by the worker once it is ready.
|
|
32
|
+
*
|
|
33
|
+
* Either a TCP `host`/`port` pair or a Unix `socketPath`.
|
|
34
|
+
*/
|
|
35
|
+
type WorkerAddress = {
|
|
36
|
+
host?: string;
|
|
37
|
+
port: number;
|
|
38
|
+
socketPath?: undefined;
|
|
39
|
+
} | {
|
|
40
|
+
host?: undefined;
|
|
41
|
+
port?: undefined;
|
|
42
|
+
socketPath: string;
|
|
43
|
+
};
|
|
44
|
+
/** Lifecycle hooks for observing runner state changes. */
|
|
45
|
+
interface WorkerHooks {
|
|
46
|
+
/** Called when the worker closes, optionally with the cause. */
|
|
47
|
+
onClose?: (worker: EnvRunner, cause?: unknown) => void;
|
|
48
|
+
/** Called when the worker is ready and listening at the given address. */
|
|
49
|
+
onReady?: (worker: EnvRunner, address?: WorkerAddress) => void;
|
|
50
|
+
}
|
|
51
|
+
/** Options for the `rpc()` method. */
|
|
52
|
+
interface RPCOptions {
|
|
53
|
+
/** Timeout in milliseconds before the RPC call rejects. Default: 3000. */
|
|
54
|
+
timeout?: number;
|
|
55
|
+
}
|
|
56
|
+
/** Core runner interface combining lifecycle hooks, RPC, and request proxying. */
|
|
57
|
+
interface EnvRunner extends WorkerHooks, RunnerRPCHooks {
|
|
58
|
+
/** Whether the worker is ready to accept requests. */
|
|
59
|
+
readonly ready: boolean;
|
|
60
|
+
/** Whether the runner has been closed. */
|
|
61
|
+
readonly closed: boolean;
|
|
62
|
+
/** Proxy an HTTP request to the worker. */
|
|
63
|
+
fetch: FetchHandler;
|
|
64
|
+
/** Proxy a WebSocket upgrade request to the worker. */
|
|
65
|
+
upgrade?: UpgradeHandler;
|
|
66
|
+
/** Returns a promise that resolves when the runner becomes ready. */
|
|
67
|
+
waitForReady(timeout?: number): Promise<void>;
|
|
68
|
+
/** Send an RPC request and wait for the response. */
|
|
69
|
+
rpc<T = unknown>(name: string, data?: unknown, opts?: RPCOptions): Promise<T>;
|
|
70
|
+
/** Re-import the entry module without restarting the worker/process. */
|
|
71
|
+
reloadModule?(timeout?: number): Promise<void>;
|
|
72
|
+
/** Gracefully shut down the worker. */
|
|
73
|
+
close(): Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { RunnerMessageListener as a, UpgradeHandler as c, RPCOptions as i, WorkerAddress as l, FetchHandler as n, RunnerRPCHooks as o, NodeUpgradeContext as r, UpgradeContext as s, EnvRunner as t, WorkerHooks as u };
|
|
@@ -12,5 +12,15 @@ function parseServerAddress(server) {
|
|
|
12
12
|
port: Number(url.port)
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Re-import the user entry module with cache busting.
|
|
17
|
+
* Tears down old IPC hooks and re-initializes new ones.
|
|
18
|
+
*/
|
|
19
|
+
async function reloadEntryModule(entryPath, currentEntry, sendMessage) {
|
|
20
|
+
await currentEntry.ipc?.onClose?.();
|
|
21
|
+
const newEntry = await resolveEntry(entryPath + "?t=" + Date.now());
|
|
22
|
+
await newEntry.ipc?.onOpen?.({ sendMessage });
|
|
23
|
+
return newEntry;
|
|
24
|
+
}
|
|
15
25
|
//#endregion
|
|
16
|
-
export {
|
|
26
|
+
export { reloadEntryModule as n, resolveEntry as r, parseServerAddress as t };
|