@frockbot/applet-sdk 0.7.178 → 0.7.180

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 CHANGED
@@ -1,113 +1,97 @@
1
1
  # @frockbot/applet-sdk
2
2
 
3
- The SDK a FrockBot Applet is written against: a schema-first Durable Object
4
- server, a TanStack DB client over one real-time socket, a precompiled component
5
- kit on the theme tokens, a linter, and the build pipeline the cloud build
6
- service runs.
7
-
8
- An Applet is authored with the `applet_*` tools, built by `apps/applet-build`,
9
- and mounted as a Durable Object facet from an immutable artifact. There is no
10
- CLI: nothing outside the service builds an Applet, and no Computer is involved
11
- at any point.
3
+ What a FrockBot Plugin is written against, and the build that turns a
4
+ Plugin's source into the module and manifest a publish stores (ADR 0026).
12
5
 
13
6
  ## Entry points
14
7
 
15
- | Import | For |
16
- | ----------------------------------- | -------------------------------------------------------------------- |
17
- | `@frockbot/applet-sdk/server` | `Applet`, `table`, `t` — the Applet's `server.ts` |
18
- | `@frockbot/applet-sdk/client` | `createApplet`, `mount`, `newId` — the Applet's `ui.tsx` |
19
- | `@frockbot/applet-sdk/kit` | the fourteen components (`src/kit/README.md`) |
20
- | `@frockbot/applet-sdk/lint` | the flat ESLint config and the five custom rules |
21
- | `@frockbot/applet-sdk/protocol` | wire protocol v1, for the kernel and for tests |
22
- | `@frockbot/applet-sdk/build` | `runAppletBuildV1` — the five stages, for the service |
23
- | `@frockbot/applet-sdk/plugin` | types only: `PluginModule`, `PluginContext` — a Plugin's `plugin.ts` |
24
- | `@frockbot/applet-sdk/build/plugin` | `runPluginBuildV1` — the four Plugin stages, for the service |
8
+ | Import | For |
9
+ | ----------------------------------- | ------------------------------------------------------------------------------------------ |
10
+ | `@frockbot/applet-sdk/plugin` | types only: `PluginModule`, `PluginContext` and the rest — a Plugin's `plugin.ts` |
11
+ | `@frockbot/applet-sdk/build/plugin` | `runPluginBuildV1` — the four stages, for the build service and the seeded Plugins' script |
12
+
13
+ ## A Plugin
14
+
15
+ A Plugin is a directory: a `plugin.json` descriptor, a `plugin.ts` module,
16
+ and any `.ts` files beside it that the module imports. `plugin.ts` has no
17
+ default export. It exports `tools` and `execute` by name, and may export
18
+ `hooks`, `services`, `triggers`, `views`, `cards` and `modelProviders`
19
+ (`PluginModule`). `tools` may be empty: a Plugin that only serves hooks
20
+ builds.
21
+
22
+ `@frockbot/applet-sdk/plugin` is declarations only, so it is imported with
23
+ `import type`; a value import of it fails the bundle stage.
24
+ `app/plugins/sdk-types.test.ts` pins its `PluginContext`, hook events, grants
25
+ and hook payloads to the kernel's own types, so a Plugin that type-checks
26
+ here sees the `ctx` the kernel builds.
27
+
28
+ A model provider (`PluginModelProvider`, ADR 0032) answers a normalized model
29
+ request with normalized stream events, and makes its one upstream call
30
+ through `ctx.modelTransport`. The deployment serves a provider only from the
31
+ artifact its own provider catalog names, so this is not a way for a
32
+ Bot-written Plugin to reach a provider.
33
+
34
+ `plugin/template/` is the scaffold a new Plugin starts as, with
35
+ `__PLUGIN_ID__` and `__PLUGIN_NAME__` for `plugin_create` to fill in.
36
+ `scripts/build-applets-assets.ts` carries it into the Worker as
37
+ `app/plugins/template.generated.ts`, and carries `plugin/index.d.ts` into the
38
+ Plugins Skill as `app/plugins/skills/plugins/references/types.md`.
25
39
 
26
40
  ## The build
27
41
 
