mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 16:16:02 +00:00
docs: added plugins docs, updated cloudflare docs
This commit is contained in:
@@ -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)/",
|
||||
|
||||
Reference in New Issue
Block a user