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