mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 16:16:02 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9fb497c2d | |||
| ccd4eb10cb | |||
| 847f75b725 | |||
| 8b4bf0d94a | |||
| bf521e2931 | |||
| 111b2ae364 | |||
| dc08d12ce6 | |||
| ce773007f0 | |||
| e78c271c2f | |||
| 7ee07adc70 | |||
| 53e5c6fd98 | |||
| 4591631d3c | |||
| 5a27c41516 | |||
| 24eefa5357 | |||
| d898018b49 | |||
| 09fdde1c5b | |||
| fb0dbb9a51 | |||
| 81f578895e | |||
| 2de12674d9 | |||
| 5ed1cf19b6 | |||
| 0b21157f92 | |||
| cacb13e464 |
@@ -3,7 +3,7 @@
|
||||

|
||||
|
||||
<p align="center" width="100%">
|
||||
<a href="https://stackblitz.com/github/bknd-io/bknd-examples?hideExplorer=1&embed=1&view=preview&startScript=example-admin-rich&initialPath=%2Fdata%2Fschema" target="_blank">
|
||||
<a href="https://stackblitz.com/github/bknd-io/bknd-demo?hideExplorer=1&embed=1&view=preview&initialPath=%2Fdata%2Fschema" target="_blank">
|
||||
<strong>⭐ Live Demo</strong>
|
||||
</a>
|
||||
</p>
|
||||
@@ -27,8 +27,8 @@ bknd simplifies app development by providing a fully functional backend for data
|
||||
## Size
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
|
||||
|
||||
@@ -52,6 +52,7 @@ Creating digital products always requires developing both the backend (the logic
|
||||
* using a React framework (Next.js, React Router, Astro)
|
||||
* 📦 Official API and React SDK with type-safety
|
||||
* ⚛️ React elements for auto-configured authentication and media components
|
||||
* 🛠️ MCP server, client and UI built-in to control your backend
|
||||
|
||||
## Structure
|
||||
The package is mainly split into 4 parts, each serving a specific purpose:
|
||||
|
||||
@@ -15,6 +15,7 @@ describe("AppServer", () => {
|
||||
},
|
||||
mcp: {
|
||||
enabled: false,
|
||||
path: "/api/system/mcp",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -36,6 +37,7 @@ describe("AppServer", () => {
|
||||
},
|
||||
mcp: {
|
||||
enabled: false,
|
||||
path: "/api/system/mcp",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+7
-7
@@ -3,7 +3,7 @@
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"bin": "./dist/cli/index.js",
|
||||
"version": "0.16.1",
|
||||
"version": "0.17.0",
|
||||
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
||||
"homepage": "https://bknd.io",
|
||||
"repository": {
|
||||
@@ -39,11 +39,11 @@
|
||||
"test:adapters": "bun test src/adapter/**/*.adapter.spec.ts --bail",
|
||||
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
||||
"test:vitest:coverage": "vitest run --coverage",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:adapters": "bun run e2e/adapters.ts",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:debug": "playwright test --debug",
|
||||
"test:e2e:report": "playwright show-report",
|
||||
"test:e2e": "VITE_DB_URL=:memory: playwright test",
|
||||
"test:e2e:adapters": "VITE_DB_URL=:memory: bun run e2e/adapters.ts",
|
||||
"test:e2e:ui": "VITE_DB_URL=:memory: playwright test --ui",
|
||||
"test:e2e:debug": "VITE_DB_URL=:memory: playwright test --debug",
|
||||
"test:e2e:report": "VITE_DB_URL=:memory: playwright show-report",
|
||||
"docs:build-assets": "bun internal/docs.build-assets.ts"
|
||||
},
|
||||
"license": "FSL-1.1-MIT",
|
||||
@@ -65,7 +65,7 @@
|
||||
"hono": "4.8.3",
|
||||
"json-schema-library": "10.0.0-rc7",
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"jsonv-ts": "^0.8.1",
|
||||
"jsonv-ts": "0.8.2",
|
||||
"kysely": "0.27.6",
|
||||
"lodash-es": "^4.17.21",
|
||||
"oauth4webapi": "^2.11.1",
|
||||
|
||||
+2
-1
@@ -293,9 +293,10 @@ export class App<C extends Connection = Connection, Options extends AppOptions =
|
||||
if (!this.mcp) {
|
||||
throw new Error("MCP is not enabled");
|
||||
}
|
||||
const mcpPath = this.modules.get("server").config.mcp.path;
|
||||
|
||||
return new McpClient({
|
||||
url: "http://localhost/mcp",
|
||||
url: "http://localhost" + mcpPath,
|
||||
fetch: this.server.request,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export function adapterTestSuite<
|
||||
url: overrides.dbUrl ?? ":memory:",
|
||||
origin: "localhost",
|
||||
} as any,
|
||||
{ id },
|
||||
{ force: false, id },
|
||||
);
|
||||
expect(app).toBeDefined();
|
||||
expect(app.toJSON().server.cors.origin).toEqual("localhost");
|
||||
@@ -69,7 +69,7 @@ export function adapterTestSuite<
|
||||
};
|
||||
|
||||
test("responds with the same app id", async () => {
|
||||
const fetcher = makeHandler(undefined, undefined, { id });
|
||||
const fetcher = makeHandler(undefined, undefined, { force: false, id });
|
||||
|
||||
const { res, data } = await getConfig(fetcher);
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { makeApp } from "./modes/fresh";
|
||||
import { makeConfig, type CfMakeConfigArgs } from "./config";
|
||||
import { makeConfig, type CloudflareContext } from "./config";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import type { CloudflareBkndConfig } from "./cloudflare-workers.adapter";
|
||||
import { type CloudflareBkndConfig, createApp } from "./cloudflare-workers.adapter";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
@@ -41,18 +40,19 @@ describe("cf adapter", () => {
|
||||
expect(dynamicConfig.connection).toBeDefined();
|
||||
});
|
||||
|
||||
adapterTestSuite<CloudflareBkndConfig, CfMakeConfigArgs<any>>(bunTestRunner, {
|
||||
adapterTestSuite<CloudflareBkndConfig, CloudflareContext<any>>(bunTestRunner, {
|
||||
makeApp: async (c, a, o) => {
|
||||
return await makeApp(c, { env: a } as any, o);
|
||||
return await createApp(c, { env: a } as any, o);
|
||||
},
|
||||
makeHandler: (c, a, o) => {
|
||||
console.log("args", a);
|
||||
return async (request: any) => {
|
||||
const app = await makeApp(
|
||||
const app = await createApp(
|
||||
// needs a fallback, otherwise tries to launch D1
|
||||
c ?? {
|
||||
connection: { url: DB_URL },
|
||||
},
|
||||
a!,
|
||||
a as any,
|
||||
o,
|
||||
);
|
||||
return app.fetch(request);
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import type { RuntimeBkndConfig } from "bknd/adapter";
|
||||
import { Hono } from "hono";
|
||||
import { serveStatic } from "hono/cloudflare-workers";
|
||||
import { getFresh } from "./modes/fresh";
|
||||
import { getCached } from "./modes/cached";
|
||||
import type { App, MaybePromise } from "bknd";
|
||||
import type { MaybePromise } from "bknd";
|
||||
import { $console } from "bknd/utils";
|
||||
import { createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
||||
import { registerAsyncsExecutionContext, makeConfig, type CloudflareContext } from "./config";
|
||||
|
||||
declare global {
|
||||
namespace Cloudflare {
|
||||
@@ -16,7 +16,6 @@ declare global {
|
||||
|
||||
export type CloudflareEnv = Cloudflare.Env;
|
||||
export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> & {
|
||||
mode?: "warm" | "fresh" | "cache";
|
||||
bindings?: (args: Env) => MaybePromise<{
|
||||
kv?: KVNamespace;
|
||||
db?: D1Database;
|
||||
@@ -34,11 +33,31 @@ export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> &
|
||||
registerMedia?: boolean | ((env: Env) => void);
|
||||
};
|
||||
|
||||
export type Context<Env = CloudflareEnv> = {
|
||||
request: Request;
|
||||
env: Env;
|
||||
ctx: ExecutionContext;
|
||||
};
|
||||
export async function createApp<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env>,
|
||||
ctx: Partial<CloudflareContext<Env>> = {},
|
||||
opts: RuntimeOptions = {
|
||||
// by default, require the app to be rebuilt every time
|
||||
force: true,
|
||||
},
|
||||
) {
|
||||
const appConfig = await makeConfig(
|
||||
{
|
||||
...config,
|
||||
onBuilt: async (app) => {
|
||||
if (ctx.ctx) {
|
||||
registerAsyncsExecutionContext(app, ctx?.ctx);
|
||||
}
|
||||
await config.onBuilt?.(app);
|
||||
},
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
return await createRuntimeApp<Env>(appConfig, ctx?.env, opts);
|
||||
}
|
||||
|
||||
// compatiblity
|
||||
export const getFresh = createApp;
|
||||
|
||||
export function serve<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env> = {},
|
||||
@@ -77,23 +96,8 @@ export function serve<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
}
|
||||
}
|
||||
|
||||
const context = { request, env, ctx } as Context<Env>;
|
||||
const mode = config.mode ?? "warm";
|
||||
|
||||
let app: App;
|
||||
switch (mode) {
|
||||
case "fresh":
|
||||
app = await getFresh(config, context, { force: true });
|
||||
break;
|
||||
case "warm":
|
||||
app = await getFresh(config, context);
|
||||
break;
|
||||
case "cache":
|
||||
app = await getCached(config, context);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown mode ${mode}`);
|
||||
}
|
||||
const context = { request, env, ctx } as CloudflareContext<Env>;
|
||||
const app = await createApp(config, context);
|
||||
|
||||
return app.fetch(request, env, ctx);
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getBinding } from "./bindings";
|
||||
import { d1Sqlite } from "./connection/D1Connection";
|
||||
import type { CloudflareBkndConfig, CloudflareEnv } from ".";
|
||||
import { App } from "bknd";
|
||||
import type { Context, ExecutionContext } from "hono";
|
||||
import type { Context as HonoContext, ExecutionContext } from "hono";
|
||||
import { $console } from "bknd/utils";
|
||||
import { setCookie } from "hono/cookie";
|
||||
|
||||
@@ -22,10 +22,10 @@ export const constants = {
|
||||
},
|
||||
};
|
||||
|
||||
export type CfMakeConfigArgs<Env extends CloudflareEnv = CloudflareEnv> = {
|
||||
export type CloudflareContext<Env extends CloudflareEnv = CloudflareEnv> = {
|
||||
env: Env;
|
||||
ctx?: ExecutionContext;
|
||||
request?: Request;
|
||||
ctx: ExecutionContext;
|
||||
request: Request;
|
||||
};
|
||||
|
||||
function getCookieValue(cookies: string | null, name: string) {
|
||||
@@ -67,7 +67,7 @@ export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
|
||||
|
||||
return undefined;
|
||||
},
|
||||
set: (c: Context, d1?: D1DatabaseSession) => {
|
||||
set: (c: HonoContext, d1?: D1DatabaseSession) => {
|
||||
if (!d1 || !config.d1?.session) return;
|
||||
|
||||
const session = d1.getBookmark();
|
||||
@@ -91,7 +91,7 @@ export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
|
||||
let media_registered: boolean = false;
|
||||
export async function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env>,
|
||||
args?: CfMakeConfigArgs<Env>,
|
||||
args?: Partial<CloudflareContext<Env>>,
|
||||
) {
|
||||
if (!media_registered && config.registerMedia !== false) {
|
||||
if (typeof config.registerMedia === "function") {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { d1Sqlite, type D1ConnectionConfig } from "./connection/D1Connection";
|
||||
|
||||
export * from "./cloudflare-workers.adapter";
|
||||
export { makeApp, getFresh } from "./modes/fresh";
|
||||
export { getCached } from "./modes/cached";
|
||||
export {
|
||||
getFresh,
|
||||
createApp,
|
||||
type CloudflareEnv,
|
||||
type CloudflareBkndConfig,
|
||||
} from "./cloudflare-workers.adapter";
|
||||
export { d1Sqlite, type D1ConnectionConfig };
|
||||
export { doSqlite, type DoConnectionConfig } from "./connection/DoConnection";
|
||||
export {
|
||||
@@ -12,10 +15,10 @@ export {
|
||||
type GetBindingType,
|
||||
type BindingMap,
|
||||
} from "./bindings";
|
||||
export { constants } from "./config";
|
||||
export { constants, type CloudflareContext } from "./config";
|
||||
export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter";
|
||||
export { registries } from "bknd";
|
||||
export { devFsPlugin, devFsWrite } from "./vite";
|
||||
export { devFsVitePlugin, devFsWrite } from "./vite";
|
||||
|
||||
// for compatibility with old code
|
||||
export function d1<DB extends D1Database | D1DatabaseSession = D1Database>(
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { App } from "bknd";
|
||||
import { createRuntimeApp } from "bknd/adapter";
|
||||
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
|
||||
import { makeConfig, registerAsyncsExecutionContext, constants } from "../config";
|
||||
|
||||
export async function getCached<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env>,
|
||||
args: Context<Env>,
|
||||
) {
|
||||
const { env, ctx } = args;
|
||||
const { kv } = await config.bindings?.(env)!;
|
||||
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
|
||||
const key = config.key ?? "app";
|
||||
|
||||
const cachedConfig = await kv.get(key);
|
||||
const initialConfig = cachedConfig ? JSON.parse(cachedConfig) : undefined;
|
||||
|
||||
async function saveConfig(__config: any) {
|
||||
ctx.waitUntil(kv!.put(key, JSON.stringify(__config)));
|
||||
}
|
||||
|
||||
const app = await createRuntimeApp(
|
||||
{
|
||||
...makeConfig(config, args),
|
||||
initialConfig,
|
||||
onBuilt: async (app) => {
|
||||
registerAsyncsExecutionContext(app, ctx);
|
||||
app.module.server.client.get(constants.cache_endpoint, async (c) => {
|
||||
await kv.delete(key);
|
||||
return c.json({ message: "Cache cleared" });
|
||||
});
|
||||
await config.onBuilt?.(app);
|
||||
},
|
||||
beforeBuild: async (app) => {
|
||||
app.emgr.onEvent(
|
||||
App.Events.AppConfigUpdatedEvent,
|
||||
async ({ params: { app } }) => {
|
||||
saveConfig(app.toJSON(true));
|
||||
},
|
||||
"sync",
|
||||
);
|
||||
await config.beforeBuild?.(app);
|
||||
},
|
||||
},
|
||||
args,
|
||||
);
|
||||
|
||||
if (!cachedConfig) {
|
||||
saveConfig(app.toJSON(true));
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
||||
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
|
||||
import { makeConfig, registerAsyncsExecutionContext, type CfMakeConfigArgs } from "../config";
|
||||
|
||||
export async function makeApp<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env>,
|
||||
args?: CfMakeConfigArgs<Env>,
|
||||
opts?: RuntimeOptions,
|
||||
) {
|
||||
return await createRuntimeApp<Env>(await makeConfig(config, args), args?.env, opts);
|
||||
}
|
||||
|
||||
export async function getFresh<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env>,
|
||||
ctx: Context<Env>,
|
||||
opts: RuntimeOptions = {},
|
||||
) {
|
||||
return await makeApp(
|
||||
{
|
||||
...config,
|
||||
onBuilt: async (app) => {
|
||||
registerAsyncsExecutionContext(app, ctx.ctx);
|
||||
await config.onBuilt?.(app);
|
||||
},
|
||||
},
|
||||
ctx,
|
||||
opts,
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export function devFsVitePlugin({
|
||||
}: {
|
||||
verbose?: boolean;
|
||||
configFile?: string;
|
||||
}): Plugin {
|
||||
} = {}): any {
|
||||
let isDev = false;
|
||||
let projectRoot = "";
|
||||
|
||||
@@ -115,7 +115,7 @@ if (typeof globalThis !== 'undefined') {
|
||||
return polyfill + code;
|
||||
}
|
||||
},
|
||||
};
|
||||
} satisfies Plugin;
|
||||
}
|
||||
|
||||
// Write function that uses the dev-fs polyfill injected by our Vite plugin
|
||||
|
||||
@@ -83,8 +83,12 @@ export async function createAdapterApp<Config extends BkndConfig = BkndConfig, A
|
||||
}
|
||||
|
||||
app = App.create(appConfig);
|
||||
apps.set(id, app);
|
||||
|
||||
if (!opts?.force) {
|
||||
apps.set(id, app);
|
||||
}
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Handler } from "hono/types";
|
||||
import type { ModuleBuildContext } from "modules";
|
||||
import { Controller } from "modules/Controller";
|
||||
import { jsc, s, describeRoute, schemaToSpec, omitKeys, pickKeys, mcpTool } from "bknd/utils";
|
||||
@@ -37,14 +36,6 @@ export class DataController extends Controller {
|
||||
const hono = this.create().use(auth(), permission(SystemPermissions.accessApi));
|
||||
const entitiesEnum = this.getEntitiesEnum(this.em);
|
||||
|
||||
// @todo: sample implementation how to augment handler with additional info
|
||||
function handler<HH extends Handler>(name: string, h: HH): any {
|
||||
const func = h;
|
||||
// @ts-ignore
|
||||
func.description = name;
|
||||
return func;
|
||||
}
|
||||
|
||||
// info
|
||||
hono.get(
|
||||
"/",
|
||||
@@ -52,10 +43,7 @@ export class DataController extends Controller {
|
||||
summary: "Retrieve data configuration",
|
||||
tags: ["data"],
|
||||
}),
|
||||
handler("data info", (c) => {
|
||||
// sample implementation
|
||||
return c.json(this.em.toJSON());
|
||||
}),
|
||||
(c) => c.json(this.em.toJSON()),
|
||||
);
|
||||
|
||||
// sync endpoint
|
||||
|
||||
@@ -51,7 +51,7 @@ export class Controller {
|
||||
|
||||
protected getEntitiesEnum(em: EntityManager<any>): s.StringSchema {
|
||||
const entities = em.entities.map((e) => e.name);
|
||||
return entities.length > 0 ? s.string({ enum: entities }) : s.string();
|
||||
return entities.length > 0 ? s.anyOf([s.string({ enum: entities }), s.string()]) : s.string();
|
||||
}
|
||||
|
||||
registerMcp(): void {}
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import {
|
||||
mark,
|
||||
stripMark,
|
||||
$console,
|
||||
s,
|
||||
objectEach,
|
||||
transformObject,
|
||||
McpServer,
|
||||
DebugLogger,
|
||||
} from "bknd/utils";
|
||||
import { mark, stripMark, $console, s, objectEach, transformObject, McpServer } from "bknd/utils";
|
||||
import { DebugLogger } from "core/utils/DebugLogger";
|
||||
import { Guard } from "auth/authorize/Guard";
|
||||
import { env } from "core/env";
|
||||
import { BkndError } from "core/errors";
|
||||
|
||||
@@ -24,6 +24,7 @@ export const serverConfigSchema = $object(
|
||||
}),
|
||||
mcp: s.strictObject({
|
||||
enabled: s.boolean({ default: false }),
|
||||
path: s.string({ default: "/api/system/mcp" }),
|
||||
}),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -60,8 +60,9 @@ export class SystemController extends Controller {
|
||||
|
||||
register(app: App) {
|
||||
app.server.route("/api/system", this.getController());
|
||||
const config = app.modules.get("server").config;
|
||||
|
||||
if (!this.app.modules.get("server").config.mcp.enabled) {
|
||||
if (!config.mcp.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,7 +85,7 @@ export class SystemController extends Controller {
|
||||
const level = consoleMap[message.params.level];
|
||||
if (!level) return;
|
||||
|
||||
$console[level](message.params.message);
|
||||
$console[level]("MCP notification", message.params.message ?? message.params);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -97,7 +98,7 @@ export class SystemController extends Controller {
|
||||
explainEndpoint: true,
|
||||
},
|
||||
endpoint: {
|
||||
path: "/mcp",
|
||||
path: config.mcp.path as any,
|
||||
// @ts-ignore
|
||||
_init: isNode() ? { duplex: "half" } : {},
|
||||
},
|
||||
@@ -375,6 +376,7 @@ export class SystemController extends Controller {
|
||||
}),
|
||||
(c) =>
|
||||
c.json({
|
||||
id: this.app._id,
|
||||
version: {
|
||||
config: c.get("app")?.version(),
|
||||
bknd: getVersion(),
|
||||
|
||||
@@ -16,7 +16,7 @@ const schema = s.partialObject({
|
||||
metadata: s.string({ enum: ["copyright", "keep", "none"] }),
|
||||
quality: s.number({ minimum: 1, maximum: 100 }),
|
||||
});
|
||||
type ImageOptimizationSchema = s.Static<typeof schema>;
|
||||
export type CloudflareImageOptimizationSchema = s.Static<typeof schema>;
|
||||
|
||||
export type CloudflareImageOptimizationOptions = {
|
||||
/**
|
||||
@@ -38,12 +38,12 @@ export type CloudflareImageOptimizationOptions = {
|
||||
* The default options to use
|
||||
* @default {}
|
||||
*/
|
||||
defaultOptions?: ImageOptimizationSchema;
|
||||
defaultOptions?: CloudflareImageOptimizationSchema;
|
||||
/**
|
||||
* The fixed options to use
|
||||
* @default {}
|
||||
*/
|
||||
fixedOptions?: ImageOptimizationSchema;
|
||||
fixedOptions?: CloudflareImageOptimizationSchema;
|
||||
/**
|
||||
* The cache control to use
|
||||
* @default public, max-age=31536000, immutable
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export {
|
||||
cloudflareImageOptimization,
|
||||
type CloudflareImageOptimizationSchema,
|
||||
type CloudflareImageOptimizationOptions,
|
||||
} from "./cloudflare/image-optimization.plugin";
|
||||
export { showRoutes, type ShowRoutesOptions } from "./dev/show-routes.plugin";
|
||||
|
||||
@@ -20,8 +20,9 @@ const style = {
|
||||
basicChildStyle: "pl-5 ml-1 border-l border-muted hover:border-primary/20",
|
||||
container: "ml-[-10px]",
|
||||
label: "text-primary/90 font-bold font-mono mr-2",
|
||||
stringValue: "text-emerald-600 dark:text-emerald-500 font-mono select-text",
|
||||
numberValue: "text-sky-500 dark:text-sky-400 font-mono",
|
||||
stringValue:
|
||||
"text-emerald-600 dark:text-emerald-500 font-mono select-text text-wrap whitespace-wrap break-words",
|
||||
numberValue: "text-sky-500 dark:text-sky-400 font-mono select-text",
|
||||
nullValue: "text-zinc-400 font-mono",
|
||||
undefinedValue: "text-zinc-400 font-mono",
|
||||
otherValue: "text-zinc-400 font-mono",
|
||||
|
||||
@@ -13,6 +13,14 @@ const Warning = ({ className, ...props }: IconProps) => (
|
||||
/>
|
||||
);
|
||||
|
||||
const Err = ({ className, ...props }: IconProps) => (
|
||||
<TbAlertCircle
|
||||
{...props}
|
||||
className={twMerge("dark:text-red-300 text-red-700 cursor-help", className)}
|
||||
/>
|
||||
);
|
||||
|
||||
export const Icon = {
|
||||
Warning,
|
||||
Err,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useClickOutside, useHotkeys } from "@mantine/hooks";
|
||||
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
|
||||
import { clampNumber } from "core/utils/numbers";
|
||||
import { transformObject, clampNumber } from "bknd/utils";
|
||||
import { throttle } from "lodash-es";
|
||||
import { ScrollArea } from "radix-ui";
|
||||
import {
|
||||
@@ -19,9 +19,21 @@ import { appShellStore } from "ui/store";
|
||||
import { useLocation } from "wouter";
|
||||
|
||||
export function Root({ children }: { children: React.ReactNode }) {
|
||||
const sidebarWidths = appShellStore((store) => store.sidebars);
|
||||
const style = transformObject(sidebarWidths, (value) => value.width);
|
||||
return (
|
||||
<AppShellProvider>
|
||||
<div id="app-shell" data-shell="root" className="flex flex-1 flex-col select-none h-dvh">
|
||||
<div
|
||||
id="app-shell"
|
||||
data-shell="root"
|
||||
className="flex flex-1 flex-col select-none h-dvh"
|
||||
style={Object.fromEntries(
|
||||
Object.entries(style).map(([key, value]) => [
|
||||
`--sidebar-width-${key}`,
|
||||
`${value}px`,
|
||||
]),
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</AppShellProvider>
|
||||
@@ -385,11 +397,16 @@ export function MaxHeightContainer(props: ComponentPropsWithoutRef<"div">) {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(updateHeaderHeight, []);
|
||||
useEffect(() => {
|
||||
updateHeaderHeight();
|
||||
const resize = throttle(updateHeaderHeight, 500);
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("resize", throttle(updateHeaderHeight, 500));
|
||||
}
|
||||
window.addEventListener("resize", resize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", resize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={scrollRef} style={{ height: `${height - offset}px` }} {...props}>
|
||||
|
||||
+5
-1
@@ -74,7 +74,7 @@
|
||||
@apply bg-background text-primary overflow-hidden h-dvh w-dvw;
|
||||
|
||||
::selection {
|
||||
@apply bg-muted;
|
||||
@apply bg-primary/15;
|
||||
}
|
||||
|
||||
input {
|
||||
@@ -122,3 +122,7 @@ body,
|
||||
@utility debug-blue {
|
||||
@apply border-blue-500 border;
|
||||
}
|
||||
@utility w-min-content {
|
||||
width: min-content;
|
||||
width: -webkit-min-content;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom/client";
|
||||
import Admin from "./Admin";
|
||||
import "./main.css";
|
||||
//import "./main.css";
|
||||
import "./styles.css";
|
||||
|
||||
function render() {
|
||||
|
||||
@@ -139,7 +139,7 @@ function Adapters() {
|
||||
<span className="font-bold">Media Adapter:</span>
|
||||
{ctx.selected === null && <span className="opacity-70"> (Choose one)</span>}
|
||||
</Formy.Label>
|
||||
<div className="flex flex-row gap-1 mb-2">
|
||||
<div className="grid grid-cols-2 md:flex flex-row gap-1 mb-2 flex-wrap">
|
||||
{ctx.schemas?.map((schema: any, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
@@ -165,7 +165,7 @@ function Adapters() {
|
||||
</div>
|
||||
{ctx.selected !== null && (
|
||||
<Formy.Group as="fieldset" error={ctx.errors.length > 0}>
|
||||
<Formy.Label as="legend" className="font-mono px-2">
|
||||
<Formy.Label as="legend" className="font-mono px-2 w-min-content">
|
||||
{autoFormatString(ctx.selectedSchema!.title!)}
|
||||
</Formy.Label>
|
||||
<FormContextOverride schema={ctx.selectedSchema} prefix={ctx.path}>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { McpClient, type McpClientConfig } from "jsonv-ts/mcp";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
|
||||
const clients = new Map<string, McpClient>();
|
||||
|
||||
export function getClient(opts: McpClientConfig) {
|
||||
if (!clients.has(JSON.stringify(opts))) {
|
||||
clients.set(JSON.stringify(opts), new McpClient(opts));
|
||||
}
|
||||
return clients.get(JSON.stringify(opts))!;
|
||||
}
|
||||
|
||||
export function useMcpClient() {
|
||||
const { config } = useBknd();
|
||||
return getClient({ url: window.location.origin + config.server.mcp.path });
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export default function ToolsMcp() {
|
||||
const setFeature = useMcpStore((state) => state.setFeature);
|
||||
const content = useMcpStore((state) => state.content);
|
||||
const openSidebar = appShellStore((store) => store.toggleSidebar("default"));
|
||||
const mcpPath = config.server.mcp.path;
|
||||
|
||||
if (!config.server.mcp.enabled) {
|
||||
return (
|
||||
@@ -39,7 +40,7 @@ export default function ToolsMcp() {
|
||||
<TbWorld />
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-mono leading-none">
|
||||
{window.location.origin + "/mcp"}
|
||||
{window.location.origin + mcpPath}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState, useTransition } from "react";
|
||||
import { getClient, getTemplate } from "./utils";
|
||||
import { getTemplate } from "./utils";
|
||||
import { useMcpStore } from "./state";
|
||||
import { AppShell } from "ui/layouts/AppShell";
|
||||
import { TbHistory, TbHistoryOff, TbRefresh } from "react-icons/tb";
|
||||
@@ -10,9 +10,11 @@ import { Field, Form } from "ui/components/form/json-schema-form";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import * as Formy from "ui/components/form/Formy";
|
||||
import { appShellStore } from "ui/store";
|
||||
import { Icon } from "ui/components/display/Icon";
|
||||
import { useMcpClient } from "./hooks/use-mcp-client";
|
||||
|
||||
export function Sidebar({ open, toggle }) {
|
||||
const client = getClient();
|
||||
const client = useMcpClient();
|
||||
const closeSidebar = appShellStore((store) => store.closeSidebar("default"));
|
||||
const tools = useMcpStore((state) => state.tools);
|
||||
const setTools = useMcpStore((state) => state.setTools);
|
||||
@@ -20,11 +22,18 @@ export function Sidebar({ open, toggle }) {
|
||||
const content = useMcpStore((state) => state.content);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const res = await client.listTools();
|
||||
if (res) setTools(res.tools);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await client.listTools();
|
||||
if (res) setTools(res.tools);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError(String(e));
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
@@ -39,6 +48,7 @@ export function Sidebar({ open, toggle }) {
|
||||
toggle={toggle}
|
||||
renderHeaderRight={() => (
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
{error && <Icon.Err title={error} className="size-5 pointer-events-auto" />}
|
||||
<span className="flex-inline bg-primary/10 px-2 py-1.5 rounded-xl text-sm font-mono leading-none">
|
||||
{tools.length}
|
||||
</span>
|
||||
@@ -88,7 +98,7 @@ export function Content() {
|
||||
const [result, setResult] = useState<object | null>(null);
|
||||
const historyVisible = useMcpStore((state) => state.historyVisible);
|
||||
const setHistoryVisible = useMcpStore((state) => state.setHistoryVisible);
|
||||
const client = getClient();
|
||||
const client = useMcpClient();
|
||||
const jsonViewerTabsRef = useRef<JsonViewerTabsRef>(null);
|
||||
const hasInputSchema =
|
||||
content?.inputSchema && Object.keys(content.inputSchema.properties ?? {}).length > 0;
|
||||
@@ -129,7 +139,7 @@ export function Content() {
|
||||
|
||||
return (
|
||||
<Form
|
||||
className="flex flex-grow flex-col max-w-screen"
|
||||
className="flex flex-grow flex-col min-w-0 max-w-screen"
|
||||
key={content.name}
|
||||
schema={{
|
||||
title: "InputSchema",
|
||||
@@ -170,7 +180,12 @@ export function Content() {
|
||||
</AppShell.SectionHeaderTitle>
|
||||
</AppShell.SectionHeader>
|
||||
<div className="flex flex-grow flex-row w-vw">
|
||||
<div className="flex flex-grow flex-col w-full">
|
||||
<div
|
||||
className="flex flex-grow flex-col max-w-full"
|
||||
style={{
|
||||
width: "calc(100% - var(--sidebar-width-right) - 1px)",
|
||||
}}
|
||||
>
|
||||
<AppShell.Scrollable>
|
||||
<div key={JSON.stringify(content)} className="flex flex-col py-4 px-5 gap-4">
|
||||
<p className="text-primary/80">{content?.description}</p>
|
||||
@@ -219,8 +234,9 @@ const History = () => {
|
||||
<JsonViewer
|
||||
key={`${item.type}-${i}`}
|
||||
json={item.data}
|
||||
title={item.type}
|
||||
title={item.type.substring(0, 3)}
|
||||
expand={2}
|
||||
showCopy
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
import { McpClient, type McpClientConfig } from "jsonv-ts/mcp";
|
||||
import { Draft2019 } from "json-schema-library";
|
||||
|
||||
const clients = new Map<string, McpClient>();
|
||||
|
||||
export function getClient(
|
||||
{ url, ...opts }: McpClientConfig = { url: window.location.origin + "/mcp" },
|
||||
) {
|
||||
if (!clients.has(String(url))) {
|
||||
clients.set(String(url), new McpClient({ url, ...opts }));
|
||||
}
|
||||
return clients.get(String(url))!;
|
||||
}
|
||||
|
||||
export function getTemplate(schema: object) {
|
||||
if (!schema || schema === undefined || schema === null) return undefined;
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@ html.fixed,
|
||||
html.fixed body {
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 100dvh;
|
||||
width: 100dvw;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
overscroll-behavior-x: contain;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"app": {
|
||||
"name": "bknd",
|
||||
"version": "0.16.1",
|
||||
"version": "0.17.0-rc.1",
|
||||
"bin": "./dist/cli/index.js",
|
||||
"dependencies": {
|
||||
"@cfworker/json-schema": "^4.1.1",
|
||||
@@ -35,7 +35,7 @@
|
||||
"hono": "4.8.3",
|
||||
"json-schema-library": "10.0.0-rc7",
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"jsonv-ts": "^0.8.1",
|
||||
"jsonv-ts": "0.8.2",
|
||||
"kysely": "0.27.6",
|
||||
"lodash-es": "^4.17.21",
|
||||
"oauth4webapi": "^2.11.1",
|
||||
@@ -2516,7 +2516,7 @@
|
||||
|
||||
"jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="],
|
||||
|
||||
"jsonv-ts": ["jsonv-ts@0.8.1", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-kqt1OHZ4WM92PDAxySZeGGzccZr6q5YdKpM8c7QWkwGoaa1azwTG5lV9SN3PT4kVgI0OYFDr3OGkgCszLQ+WPw=="],
|
||||
"jsonv-ts": ["jsonv-ts@0.8.2", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-1Z7+maCfoGGqBPu5vN8rU9gIsW7OatYmn+STBTPkybbtNqeMzAoJDDrXHjsZ89x5dPH9W+OgMpNLtN0ouwiMYg=="],
|
||||
|
||||
"jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="],
|
||||
|
||||
|
||||
@@ -3,79 +3,77 @@
|
||||
import * as React from "react";
|
||||
|
||||
export const examples = {
|
||||
adminRich: {
|
||||
path: "github/bknd-io/bknd-examples",
|
||||
startScript: "example-admin-rich",
|
||||
initialPath: "/data/schema"
|
||||
}
|
||||
adminRich: {
|
||||
path: "github/bknd-io/bknd-examples",
|
||||
startScript: "example-admin-rich",
|
||||
initialPath: "/data/schema",
|
||||
},
|
||||
};
|
||||
|
||||
export const StackBlitz = ({
|
||||
path,
|
||||
ratio = 9 / 16,
|
||||
example,
|
||||
...props
|
||||
path,
|
||||
ratio = 9 / 16,
|
||||
example,
|
||||
...props
|
||||
}: {
|
||||
path?: string;
|
||||
ratio?: number;
|
||||
example?: keyof typeof examples;
|
||||
[key: string]: unknown;
|
||||
path?: string;
|
||||
ratio?: number;
|
||||
example?: keyof typeof examples;
|
||||
[key: string]: unknown;
|
||||
}) => {
|
||||
const selected = example ? examples[example] : undefined;
|
||||
const finalPath = path || selected?.path || "github/bknd-io/bknd-examples";
|
||||
const selected = example ? examples[example] : undefined;
|
||||
const finalPath = path || selected?.path || "github/bknd-io/bknd-examples";
|
||||
|
||||
const params = new URLSearchParams({
|
||||
ctl: "1",
|
||||
hideExplorer: "1",
|
||||
embed: "1",
|
||||
view: "preview",
|
||||
...(selected || {}),
|
||||
...props
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
ctl: "1",
|
||||
hideExplorer: "1",
|
||||
embed: "1",
|
||||
view: "preview",
|
||||
...(selected || {}),
|
||||
...props,
|
||||
});
|
||||
|
||||
const url = new URL(
|
||||
`https://stackblitz.com/${finalPath}?${params.toString()}`
|
||||
);
|
||||
const url = new URL(`https://stackblitz.com/${finalPath}?${params.toString()}`);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
paddingTop: `${ratio * 100}%`
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
src={url.toString()}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
border: "none"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "80%",
|
||||
opacity: 0.7,
|
||||
marginTop: "0.2rem",
|
||||
marginBottom: "1rem",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
If you’re having issues viewing it inline,{" "}
|
||||
<a href={url.toString()} target="_blank" rel="noreferrer">
|
||||
try in a new tab
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
paddingTop: `${ratio * 100}%`,
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
src={url.toString()}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
border: "none",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "80%",
|
||||
opacity: 0.7,
|
||||
marginTop: "0.2rem",
|
||||
marginBottom: "1rem",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
If you’re having issues viewing it inline,{" "}
|
||||
<a href={url.toString()} target="_blank" rel="noreferrer">
|
||||
try in a new tab
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -29,15 +29,15 @@ const app = createApp();
|
||||
|
||||
app.emgr.onEvent(AppEvents.AppRequest, async (event) => {
|
||||
// ^? AppRequest
|
||||
console.log("Request received", event.request.url);
|
||||
console.log("Request received", event.params.request.url);
|
||||
});
|
||||
|
||||
app.emgr.on("app-request", async (event) => {
|
||||
console.log("Request received", event.request.url);
|
||||
console.log("Request received", event.params.request.url);
|
||||
});
|
||||
|
||||
app.emgr.onAny(async (event) => {
|
||||
console.log("Event received", event.slug);
|
||||
app.emgr.onAny(async (event, name) => {
|
||||
console.log("Event received", event, name);
|
||||
});
|
||||
```
|
||||
|
||||
@@ -49,7 +49,7 @@ import { AppEvents } from "bknd";
|
||||
export default {
|
||||
onBuilt: (app) => {
|
||||
app.emgr.onEvent(AppEvents.AppRequest, async (event) => {
|
||||
console.log("Request received", event.request.url);
|
||||
console.log("Request received", event.params.request.url);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -63,7 +63,7 @@ By default, listeners are registered as `async` listeners, meaning that the list
|
||||
app.emgr.onEvent(
|
||||
AppEvents.AppRequest,
|
||||
async (event) => {
|
||||
console.log("Request received", event.request.url);
|
||||
console.log("Request received", event.params.request.url);
|
||||
},
|
||||
{ mode: "sync" },
|
||||
);
|
||||
|
||||
@@ -108,7 +108,7 @@ import { config } from "@/bknd";
|
||||
import { serve } from "bknd/adapter/nextjs";
|
||||
|
||||
// optionally, you can set the runtime to edge for better performance
|
||||
export const runtime = "edge";
|
||||
// export const runtime = "edge";
|
||||
|
||||
const handler = serve({
|
||||
...config,
|
||||
|
||||
@@ -42,8 +42,7 @@ bun add bknd
|
||||
|
||||
## Serve the API
|
||||
|
||||
If you don't choose anything specific, the following code will use the `warm` mode and uses the first D1 binding it finds. See the
|
||||
chapter [Using a different mode](#using-a-different-mode) for available modes.
|
||||
If you don't choose anything specific, it uses the first D1 binding it finds.
|
||||
|
||||
```ts title="src/index.ts"
|
||||
import { serve, d1 } from "bknd/adapter/cloudflare";
|
||||
@@ -130,46 +129,6 @@ export default serve<Env>({
|
||||
|
||||
The property `app.server` is a [Hono](https://hono.dev/) instance, you can literally anything you can do with Hono.
|
||||
|
||||
## Using a different mode
|
||||
|
||||
With the Cloudflare Workers adapter, you're being offered to 4 modes to choose from (default:
|
||||
`warm`):
|
||||
|
||||
| Mode | Description | Use Case |
|
||||
| :-------- | :----------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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. |
|
||||
|
||||
### Modes: `fresh` and `warm`
|
||||
|
||||
To use either `fresh` or `warm`, all you have to do is adding the desired mode to `cloudflare.
|
||||
mode`, like so:
|
||||
|
||||
```ts
|
||||
import { serve } from "bknd/adapter/cloudflare";
|
||||
|
||||
export default serve({
|
||||
// ...
|
||||
mode: "fresh", // mode: "fresh" | "warm" | "cache" | "durable"
|
||||
});
|
||||
```
|
||||
|
||||
### Mode: `cache`
|
||||
|
||||
For the cache mode to work, you also need to specify the KV to be used. For this, use the
|
||||
`bindings` property:
|
||||
|
||||
```ts
|
||||
import { serve } from "bknd/adapter/cloudflare";
|
||||
|
||||
export default serve<Env>({
|
||||
// ...
|
||||
mode: "cache",
|
||||
bindings: ({ env }) => ({ kv: env.KV }),
|
||||
});
|
||||
```
|
||||
|
||||
## 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:
|
||||
@@ -178,9 +137,6 @@ D1 now supports to enable [global read replication](https://developers.cloudflar
|
||||
import { serve } from "bknd/adapter/cloudflare";
|
||||
|
||||
export default serve({
|
||||
// currently recommended to use "fresh" mode
|
||||
// otherwise consecutive requests will use the same bookmark
|
||||
mode: "fresh",
|
||||
// ...
|
||||
d1: {
|
||||
// enables D1 sessions
|
||||
@@ -207,7 +163,7 @@ The [Cloudflare Vite Plugin](https://developers.cloudflare.com/workers/vite-plug
|
||||
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";
|
||||
import { devFsVitePlugin } from "bknd/adapter/cloudflare";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [devFsVitePlugin()], // [!code highlight]
|
||||
@@ -217,7 +173,7 @@ export default defineConfig({
|
||||
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 { devFsWrite } from "bknd/adapter/cloudflare"; // [!code highlight]
|
||||
import { syncTypes } from "bknd/plugins";
|
||||
|
||||
export default {
|
||||
|
||||
@@ -19,7 +19,7 @@ Glad you're here! **bknd** is a lightweight, infrastructure agnostic and feature
|
||||
|
||||
Here is a preview of **bknd** in StackBlitz:
|
||||
|
||||
<StackBlitz {...examples.adminRich} />
|
||||
<StackBlitz path="github/bknd-io/bknd-demo" initialPath="/" />
|
||||
|
||||
<Accordions>
|
||||
<Accordion title="What's going on?">
|
||||
|
||||
@@ -24,7 +24,7 @@ bknd includes a fully featured MCP server that can be used to interact with the
|
||||
<source src="/content/mcp/v0.17_mcp_o.mp4" type="video/mp4" />
|
||||
</video>
|
||||
|
||||
Once enabled, you can access the MCP UI at `/mcp` or choose "MCP" from the top right user menu.
|
||||
Once enabled, you can access the MCP UI at `/mcp`, or choose "MCP" from the top right user menu.
|
||||
|
||||
## Enable MCP
|
||||
|
||||
@@ -54,7 +54,7 @@ The implementation is closely following the [MCP spec 2025-06-18](https://modelc
|
||||
import { McpClient } from "bknd/utils";
|
||||
|
||||
const client = new McpClient({
|
||||
url: "http://localhost:1337/mcp",
|
||||
url: "http://localhost:1337/api/system/mcp",
|
||||
});
|
||||
```
|
||||
|
||||
@@ -128,7 +128,7 @@ Pasting the following config into your Cursor `~/.cursor/mcp.json` file is the r
|
||||
{
|
||||
"mcpServers": {
|
||||
"bknd": {
|
||||
"url": "http://localhost:1337/mcp"
|
||||
"url": "http://localhost:1337/api/system/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,7 +155,7 @@ Add this to your VS Code MCP config. See [VS Code MCP docs](https://code.visuals
|
||||
"servers": {
|
||||
"bknd": {
|
||||
"type": "http",
|
||||
"url": "http://localhost:1337/mcp"
|
||||
"url": "http://localhost:1337/api/system/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,7 +188,7 @@ When using the Streamable HTTP transport, you can pass the `Authorization` heade
|
||||
|
||||
```typescript
|
||||
const client = new McpClient({
|
||||
url: "http://localhost:1337/mcp",
|
||||
url: "http://localhost:1337/api/system/mcp",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@ async function generateDocument({
|
||||
resources: ReturnType<Resource["toJSON"]>[];
|
||||
}) {
|
||||
return `---
|
||||
title: "MCP"
|
||||
title: "Tools & Resources"
|
||||
description: "Tools & Resources of the built-in full featured MCP server."
|
||||
tags: ["documentation"]
|
||||
---
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* We're using separate files, so that "wrangler" doesn't get bundled with your worker.
|
||||
*/
|
||||
|
||||
import { withPlatformProxy } from "bknd/adapter/cloudflare";
|
||||
import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy";
|
||||
import config from "./config.ts";
|
||||
|
||||
export default withPlatformProxy(config);
|
||||
|
||||
@@ -1,21 +1,7 @@
|
||||
import type { CloudflareBkndConfig } from "bknd/adapter/cloudflare";
|
||||
import { syncTypes } from "bknd/plugins";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
|
||||
const isDev = !import.meta.env.PROD;
|
||||
|
||||
export default {
|
||||
d1: {
|
||||
session: true,
|
||||
},
|
||||
options: {
|
||||
plugins: [
|
||||
syncTypes({
|
||||
enabled: isDev,
|
||||
write: async (et) => {
|
||||
await writeFile("bknd-types.d.ts", et.toString());
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
} satisfies CloudflareBkndConfig;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "bknd-cf-worker-example",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-02-04",
|
||||
"compatibility_date": "2025-08-03",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
"workers_dev": true,
|
||||
"minify": true,
|
||||
|
||||
Reference in New Issue
Block a user