Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 3ee4d22

Browse files
committed
Add core plugin
1 parent 3e96793 commit 3ee4d22

File tree

1 file changed

+204
-0
lines changed

1 file changed

+204
-0
lines changed
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import fs from "fs/promises";
2+
import { Request, Response } from "undici";
3+
import { z } from "zod";
4+
import { Awaitable, JsonSchema } from "../../helpers";
5+
import { Service, Worker, Worker_Binding } from "../../runtime";
6+
import { BINDING_SERVICE_LOOPBACK, Plugin } from "../shared";
7+
8+
// (request: Request) => Awaitable<Response>
9+
export const ServiceFetch = z
10+
.function()
11+
.args(z.instanceof(Request))
12+
.returns(z.instanceof(Response).or(z.promise(z.instanceof(Response))));
13+
14+
export const CoreOptionsSchema = z.object({
15+
name: z.string().optional(),
16+
script: z.string().optional(),
17+
scriptPath: z.string().optional(),
18+
modules: z.boolean().optional(),
19+
compatibilityDate: z.string().optional(),
20+
compatibilityFlags: z.string().array().optional(),
21+
22+
bindings: z.record(JsonSchema).optional(),
23+
wasmBindings: z.record(z.string()).optional(),
24+
textBlobBindings: z.record(z.string()).optional(),
25+
dataBlobBindings: z.record(z.string()).optional(),
26+
serviceBindings: z.record(z.union([z.string(), ServiceFetch])).optional(),
27+
});
28+
export const CoreSharedOptionsSchema = z.object({
29+
host: z.string().optional(),
30+
port: z.number().optional(),
31+
});
32+
33+
export const CORE_PLUGIN_NAME = "core";
34+
35+
// Service looping back to Miniflare's Node.js process (for storage, etc)
36+
export const SERVICE_LOOPBACK = `${CORE_PLUGIN_NAME}:loopback`;
37+
// Service for HTTP socket entrypoint (for checking runtime ready, routing, etc)
38+
export const SERVICE_ENTRY = `${CORE_PLUGIN_NAME}:entry`;
39+
// Service prefix for all regular user workers
40+
const SERVICE_USER_PREFIX = `${CORE_PLUGIN_NAME}:user`;
41+
// Service prefix for custom fetch functions defined in `serviceBindings` option
42+
const SERVICE_CUSTOM_PREFIX = `${CORE_PLUGIN_NAME}:custom`;
43+
44+
export const HEADER_PROBE = "MF-Probe";
45+
export const HEADER_CUSTOM_SERVICE = "MF-Custom-Service";
46+
47+
const BINDING_JSON_VERSION = "MINIFLARE_VERSION";
48+
const BINDING_SERVICE_USER = "MINIFLARE_USER";
49+
const BINDING_TEXT_CUSTOM_SERVICE = "MINIFLARE_CUSTOM_SERVICE";
50+
51+
// TODO: is there a way of capturing the full stack trace somehow?
52+
// Using `>=` for version check to handle multiple `setOptions` calls before
53+
// reload complete.
54+
export const SCRIPT_ENTRY = `addEventListener("fetch", (event) => {
55+
const probe = event.request.headers.get("${HEADER_PROBE}");
56+
if (probe !== null) {
57+
const probeMin = parseInt(probe);
58+
const status = ${BINDING_JSON_VERSION} >= probeMin ? 204 : 412;
59+
return event.respondWith(new Response(null, { status }));
60+
}
61+
62+
if (globalThis.${BINDING_SERVICE_USER} !== undefined) {
63+
event.respondWith(${BINDING_SERVICE_USER}.fetch(event.request).catch((err) => new Response(err.stack)));
64+
} else {
65+
event.respondWith(new Response("No script! 😠", { status: 404 }));
66+
}
67+
});`;
68+
export const SCRIPT_CUSTOM_SERVICE = `addEventListener("fetch", (event) => {
69+
const request = new Request(event.request);
70+
request.headers.set("${HEADER_CUSTOM_SERVICE}", ${BINDING_TEXT_CUSTOM_SERVICE});
71+
event.respondWith(${BINDING_SERVICE_LOOPBACK}.fetch(request));
72+
})`;
73+
74+
export const CORE_PLUGIN: Plugin<
75+
typeof CoreOptionsSchema,
76+
typeof CoreSharedOptionsSchema
77+
> = {
78+
options: CoreOptionsSchema,
79+
sharedOptions: CoreSharedOptionsSchema,
80+
getBindings(options) {
81+
const bindings: Awaitable<Worker_Binding>[] = [];
82+
83+
if (options.bindings !== undefined) {
84+
bindings.push(
85+
...Object.entries(options.bindings).map(([name, value]) => ({
86+
name,
87+
json: JSON.stringify(value),
88+
}))
89+
);
90+
}
91+
if (options.wasmBindings !== undefined) {
92+
bindings.push(
93+
...Object.entries(options.wasmBindings).map(([name, path]) =>
94+
fs.readFile(path).then((wasmModule) => ({ name, wasmModule }))
95+
)
96+
);
97+
}
98+
if (options.textBlobBindings !== undefined) {
99+
bindings.push(
100+
...Object.entries(options.textBlobBindings).map(([name, path]) =>
101+
fs.readFile(path, "utf8").then((text) => ({ name, text }))
102+
)
103+
);
104+
}
105+
if (options.dataBlobBindings !== undefined) {
106+
bindings.push(
107+
...Object.entries(options.dataBlobBindings).map(([name, path]) =>
108+
fs.readFile(path).then((data) => ({ name, data }))
109+
)
110+
);
111+
}
112+
if (options.serviceBindings !== undefined) {
113+
bindings.push(
114+
...Object.entries(options.serviceBindings).map(([name, service]) => ({
115+
name,
116+
service: {
117+
name:
118+
typeof service === "function"
119+
? `${SERVICE_CUSTOM_PREFIX}:${name}` // Custom `fetch` function
120+
: `${SERVICE_USER_PREFIX}:${name}`, // Regular user worker
121+
},
122+
}))
123+
);
124+
}
125+
126+
return Promise.all(bindings);
127+
},
128+
async getServices({ options, optionsVersion, workerBindings, workerIndex }) {
129+
// Define core/shared services.
130+
// Services get de-duped by name, so only the first worker's
131+
// SERVICE_LOOPBACK and SERVICE_ENTRY will be used
132+
const serviceEntryBindings: Worker_Binding[] = [
133+
{ name: BINDING_JSON_VERSION, json: optionsVersion.toString() },
134+
];
135+
const services: Service[] = [
136+
{ name: SERVICE_LOOPBACK, external: { http: {} } },
137+
{
138+
name: SERVICE_ENTRY,
139+
worker: {
140+
serviceWorkerScript: SCRIPT_ENTRY,
141+
bindings: serviceEntryBindings,
142+
},
143+
},
144+
];
145+
146+
// Define regular user worker if script is set
147+
let workerScript: Partial<Worker> | undefined;
148+
if (options.script !== undefined) {
149+
workerScript = options.modules
150+
? { modules: [{ name: "<script>", esModule: options.script }] }
151+
: { serviceWorkerScript: options.script };
152+
} else if (options.scriptPath !== undefined) {
153+
if (options.modules) {
154+
// TODO: collect modules
155+
} else {
156+
const script = await fs.readFile(options.scriptPath, "utf8");
157+
workerScript = { serviceWorkerScript: script };
158+
}
159+
}
160+
161+
if (workerScript !== undefined) {
162+
const name = `${SERVICE_USER_PREFIX}:${options.name ?? ""}`;
163+
services.push({
164+
name,
165+
worker: {
166+
...workerScript,
167+
compatibilityDate: options.compatibilityDate,
168+
compatibilityFlags: options.compatibilityFlags,
169+
bindings: workerBindings,
170+
},
171+
});
172+
serviceEntryBindings.push({
173+
name: BINDING_SERVICE_USER,
174+
service: { name },
175+
});
176+
}
177+
178+
// Define custom `fetch` services if set
179+
if (options.serviceBindings !== undefined) {
180+
for (const [name, service] of Object.entries(options.serviceBindings)) {
181+
if (typeof service === "function") {
182+
services.push({
183+
name: `${SERVICE_CUSTOM_PREFIX}:${name}`,
184+
worker: {
185+
serviceWorkerScript: SCRIPT_CUSTOM_SERVICE,
186+
bindings: [
187+
{
188+
name: BINDING_TEXT_CUSTOM_SERVICE,
189+
text: `${workerIndex}/${name}`,
190+
},
191+
{
192+
name: BINDING_SERVICE_LOOPBACK,
193+
service: { name: SERVICE_LOOPBACK },
194+
},
195+
],
196+
},
197+
});
198+
}
199+
}
200+
}
201+
202+
return services;
203+
},
204+
};

0 commit comments

Comments
 (0)