28
- `runAppletBuildV1(directory, { mode })` is five named stages over one
29
- directory: `descriptor`, `typecheck`, `lint`, `bundle`, `describe`. `check`
30
- stops after the linter; `build` goes on to the artifacts. A stage that fails
31
- stops the run and names itself, and every failure is a list of
32
- `{file, line, column, message, severity}`.
33
-
34
- `manifest.json`'s tool declarations are derived by mounting the built
35
- `server.js` in Miniflare and calling `health()` — the same question the kernel
36
- asks the facet before it admits a generation, so the manifest cannot disagree
37
- with the code.
38
-
39
- `template/` is the scaffold a new Applet starts as.
40
- `scripts/build-applets-assets.ts` turns it into `applets/template.generated.ts`,
41
- which `applet_create` writes through the Workspace.
42
-
43
- ## Plugins
44
-
45
- A Plugin (ADR 0026) is written against `@frockbot/applet-sdk/plugin`, which
46
- is declarations only: `plugin.ts` exports `tools` and `execute`, and may
47
- export `hooks`, `services`, `triggers`, `views` and `modelProviders`, beside a `plugin.json` descriptor.
48
- `tools` may be empty — a Plugin that only serves hooks is admissible, because
49
- the kernel's own descriptor contract admits one. A model provider
50
- (`PluginModelProvider`, ADR 0032) answers a normalized model request with
51
- normalized stream events and makes its one upstream call through
52
- `ctx.modelTransport`; the deployment serves it only from the artifact its own
53
- provider catalog names, so this is not a way for a Bot-written Plugin to reach
54
- a provider.
55
- `runPluginBuildV1(directory, { mode, id })` is four stages — `descriptor`,
56
- `typecheck`, `bundle`, `describe` — with no lint stage, because a Plugin's
57
- reach is a grant the descriptor declares and the kernel enforces. The bundle
58
- is one ESM module with every import inlined, and the manifest is read by
59
- running that module in Miniflare with no outbound network. `plugin/template/`
60
- is the scaffold a new Plugin starts as. `PluginContext` is held to the
61
- kernel's own `ctx` keys by `app/plugins/sdk-types.test.ts`.
62
-
63
- ## What runs where
64
-
65
- `server.ts` becomes a single ESM file whose only import is `cloudflare:workers`,
66
- loaded by the kernel's `APPLETS` Worker Loader with no outbound network, and
67
- mounted as a facet under `AppletState`. `ui.tsx` becomes one self-contained HTML
68
- page served from the anonymous artifact origin into a sandboxed iframe, which
69
- receives its theme tokens and a short-lived viewer token through the host's
70
- `init` message and opens exactly one WebSocket back to the facet.
71
-
72
- The Cloudflare programming model is not hidden: an Applet is a Durable Object
73
- with SQLite and hibernating sockets. What the SDK does hide is every binding
74
- name — an author sees `tables`, `tools`, and `this.db`.
75
-
76
- ## Wire protocol
77
-
78
- JSON frames, at most 64 KB each, decoded by `src/protocol/` at both ends;
79
- an unknown type, field, or table fails closed. Two versions are spoken on the
80
- same server, told apart by the socket URL: a page built against v2 opens with
81
- `v=2`, and a page built before it opens with nothing and is spoken to in v1.
82
-
83
- | Direction | Frame | Carries |
84
- | --------------- | ---------- | ------------------------------------------------------------------------------------------- |
85
- | server → client | `hello` | contract, generationId, viewer, tables, revision, cursor — and in v2, the `snapshot` itself |
86
- | client → server | `hello` | contract, optional `since` cursor for catch-up; in v2 only on a resume or when asked |
87
- | server → client | `snapshot` | every row of every table, plus the cursor |
88
- | server → client | `changes` | ordered row changes, optionally tagged with a client txn id |
89
- | client → server | `mutate` | one client transaction: insert/update/delete |
90
- | server → client | `ack` | the resulting rows for that txn |
91
- | server → client | `reject` | why the txn was refused (the client rolls back) |
92
-
93
- The host hands the page its credential in an `init` postMessage, and a fresh
94
- credential later in a `refresh` of the same shape; the page reconnects in
95
- place rather than being reloaded, and with its cursor on the URL that is the
96
- `changes` path.
97
-
98
- A v2 page's first render waits on one frame: the server's `hello` carries the
99
- snapshot when the URL named no `since` cursor, and the page marks its
100
- collections ready on it. A reconnect puts `since` on the URL, gets a plain
101
- `hello`, and asks for `changes` as v1 does. A snapshot that would not fit the
102
- frame is left out of the hello and the v1 exchange follows.
42
+ `runPluginBuildV1(directory, { mode, id })` is four named stages over one
43
+ directory. A stage that fails stops the run and names itself, with a list of
44
+ `{file, line, column, message, severity}` diagnostics. `check` stops after
45
+ the type checker; `build` goes on to the module and its manifest.
46
+
47
+ 1. `descriptor`: `plugin.json` is a JSON object whose `id` matches
48
+ `/^[a-z][a-z0-9-]{0,63}$/` and, when the caller passes `id`, is that id.
49
+ The build reads nothing else from it. The app Worker decodes the full
50
+ descriptor and refuses a publish whose descriptor and manifest disagree
51
+ (`pluginManifestDisagreementV1` in `app/plugins/authoring.ts`).
52
+ 2. `typecheck`: every `.ts` file in the directory, strict, against ES2022
53
+ and the DOM lib for `fetch`, `Request` and `Response`, with
54
+ `@frockbot/applet-sdk/plugin` resolved to `plugin/index.d.ts`. The
55
+ directory must hold a `plugin.ts`. Only errors fail the stage.
56
+ 3. `bundle`: esbuild makes one unminified ESM module with every import
57
+ inlined. Nothing is external, so a specifier the bundler cannot inline
58
+ fails here, not at mount. `module-paths.ts` rewrites esbuild's module-path
59
+ comments relative to the Plugin's directory, so the same source builds to
60
+ the same bytes wherever it is built.
61
+ 4. `describe`: the bundle runs in Miniflare beside a describing Worker, with
62
+ no bindings and no outbound network: every `fetch` is answered with a 403.
63
+ Import-time code runs inside workerd, never in the build's own process.
64
+ What the module exports is the manifest. `tools` must be an array and
65
+ `execute` a function; each tool needs a name matching
66
+ `/^[a-z][a-z0-9_]{0,63}$/` and a description; `hooks`, `triggers` and
67
+ `views` hold functions, `services` any values, each card a `render` and
68
+ each model provider a `stream`. A Plugin declares at most 64 tools, each
69
+ name once.
70
+
71
+ Each describe spawns its own workerd, and `boot.ts` bounds the boot. A
72
+ runtime that is not ready within `BOOT_DEADLINE_MS` (30 seconds), or whose
73
+ spawn fails outright, is a `RuntimeDidNotStart`. The build lets that runtime
74
+ go rather than waiting on it and boots once more; if the second boot does not
75
+ come up either, the stage fails with that as its diagnostic.
76
+
77
+ A build answers the module text and its manifest:
78
+ `{ contract: 1, tools, hooks, services, triggers, views, cards, modelProviders, hashes: { module } }`,
79
+ where `hashes.module` is the SHA-256 of the module.
80
+
81
+ Two callers run it. The build service in `apps/applet-build` runs `check`
82
+ for `plugin_check` and `build` for `plugin_publish`.
83
+ `scripts/build-seeded-plugins.ts` builds each Plugin under
84
+ `app/plugins/seeded/` into the Worker bundle through the same stages.
103
85
 
