Skip to content

Commit 931f467

Browse files
authored
feat: hello world binding (#9475)
* Hello World Binding: Local binding implementation * Hello World Binding: Miniflare Plugin Implementation * Hello World Binding: Expose your binding in Node.js * Hello World Binding: Update wrangler config * Hello World Binding: Wire new binding config to Miniflare and deploy config * Hello World Binding: Update wrangler type generation * Hello World Binding: Add wrangler local commands * add changeset
1 parent 0b2ba45 commit 931f467

File tree

27 files changed

+771
-2
lines changed

27 files changed

+771
-2
lines changed

.changeset/dirty-dryers-tie.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"miniflare": patch
3+
"wrangler": patch
4+
---
5+
6+
add hello world binding that serves as as an explanatory example.

packages/miniflare/src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
DurableObjectClassNames,
4646
getDirectSocketName,
4747
getGlobalServices,
48+
HELLO_WORLD_PLUGIN_NAME,
4849
HOST_CAPNP_CONNECT,
4950
KV_PLUGIN_NAME,
5051
normaliseDurableObject,
@@ -2620,6 +2621,15 @@ export class Miniflare {
26202621
): Promise<ReplaceWorkersTypes<R2Bucket>> {
26212622
return this.#getProxy(R2_PLUGIN_NAME, bindingName, workerName);
26222623
}
2624+
getHelloWorldBinding(
2625+
bindingName: string,
2626+
workerName?: string
2627+
): Promise<{
2628+
get: () => Promise<{ value: string; ms?: number }>;
2629+
set: (value: string) => Promise<void>;
2630+
}> {
2631+
return this.#getProxy(HELLO_WORLD_PLUGIN_NAME, bindingName, workerName);
2632+
}
26232633

26242634
/** @internal */
26252635
_getInternalDurableObjectNamespace(
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import fs from "fs/promises";
2+
import BINDING_SCRIPT from "worker:hello-world/binding";
3+
import OBJECT_SCRIPT from "worker:hello-world/object";
4+
import { z } from "zod";
5+
import { Service, Worker_Binding } from "../../runtime";
6+
import { SharedBindings } from "../../workers";
7+
import {
8+
getMiniflareObjectBindings,
9+
getPersistPath,
10+
PersistenceSchema,
11+
Plugin,
12+
ProxyNodeBinding,
13+
SERVICE_LOOPBACK,
14+
} from "../shared";
15+
16+
export const HELLO_WORLD_PLUGIN_NAME = "hello-world";
17+
18+
export const HelloWorldOptionsSchema = z.object({
19+
helloWorld: z
20+
.record(
21+
z.object({
22+
enable_timer: z.boolean().optional(),
23+
})
24+
)
25+
.optional(),
26+
});
27+
28+
export const HelloWorldSharedOptionsSchema = z.object({
29+
helloWorldPersist: PersistenceSchema,
30+
});
31+
32+
export const HELLO_WORLD_PLUGIN: Plugin<
33+
typeof HelloWorldOptionsSchema,
34+
typeof HelloWorldSharedOptionsSchema
35+
> = {
36+
options: HelloWorldOptionsSchema,
37+
sharedOptions: HelloWorldSharedOptionsSchema,
38+
async getBindings(options) {
39+
if (!options.helloWorld) {
40+
return [];
41+
}
42+
43+
const bindings = Object.entries(options.helloWorld).map<Worker_Binding>(
44+
([name, config]) => {
45+
return {
46+
name,
47+
service: {
48+
name: `${HELLO_WORLD_PLUGIN_NAME}:${JSON.stringify(config.enable_timer ?? false)}`,
49+
entrypoint: "HelloWorldBinding",
50+
},
51+
};
52+
}
53+
);
54+
return bindings;
55+
},
56+
getNodeBindings(options: z.infer<typeof HelloWorldOptionsSchema>) {
57+
if (!options.helloWorld) {
58+
return {};
59+
}
60+
return Object.fromEntries(
61+
Object.keys(options.helloWorld).map((name) => [
62+
name,
63+
new ProxyNodeBinding(),
64+
])
65+
);
66+
},
67+
async getServices({
68+
options,
69+
sharedOptions,
70+
tmpPath,
71+
defaultPersistRoot,
72+
unsafeStickyBlobs,
73+
}) {
74+
const configs = options.helloWorld ? Object.values(options.helloWorld) : [];
75+
76+
if (configs.length === 0) {
77+
return [];
78+
}
79+
80+
const persistPath = getPersistPath(
81+
HELLO_WORLD_PLUGIN_NAME,
82+
tmpPath,
83+
defaultPersistRoot,
84+
sharedOptions.helloWorldPersist
85+
);
86+
87+
await fs.mkdir(persistPath, { recursive: true });
88+
89+
const storageService = {
90+
name: `${HELLO_WORLD_PLUGIN_NAME}:storage`,
91+
disk: { path: persistPath, writable: true },
92+
} satisfies Service;
93+
const objectService = {
94+
name: `${HELLO_WORLD_PLUGIN_NAME}:object`,
95+
worker: {
96+
compatibilityDate: "2025-01-01",
97+
modules: [
98+
{
99+
name: "object.worker.js",
100+
esModule: OBJECT_SCRIPT(),
101+
},
102+
],
103+
durableObjectNamespaces: [
104+
{
105+
className: "HelloWorldObject",
106+
uniqueKey: `miniflare-hello-world-HelloWorldObject`,
107+
},
108+
],
109+
// Store Durable Object SQL databases in persist path
110+
durableObjectStorage: { localDisk: storageService.name },
111+
// Bind blob disk directory service to object
112+
bindings: [
113+
{
114+
name: SharedBindings.MAYBE_SERVICE_BLOBS,
115+
service: { name: storageService.name },
116+
},
117+
{
118+
name: SharedBindings.MAYBE_SERVICE_LOOPBACK,
119+
service: { name: SERVICE_LOOPBACK },
120+
},
121+
...getMiniflareObjectBindings(unsafeStickyBlobs),
122+
],
123+
},
124+
} satisfies Service;
125+
const services = configs.map<Service>((config) => ({
126+
name: `${HELLO_WORLD_PLUGIN_NAME}:${JSON.stringify(config.enable_timer ?? false)}`,
127+
worker: {
128+
compatibilityDate: "2025-01-01",
129+
modules: [
130+
{
131+
name: "binding.worker.js",
132+
esModule: BINDING_SCRIPT(),
133+
},
134+
],
135+
bindings: [
136+
{
137+
name: "config",
138+
json: JSON.stringify(config),
139+
},
140+
{
141+
name: "store",
142+
durableObjectNamespace: {
143+
className: "HelloWorldObject",
144+
serviceName: objectService.name,
145+
},
146+
},
147+
],
148+
},
149+
}));
150+
151+
return [...services, storageService, objectService];
152+
},
153+
getPersistPath(sharedOptions, tmpPath) {
154+
return getPersistPath(
155+
HELLO_WORLD_PLUGIN_NAME,
156+
tmpPath,
157+
undefined,
158+
sharedOptions.helloWorldPersist
159+
);
160+
},
161+
};

packages/miniflare/src/plugins/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from "./dispatch-namespace";
2222
import { DURABLE_OBJECTS_PLUGIN, DURABLE_OBJECTS_PLUGIN_NAME } from "./do";
2323
import { EMAIL_PLUGIN, EMAIL_PLUGIN_NAME } from "./email";
24+
import { HELLO_WORLD_PLUGIN, HELLO_WORLD_PLUGIN_NAME } from "./hello-world";
2425
import { HYPERDRIVE_PLUGIN, HYPERDRIVE_PLUGIN_NAME } from "./hyperdrive";
2526
import { IMAGES_PLUGIN, IMAGES_PLUGIN_NAME } from "./images";
2627
import { KV_PLUGIN, KV_PLUGIN_NAME } from "./kv";
@@ -56,6 +57,7 @@ export const PLUGINS = {
5657
[VECTORIZE_PLUGIN_NAME]: VECTORIZE_PLUGIN,
5758
[CONTAINER_PLUGIN_NAME]: CONTAINER_PLUGIN,
5859
[MTLS_PLUGIN_NAME]: MTLS_PLUGIN,
60+
[HELLO_WORLD_PLUGIN_NAME]: HELLO_WORLD_PLUGIN,
5961
};
6062
export type Plugins = typeof PLUGINS;
6163

@@ -115,7 +117,8 @@ export type WorkerOptions = z.input<typeof CORE_PLUGIN.options> &
115117
z.input<typeof IMAGES_PLUGIN.options> &
116118
z.input<typeof VECTORIZE_PLUGIN.options> &
117119
z.input<typeof CONTAINER_PLUGIN.options> &
118-
z.input<typeof MTLS_PLUGIN.options>;
120+
z.input<typeof MTLS_PLUGIN.options> &
121+
z.input<typeof HELLO_WORLD_PLUGIN.options>;
119122

120123
export type SharedOptions = z.input<typeof CORE_PLUGIN.sharedOptions> &
121124
z.input<typeof CACHE_PLUGIN.sharedOptions> &
@@ -126,7 +129,8 @@ export type SharedOptions = z.input<typeof CORE_PLUGIN.sharedOptions> &
126129
z.input<typeof WORKFLOWS_PLUGIN.sharedOptions> &
127130
z.input<typeof SECRET_STORE_PLUGIN.sharedOptions> &
128131
z.input<typeof ANALYTICS_ENGINE_PLUGIN.sharedOptions> &
129-
z.input<typeof CONTAINER_PLUGIN.sharedOptions>;
132+
z.input<typeof CONTAINER_PLUGIN.sharedOptions> &
133+
z.input<typeof HELLO_WORLD_PLUGIN.sharedOptions>;
130134

131135
export const PLUGIN_ENTRIES = Object.entries(PLUGINS) as [
132136
keyof Plugins,
@@ -187,3 +191,4 @@ export * from "./vectorize";
187191
export * from "./containers";
188192
export { ContainerController } from "./containers/service";
189193
export * from "./mtls";
194+
export * from "./hello-world";
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Emulated Hello World Binding
2+
3+
import { WorkerEntrypoint } from "cloudflare:workers";
4+
import type { HelloWorldObject } from "./object.worker";
5+
6+
// ENV configuration
7+
interface Env {
8+
config: { enable_timer?: boolean };
9+
store: DurableObjectNamespace<HelloWorldObject>;
10+
}
11+
12+
export class HelloWorldBinding extends WorkerEntrypoint<Env> {
13+
async get(): Promise<{ value: string; ms?: number }> {
14+
const objectNamespace = this.env.store;
15+
const namespaceId = JSON.stringify(this.env.config);
16+
const id = objectNamespace.idFromName(namespaceId);
17+
const stub = objectNamespace.get(id);
18+
const value = await stub.get();
19+
return {
20+
value: value ?? "",
21+
ms: this.env.config.enable_timer ? 100 : undefined,
22+
};
23+
}
24+
25+
async set(value: string): Promise<void> {
26+
const objectNamespace = this.env.store;
27+
const namespaceId = JSON.stringify(this.env.config);
28+
const id = objectNamespace.idFromName(namespaceId);
29+
const stub = objectNamespace.get(id);
30+
await stub.set(value);
31+
}
32+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { DurableObject } from "cloudflare:workers";
2+
3+
export class HelloWorldObject extends DurableObject {
4+
async get() {
5+
return await this.ctx.storage.get<string>("value");
6+
}
7+
8+
async set(value: string) {
9+
await this.ctx.storage.put<string>("value", value);
10+
}
11+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import test from "ava";
2+
import { Miniflare } from "miniflare";
3+
4+
test("hello-world", async (t) => {
5+
const mf = new Miniflare({
6+
verbose: true,
7+
compatibilityDate: "2025-01-01",
8+
helloWorld: {
9+
BINDING: {
10+
enable_timer: true,
11+
},
12+
},
13+
helloWorldPersist: false,
14+
modules: true,
15+
script: `
16+
export default {
17+
async fetch(request, env, ctx) {
18+
if (request.method === "POST") {
19+
await env.BINDING.set(await request.text());
20+
}
21+
const result = await env.BINDING.get();
22+
if (!result.value) {
23+
return new Response('Not found', { status: 404 });
24+
}
25+
return Response.json(result);
26+
},
27+
}
28+
`,
29+
});
30+
t.teardown(() => mf.dispose());
31+
32+
const response1 = await mf.dispatchFetch("http://placeholder");
33+
34+
t.is(await response1.text(), "Not found");
35+
t.is(response1.status, 404);
36+
37+
const response2 = await mf.dispatchFetch("http://placeholder", {
38+
method: "POST",
39+
body: "hello world",
40+
});
41+
42+
t.deepEqual(await response2.json(), { value: "hello world", ms: 100 });
43+
t.is(response2.status, 200);
44+
45+
const response3 = await mf.dispatchFetch("http://placeholder");
46+
47+
t.deepEqual(await response3.json(), { value: "hello world", ms: 100 });
48+
t.is(response3.status, 200);
49+
50+
const response4 = await mf.dispatchFetch("http://placeholder", {
51+
method: "POST",
52+
body: "",
53+
});
54+
55+
t.is(await response4.text(), "Not found");
56+
t.is(response4.status, 404);
57+
});

0 commit comments

Comments
 (0)