docs: added plugins docs, updated cloudflare docs

This commit is contained in:
dswbx
2025-08-29 08:35:56 +02:00
parent 9436b8bac5
commit 1fdfcf02b8
7 changed files with 341 additions and 79 deletions
+22 -1
View File
@@ -23,13 +23,34 @@ import type { IEmailDriver, ICacheDriver } from "core/drivers";
import { Api, type ApiOptions } from "Api";
export type AppPluginConfig = {
/**
* The name of the plugin.
*/
name: string;
/**
* The schema of the plugin.
*/
schema?: () => MaybePromise<ReturnType<typeof prototypeEm> | void>;
/**
* Called before the app is built.
*/
beforeBuild?: () => MaybePromise<void>;
/**
* Called after the app is built.
*/
onBuilt?: () => MaybePromise<void>;
/**
* Called when the server is initialized.
*/
onServerInit?: (server: Hono<ServerEnv>) => MaybePromise<void>;
onFirstBoot?: () => MaybePromise<void>;
/**
* Called when the app is booted.
*/
onBoot?: () => MaybePromise<void>;
/**
* Called when the app is first booted.
*/
onFirstBoot?: () => MaybePromise<void>;
};
export type AppPlugin = (app: App) => AppPluginConfig;
+6 -2
View File
@@ -11,7 +11,7 @@ type BunEnv = Bun.Env;
export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOptions, "fetch">;
export async function createApp<Env = BunEnv>(
{ distPath, ...config }: BunBkndConfig<Env> = {},
{ distPath, serveStatic: _serveStatic, ...config }: BunBkndConfig<Env> = {},
args: Env = {} as Env,
opts?: RuntimeOptions,
) {
@@ -20,7 +20,11 @@ export async function createApp<Env = BunEnv>(
return await createRuntimeApp(
{
serveStatic: serveStatic({ root }),
serveStatic:
_serveStatic ??
serveStatic({
root,
}),
...config,
},
args ?? (process.env as Env),
+1 -1
View File
@@ -6,7 +6,7 @@ import { resolve } from "node:path";
* Vite plugin that provides Node.js filesystem access during development
* by injecting a polyfill into the SSR environment
*/
export function devFsPlugin({
export function devFsVitePlugin({
verbose = false,
configFile = "bknd.config.ts",
}: {
@@ -19,22 +19,41 @@ const schema = s.partialObject({
type ImageOptimizationSchema = s.Static<typeof schema>;
export type CloudflareImageOptimizationOptions = {
/**
* The url to access the image optimization plugin, defaults to `/api/plugin/image/optimize`
*/
accessUrl?: string;
/**
* The path to resolve the image from, defaults to `/api/media/file`
*/
resolvePath?: string;
/**
* Whether to explain the image optimization schema, defaults to `false`
*/
explain?: boolean;
/**
* The default options to use, defaults to `{}`
* @param: config
*/
defaultOptions?: ImageOptimizationSchema;
/**
* The fixed options to use, defaults to `{}`
*/
fixedOptions?: ImageOptimizationSchema;
/**
* The cache control to use, defaults to `public, max-age=31536000, immutable`
*/
cacheControl?: string;
};
export function cloudflareImageOptimization({
accessUrl = "/_plugin/image/optimize",
accessUrl = "/api/plugin/image/optimize",
resolvePath = "/api/media/file",
explain = false,
defaultOptions = {},
fixedOptions = {},
}: CloudflareImageOptimizationOptions = {}): AppPlugin {
const disallowedAccessUrls = ["/api", "/admin", "/_optimize"];
const disallowedAccessUrls = ["/api", "/admin", "/api/plugin"];
if (disallowedAccessUrls.includes(accessUrl) || accessUrl.length < 2) {
throw new Error(`Disallowed accessUrl: ${accessUrl}`);
}
@@ -0,0 +1,209 @@
---
title: Plugins
tags: ["documentation"]
---
import { TypeTable } from 'fumadocs-ui/components/type-table';
bknd allows you to extend its functionality by creating plugins. These allows to hook into the app lifecycle and to provide a data structure that is guaranteed to be merged. A plugin is a function that takes in an instance of `App` and returns the following structure:
<AutoTypeTable path="../app/src/App.ts" name="AppPluginConfig" />
## Creating a simple plugin
To create a simple plugin which guarantees an entity `pages` to be available and an additioanl endpoint to render a html list of pages, you can create it as follows:
```tsx title="myPagesPlugin.tsx"
/** @jsxImportSource hono/jsx */
import { type App, type AppPlugin, em, entity, text } from "bknd";
export const myPagesPlugin: AppPlugin = (app) => ({
name: "my-pages-plugin",
// define the schema of the plugin
// this will always be merged into the app's schema
schema: () => em({
pages: entity("pages", {
title: text(),
content: text(),
}),
}),
// execute code after the app is built
onBuilt: () => {
// register a new endpoint, make sure that you choose an endpoint that is reachable for bknd
app.server.get("/my-pages", async (c) => {
const { data: pages } = await app.em.repo("pages").findMany({});
return c.html(
<body>
<h1>Pages: {pages.length}</h1>
<ul>
{pages.map((page: any) => (
<li key={page.id}>{page.title}</li>
))}
</ul>
</body>,
);
});
},
});
```
And then register it in your `bknd.config.ts` file:
```typescript
import type { BkndConfig } from "bknd/adapter";
import { myPagesPlugin } from "./myPagesPlugin";
export default {
options: {
plugins: [myPagesPlugin],
}
} satisfies BkndConfig;
```
The schema returned from the plugin will be merged into the schema of the app.
## Built-in plugins
bknd comes with a few built-in plugins that you can use.
### `syncTypes`
A simple plugin that writes down the TypeScript types of the data schema on boot and each build. The output is equivalent to running `npx bknd types`.
```typescript title="bknd.config.ts"
import { syncTypes } from "bknd/plugins";
import { writeFile } from "node:fs/promises";
export default {
options: {
plugins: [
syncTypes({
// whether to enable the plugin, make sure to disable in production
enabled: true,
// your writing function (required)
write: async (et) => {
await writeFile("bknd-types.d.ts", et.toString(), "utf-8");
}
}),
]
},
} satisfies BkndConfig;
```
### `syncConfig`
A simple plugin that writes down the app configuration on boot and each build.
```typescript title="bknd.config.ts"
import { syncConfig } from "bknd/plugins";
import { writeFile } from "node:fs/promises";
export default {
options: {
plugins: [
syncConfig({
// whether to enable the plugin, make sure to disable in production
enabled: true,
// your writing function (required)
write: async (config) => {
await writeFile("config.json", JSON.stringify(config, null, 2), "utf-8");
},
}),
]
},
} satisfies BkndConfig;
```
### `showRoutes`
A simple plugin that logs the routes of your app in the console.
```typescript title="bknd.config.ts"
import { showRoutes } from "bknd/plugins";
export default {
options: {
plugins: [
showRoutes({
// whether to show the routes only once (on first build)
once: true
})
],
},
} satisfies BkndConfig;
```
### `cloudflareImageOptimization`
A plugin that add Cloudflare Image Optimization to your app's media storage.
```typescript title="bknd.config.ts"
import { cloudflareImageOptimization } from "bknd/plugins";
export default {
options: {
plugins: [
cloudflareImageOptimization({
// the url to access the image optimization plugin
accessUrl: "/api/plugin/image/optimize",
// the path to resolve the image from, defaults to `/api/media/file`
resolvePath: "/api/media/file",
// for example, you may want to have default option to limit to a width of 1000px
defaultOptions: {
width: 1000,
}
})
],
},
} satisfies BkndConfig;
```
Here is a break down of all configuration options:
<AutoTypeTable path="../app/src/plugins/cloudflare/image-optimization.plugin.ts" name="CloudflareImageOptimizationOptions" />
When enabled, you can now access your images at your configured `accessUrl`. For example, if you have a media file at `/api/media/file/image.jpg`, you can access the optimized image at `/api/plugin/image/optimize/image.jpg` for optimization.
Now you can add query parameters for the transformations, e.g. `?width=1000&height=1000`.
<TypeTable
type={{
dpr: {
description:
'The device pixel ratio to use',
type: 'number',
default: 1,
},
fit: {
description: 'The fit mode to use',
type: '"scale-down" | "contain" | "cover" | "crop" | "pad"',
},
format: {
description: 'The format to use',
type: '"auto" | "avif" | "webp" | "jpeg" | "baseline-jpeg" | "json"'
},
height: {
description: 'The height to use',
type: 'number',
},
width: {
description: 'The width to use',
type: 'number',
},
metadata: {
description: 'The metadata to use',
type: '"copyright" | "keep" | "none"'
},
quality: {
description: 'The quality to use',
type: 'number'
},
}}
/>
@@ -123,9 +123,8 @@ import { serve } from "bknd/adapter/cloudflare";
export default serve<Env>({
// ...
onBuilt: async (app) => {
// [!code highlight]
app.server.get("/hello", (c) => c.json({ hello: "world" })); // [!code highlight]
}, // [!code highlight]
},
});
```
@@ -141,7 +140,6 @@ With the Cloudflare Workers adapter, you're being offered to 4 modes to choose f
| `fresh` | On every request, the configuration gets refetched, app built and then served. | Ideal if you don't want to deal with eviction, KV or Durable Objects. |
| `warm` | It tries to keep the built app in memory for as long as possible, and rebuilds if evicted. | Better response times, should be the default choice. |
| `cache` | The configuration is fetched from KV to reduce the initial roundtrip to the database. | Generally faster response times with irregular access patterns. |
| `durable` | The bknd app is ran inside a Durable Object and can be configured to stay alive. | Slowest boot time, but fastest responses. Can be kept alive for as long as you want, giving similar response times as server instances. |
### Modes: `fresh` and `warm`
@@ -172,76 +170,6 @@ export default serve<Env>({
});
```
### Mode: `durable` (advanced)
To use the `durable` mode, you have to specify the Durable Object to extract from your
environment, and additionally export the `DurableBkndApp` class:
```ts
import { serve, DurableBkndApp } from "bknd/adapter/cloudflare";
export { DurableBkndApp };
export default serve<Env>({
// ...
mode: "durable",
bindings: ({ env }) => ({ dobj: env.DOBJ }),
keepAliveSeconds: 60, // optional
});
```
Next, you need to define the Durable Object in your `wrangler.toml` file (refer to the [Durable
Objects](https://developers.cloudflare.com/durable-objects/) documentation):
```toml
[[durable_objects.bindings]]
name = "DOBJ"
class_name = "DurableBkndApp"
[[migrations]]
tag = "v1"
new_classes = ["DurableBkndApp"]
```
Since the communication between the Worker and Durable Object is serialized, the `onBuilt`
property won't work. To use it (e.g. to specify special routes), you need to extend from the
`DurableBkndApp`:
```ts
import type { App } from "bknd";
import { serve, DurableBkndApp } from "bknd/adapter/cloudflare";
export default serve({
// ...
mode: "durable",
bindings: ({ env }) => ({ dobj: env.DOBJ }),
keepAliveSeconds: 60, // optional
});
export class CustomDurableBkndApp extends DurableBkndApp {
async onBuilt(app: App) {
app.modules.server.get("/custom/endpoint", (c) => c.text("Custom"));
}
}
```
In case you've already deployed your Worker, the deploy command may complain about a new class
being used. To fix this issue, you need to add a "rename migration":
```toml
[[durable_objects.bindings]]
name = "DOBJ"
class_name = "CustomDurableBkndApp"
[[migrations]]
tag = "v1"
new_classes = ["DurableBkndApp"]
[[migrations]]
tag = "v2"
renamed_classes = [{from = "DurableBkndApp", to = "CustomDurableBkndApp"}]
deleted_classes = ["DurableBkndApp"]
```
## D1 Sessions (experimental)
D1 now supports to enable [global read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/). This allows to reduce latency by reading from the closest region. In order for this to work, D1 has to be started from a bookmark. You can enable this behavior on bknd by setting the `d1.session` property:
@@ -272,3 +200,83 @@ If bknd is used in a stateful user context (like in a browser), it'll automatica
```bash
curl -H "x-cf-d1-session: <bookmark>" ...
```
## Filesystem access with Vite Plugin
The [Cloudflare Vite Plugin](https://developers.cloudflare.com/workers/vite-plugin/) allows to use Vite with Miniflare to emulate the Cloudflare Workers runtime. This is great, however, `unenv` disables any Node.js APIs that aren't supported, including the `fs` module. If you want to use plugins such as [`syncTypes`](/extending/plugins#synctypes), this will cause issues.
To fix this, bknd exports a Vite plugin that provides filesystem access during development. You can use it by adding the following to your `vite.config.ts` file:
```ts
import { devFsVitePlugin } from "bknd/adapter/cloudflare/vite";
export default defineConfig({
plugins: [devFsVitePlugin()], // [!code highlight]
});
```
Now to use this polyfill, you can use the `devFsWrite` function to write files to the filesystem.
```ts
import { devFsWrite } from "bknd/adapter/cloudflare/vite"; // [!code highlight]
import { syncTypes } from "bknd/plugins";
export default {
options: {
plugins: [
syncTypes({
write: async (et) => {
await devFsWrite("bknd-types.d.ts", et.toString()); // [!code highlight]
}
}),
]
},
} satisfies BkndConfig;
```
## Cloudflare Bindings in CLI
The bknd CLI does not automatically have access to the Cloudflare bindings. We need to manually proxy them to the CLI by using the `withPlatformProxy` helper function:
```typescript title="bknd.config.ts"
import { d1 } from "bknd/adapter/cloudflare";
import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy";
export default withPlatformProxy({
app: ({ env }) => ({
connection: d1({ binding: env.DB }),
}),
});
```
Now you can use the CLI with your Cloudflare resources.
<Callout type="warning">
Make sure to not import from this file in your app, as this would include `wrangler` as a dependency.
</Callout>
Instead, it's recommended to split this configuration into separate files, e.g. `bknd.config.ts` and `config.ts`:
```typescript title="config.ts"
import type { CloudflareBkndConfig } from "bknd/adapter/cloudflare";
export default {
app: ({ env }) => ({
connection: d1({ binding: env.DB }),
}),
} satisfies CloudflareBkndConfig;
```
`config.ts` now holds the configuration, and can safely be imported in your app. Since the CLI looks for a `bknd.config.ts` file by default, we change it to wrap the configuration from `config.ts` in the `withPlatformProxy` helper function.
```typescript title="bknd.config.ts"
import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy";
import config from "./config";
export default withPlatformProxy(config);
```
As an additional safe guard, you have to set a `PROXY` environment variable to `1` to enable the proxy.
```bash
PROXY=1 npx bknd types
```
@@ -18,6 +18,7 @@
"---Extending---",
"./extending/config",
"./extending/events",
"./extending/plugins",
"---Integration---",
"./integration/introduction",
"./integration/(frameworks)/",