104
86
  ## Tests
105
87
 
106
88
  ```sh
107
- bun test test spike
89
+ bun run test
108
90
  ```
109
91
 
110
- Pure modules and the client are tested in `bun test`: the store runs against
111
- `bun:sqlite`, and `test/loopback.ts` joins the real protocol server to the real
112
- client transport through a pair of fake sockets. `test/build.test.ts` and
113
- `spike/` run the real pipeline and the built Applet in Miniflare.
92
+ `test/plugin-build.test.ts` runs `runPluginBuildV1` over the real template,
93
+ which `test/plugin-scaffold.ts` fills in and writes to a temporary directory.
94
+ It covers each stage's failure and diagnostics, identical module bytes from
95
+ different and symlinked roots, and the manifest read off each kind of export.
96
+ Every build test boots workerd through Miniflare, so a run needs to bind a
97
+ local port. The root `bun test` runs this file too.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/applet-sdk",
3
- "version": "0.7.178",
3
+ "version": "0.7.180",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Authoring SDK for FrockBot Applets: schema-first Durable Object server, TanStack DB client, component kit, linter, and the build pipeline.",
@@ -1,6 +1,5 @@
1
1
  /**
2
- * The Plugin build (ADR 0026): four named stages over one directory, sharing
3
- * the Applet build's type checker, bundler and Miniflare boot.
2
+ * The Plugin build (ADR 0026): four named stages over one directory.
4
3
  *
5
4
  * 1. `descriptor` — `plugin.json` parses and names the Plugin the caller
6
5
  * asked for. Nothing more is decided here: the app Worker holds the full
@@ -29,11 +28,42 @@ import { convertV4MiniflareOptions, Miniflare } from "miniflare";
29
28
  import ts from "typescript";
30
29
 
31
30
  import type { AppletDiagnostic } from "../lint/index.js";
32
- import { bootedWithin, withOneMoreBoot } from "./boot.js";
33
31
  import { stableModulePaths } from "./module-paths.js";
34
- import { APPLET_COMPATIBILITY_DATE } from "./runtime.js";
35
32
  import { SDK_PLUGIN_TYPES } from "./paths.js";
36
33
 
34
+ /** Pinned with the SDK: the runtime a Plugin build is checked against. */
35
+ const PLUGIN_COMPATIBILITY_DATE = "2026-08-27";
36
+
37
+ /**
38
+ * A workerd boot takes well under a second; this is room for a loaded
39
+ * machine. Without it a runtime that never reported ready would hang the
40
+ * build container's request rather than answer.
41
+ */
42
+ const BOOT_DEADLINE_MS = 30_000;
43
+
44
+ /**
45
+ * Tearing a runtime down takes milliseconds, but Miniflare's dispose first
46
+ * waits out the startup, which can be the very thing that hung.
47
+ */
48
+ const DISPOSE_DEADLINE_MS = 10_000;
49
+
50
+ /** `work`, or an error naming what did not happen in time. */
51
+ async function within<T>(
52
+ work: Promise<T>,
53
+ ms: number,
54
+ what: string,
55
+ ): Promise<T> {
56
+ let timer: ReturnType<typeof setTimeout> | undefined;
57
+ const deadline = new Promise<never>((_, reject) => {
58
+ timer = setTimeout(() => reject(new Error(`${what} within ${ms}ms`)), ms);
59
+ });
60
+ try {
61
+ return await Promise.race([work, deadline]);
62
+ } finally {
63
+ clearTimeout(timer);
64
+ }
65
+ }
66
+
37
67
  export type PluginBuildStage =
38
68
  "descriptor" | "typecheck" | "bundle" | "describe";
39
69
 
@@ -348,17 +378,11 @@ export default {
348
378
  `;
349
379
 
350
380
  /**
351
- * Ask the built module what it exports, by running it. The boot is bounded
352
- * and tried once more (`boot.ts`), so a build answers rather than hanging on
353
- * a runtime that never came up.
381
+ * Ask the built module what it exports, by running it. Both the boot and the
382
+ * teardown are bounded, so a build answers rather than hanging on a runtime
383
+ * that never came up.
354
384
  */
355
- export function describePlugin(
356
- moduleCode: string,
357
- ): Promise<PluginDescriptionV1> {
358
- return withOneMoreBoot(() => describeInWorkerd(moduleCode));
359
- }
360
-
361
- async function describeInWorkerd(
385
+ export async function describePlugin(
362
386
  moduleCode: string,
363
387
  ): Promise<PluginDescriptionV1> {
364
388
  const miniflare = new Miniflare(
@@ -368,7 +392,7 @@ async function describeInWorkerd(
368
392
  { type: "ESModule", path: "/plugin.js", contents: moduleCode },
369
393
  ],
370
394
  modulesRoot: "/",
371
- compatibilityDate: APPLET_COMPATIBILITY_DATE,
395
+ compatibilityDate: PLUGIN_COMPATIBILITY_DATE,
372
396
  // Import-time code runs with no way out: every fetch is answered here.
373
397
  outboundService: async () =>
374
398
  new Response("the build describes a Plugin without a network", {
@@ -378,10 +402,12 @@ async function describeInWorkerd(
378
402
  port: 0,
379
403
  }),
380
404
  );
381
- let started = false;
382
405
  try {
383
- const url = await bootedWithin(miniflare.ready);
384
- started = true;
406
+ const url = await within(
407
+ miniflare.ready,
408
+ BOOT_DEADLINE_MS,
409
+ "The Workers runtime did not start",
410
+ );
385
411
  const response = (await miniflare.dispatchFetch(
386
412
  new URL(`/describe?${randomUUID()}`, url).toString(),
387
413
  )) as unknown as Response;
@@ -393,9 +419,15 @@ async function describeInWorkerd(
393
419
  }
394
420
  return validateDescription(body.description);
395
421
  } finally {
396
- // A runtime that never started is let go of rather than waited on.
397
- if (started) await miniflare.dispose();
398
- else void miniflare.dispose().catch(() => {});
422
+ // Awaited on every path, a boot that missed its deadline included: this
423
+ // is what kills workerd. Miniflare's fallback is a process exit hook,
424
+ // `bun test` runs none, and a dispose still pending when the host exits
425
+ // leaves workerd running.
426
+ await within(
427
+ miniflare.dispose(),
428
+ DISPOSE_DEADLINE_MS,
429
+ "The Workers runtime did not stop",
430
+ );
399
431
  }
400
432
  }
401
433
 
package/src/build/boot.ts DELETED
@@ -1,75 +0,0 @@
1
- // A workerd boot, bounded.
2
- //
3
- // Each build spawns its own workerd through Miniflare, and a spawn
4
- // occasionally never comes up (a bun+workerd spawn race, roughly one boot in
5
- // fifty): it either never reports ready, or it fails outright and leaves the
6
- // runtime's stdio socket with nothing to connect to. A build that awaited
7
- // `ready` unbounded would hang to the test's timeout, or hang the build
8
- // container's request. So a boot is given a deadline, a boot that misses it
9
- // is let go of rather than waited on, both shapes are named
10
- // `RuntimeDidNotStart`, and the caller tries once more before answering with
11
- // that as its diagnostic.
12
-
13
- /** How long a workerd boot is given before the build gives up on it. */
14
- export const BOOT_DEADLINE_MS = 30_000;
15
-
16
- /** A workerd that never came up; the boot, not the code, failed. */
17
- export class RuntimeDidNotStart extends Error {}
18
-
19
- /**
20
- * The same race seen from the other side: the spawn fails outright rather
21
- * than hanging, and the runtime's stdio socket is never there to connect to.
22
- * That is a boot that did not happen, not a fault in the code being built.
23
- */
24
- function spawnFailed(error: unknown): boolean {
25
- const { code, syscall } = (error ?? {}) as {
26
- code?: unknown;
27
- syscall?: unknown;
28
- };
29
- return (
30
- (syscall === "connect" || syscall === "spawn") &&
31
- (code === "ENOENT" || code === "ECONNREFUSED" || code === "EAGAIN")
32
- );
33
- }
34
-
35
- /**
36
- * `ready`, or a `RuntimeDidNotStart` when the runtime never came up — either
37
- * because `ready` did not settle in time or because the spawn failed.
38
- */
39
- export async function bootedWithin<T>(ready: Promise<T>): Promise<T> {
40
- let timer: ReturnType<typeof setTimeout> | undefined;
41
- const deadline = new Promise<never>((_, reject) => {
42
- timer = setTimeout(
43
- () =>
44
- reject(
45
- new RuntimeDidNotStart(
46
- `The Workers runtime did not start within ${BOOT_DEADLINE_MS}ms`,
47
- ),
48
- ),
49
- BOOT_DEADLINE_MS,
50
- );
51
- });
52
- try {
53
- return await Promise.race([ready, deadline]);
54
- } catch (error) {
55
- if (!spawnFailed(error)) throw error;
56
- throw new RuntimeDidNotStart(
57
- `The Workers runtime could not be spawned: ${
58
- error instanceof Error ? error.message : String(error)
59
- }`,
60
- { cause: error },
61
- );
62
- } finally {
63
- clearTimeout(timer);
64
- }
65
- }
66
-
67
- /** Runs `boot` again, once, when the runtime never came up the first time. */
68
- export async function withOneMoreBoot<T>(boot: () => Promise<T>): Promise<T> {
69
- try {
70
- return await boot();
71
- } catch (error) {
72
- if (!(error instanceof RuntimeDidNotStart)) throw error;
73
- return await boot();
74
- }
75
- }
@@ -1,132 +0,0 @@
1
- /**
2
- * The built Applet, running for real, in a Node process.
3
- *
4
- * Miniflare gives the built `dist/server.js` the one thing no fake can: a
5
- * SQLite-backed Durable Object with hibernating WebSockets, which is exactly
6
- * what the loader gives it in production. The build uses it to ask the
7
- * mounted class what
8
- * tools it declares rather than guessing from the source.
9
- */
10
-
11
- import { convertV4MiniflareOptions, Miniflare } from "miniflare";
12
-
13
- import { bootedWithin } from "./boot.js";
14
-
15
- /** Pinned with the SDK: the runtime a Plugin build is checked against. */
16
- export const APPLET_COMPATIBILITY_DATE = "2026-08-27";
17
-
18
- /**
19
- * The dev worker. It exists only to route: the DO class is the Applet's own,
20
- * and everything else here is the two seams the kernel provides in production
21
- * — a viewer token on the socket, and a `CAPABILITIES` binding.
22
- */
23
- const DEV_WORKER = `
24
- export { Applet } from "./server.js";
25
-
26
- export default {
27
- async fetch(request, env) {
28
- const url = new URL(request.url);
29
- const stub = env.APPLET.get(env.APPLET.idFromName(env.APPLET_ID));
30
-
31
- if (url.pathname === "/socket") {
32
- if (url.searchParams.get("token") !== env.APPLET_TOKEN) {
33
- return new Response("Forbidden", { status: 403 });
34
- }
35
- url.searchParams.set("viewer", url.searchParams.get("viewer") ?? "dev-viewer");
36
- return stub.fetch(new Request(url, request));
37
- }
38
-
39
- if (url.pathname === "/health") {
40
- return Response.json(await stub.health());
41
- }
42
-
43
- if (url.pathname === "/describe") {
44
- return Response.json(await stub.describe());
45
- }
46
-
47
- if (url.pathname === "/tool" && request.method === "POST") {
48
- const body = await request.json();
49
- try {
50
- return Response.json({ ok: true, result: await stub.invokeTool(body.name, body.input) });
51
- } catch (error) {
52
- return Response.json({ ok: false, error: String(error && error.message || error) });
53
- }
54
- }
55
-
56
- if (url.pathname === "/" || url.pathname === "/index.html") {
57
- return new Response(env.APPLET_UI, {
58
- headers: { "content-type": "text/html; charset=utf-8" },
59
- });
60
- }
61
- return new Response("Not found", { status: 404 });
62
- },
63
- };
64
- `;
65
-
66
- export interface AppletRuntimeOptions {
67
- /** Contents of `dist/server.js`: one ESM file importing only cloudflare:workers. */
68
- serverCode: string;
69
- /** Contents of `dist/ui.html`; omitted when only `health()` is wanted. */
70
- html?: string;
71
- appletId: string;
72
- /** The dev viewer token the socket demands. */
73
- token: string;
74
- /** 0 picks a free port. */
75
- port?: number;
76
- }
77
-
78
- export interface AppletRuntime {
79
- url: URL;
80
- fetch(path: string, init?: RequestInit): Promise<Response>;
81
- dispose(): Promise<void>;
82
- }
83
-
84
- export async function startAppletRuntime(
85
- options: AppletRuntimeOptions,
86
- ): Promise<AppletRuntime> {
87
- // Miniflare 5's own option shape is the wrangler config (`workers[].config`).
88
- // `convertV4MiniflareOptions` is the supported way to keep the flat v4 shape,
89
- // which is the one the Workers docs and the rest of this repo speak.
90
- const miniflare = new Miniflare(
91
- convertV4MiniflareOptions({
92
- modules: [
93
- { type: "ESModule", path: "/index.mjs", contents: DEV_WORKER },
94
- { type: "ESModule", path: "/server.js", contents: options.serverCode },
95
- ],
96
- modulesRoot: "/",
97
- compatibilityDate: APPLET_COMPATIBILITY_DATE,
98
- compatibilityFlags: ["nodejs_compat"],
99
- durableObjects: { APPLET: { className: "Applet", useSQLite: true } },
100
- serviceBindings: {
101
- // The lease-backed proxy is a later slice; models are unavailable.
102
- CAPABILITIES: async () =>
103
- Response.json({ status: "unavailable", reason: "dev" }),
104
- },
105
- bindings: {
106
- APPLET_ID: options.appletId,
107
- APPLET_TOKEN: options.token,
108
- APPLET_UI: options.html ?? "",
109
- },
110
- host: "127.0.0.1",
111
- port: options.port ?? 0,
112
- }),
113
- );
114
-
115
- let url: URL;
116
- try {
117
- url = await bootedWithin(miniflare.ready);
118
- } catch (error) {
119
- // A runtime that never started is let go of rather than waited on.
120
- void miniflare.dispose().catch(() => {});
121
- throw error;
122
- }
123
- return {
124
- url,
125
- fetch: (path, init) =>
126
- miniflare.dispatchFetch(
127
- new URL(path, url).toString(),
128
- init as never,
129
- ) as unknown as Promise<Response>,
130
- dispose: () => miniflare.dispose(),
131
- };
132
- }