Compare commits

...

13 Commits

Author SHA1 Message Date
dswbx 06d7558c3c feat: batch schema manager statements
run all schema modification queries in a single batch/transaction, to enable automatic rollbacks, and to stay within cloudflare's subrequest limits in free plan.
2025-09-24 14:48:45 +02:00
dswbx 832eb6ac31 feat: add migration to version 10 and update tests
introduced a new config migration to version 10, updated related tests to validate migration.
2025-09-24 09:58:22 +02:00
dswbx aa8bf156b0 fix: throw error if fetch called before build
added a check in the fetch getter to prevent its usage before the application is built, ensuring proper usage and avoiding runtime issues.
2025-09-24 09:53:28 +02:00
dswbx 83c1c86eff chore: update Bun version to 1.2.22 and bump package version to 0.18.0-rc.7 2025-09-23 14:00:08 +02:00
dswbx ffe53d3fb5 fix: ensure form updates with latest data after mutation
added a form reset to reflect up-to-date data changes and adjusted mutation to prevent id exclusion, ensuring lists are updated properly.
2025-09-23 13:47:10 +02:00
dswbx 49aee37199 feat: lazy load mcp server 2025-09-23 13:46:39 +02:00
dswbx 54eee8cd34 feat: add schema marking and validation skip mechanism 2025-09-23 13:43:37 +02:00
dswbx 5e62e681e7 feat: introduce DummyConnection class for testing purposes 2025-09-21 14:20:07 +02:00
dswbx 99c1645411 chore: bump version to 0.18.0-rc.6 and fix EntityTypescript for system entities 2025-09-21 14:16:57 +02:00
dswbx 564eab23af docs: improve CLI documentation formatting 2025-09-20 20:02:56 +02:00
dswbx f2da54c92b docs: enhance documentation with new modes and plugins
- Updated documentation to include new modes for configuring bknd (UI-only, Code-only, Hybrid).
- Introduced `syncSecrets` plugin example in the extending plugins documentation.
- Added `react-icons` dependency to package.json and package-lock.json.
- Enhanced various documentation pages with icons and improved content structure.
2025-09-20 19:57:38 +02:00
dswbx cd262097dc chore: bump version to 0.18.0-rc.5 2025-09-20 14:58:11 +02:00
dswbx d1726b23f1 fix: prevent rendering of hidden fields in EntityForm component 2025-09-20 14:51:30 +02:00
47 changed files with 1309 additions and 190 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
- name: Setup Bun - name: Setup Bun
uses: oven-sh/setup-bun@v1 uses: oven-sh/setup-bun@v1
with: with:
bun-version: "1.2.19" bun-version: "1.2.22"
- name: Install dependencies - name: Install dependencies
working-directory: ./app working-directory: ./app
+2
View File
@@ -16,6 +16,7 @@ describe("AppServer", () => {
mcp: { mcp: {
enabled: false, enabled: false,
path: "/api/system/mcp", path: "/api/system/mcp",
logLevel: "warning",
}, },
}); });
} }
@@ -38,6 +39,7 @@ describe("AppServer", () => {
mcp: { mcp: {
enabled: false, enabled: false,
path: "/api/system/mcp", path: "/api/system/mcp",
logLevel: "warning",
}, },
}); });
} }
+1
View File
@@ -44,6 +44,7 @@ describe("mcp auth", async () => {
}, },
}); });
await app.build(); await app.build();
await app.getMcpClient().ping();
server = app.mcp!; server = app.mcp!;
server.setLogLevel("error"); server.setLogLevel("error");
server.onNotification((message) => { server.onNotification((message) => {
+5
View File
@@ -34,6 +34,11 @@ describe("mcp", () => {
}); });
await app.build(); await app.build();
// expect mcp to not be loaded yet
expect(app.mcp).toBeNull();
// after first request, mcp should be loaded
await app.getMcpClient().listTools();
expect(app.mcp?.tools.length).toBeGreaterThan(0); expect(app.mcp?.tools.length).toBeGreaterThan(0);
}); });
}); });
+1
View File
@@ -50,6 +50,7 @@ describe("mcp data", async () => {
}, },
}); });
await app.build(); await app.build();
await app.getMcpClient().ping();
server = app.mcp!; server = app.mcp!;
server.setLogLevel("error"); server.setLogLevel("error");
server.onNotification((message) => { server.onNotification((message) => {
+1
View File
@@ -39,6 +39,7 @@ describe("mcp media", async () => {
}, },
}); });
await app.build(); await app.build();
await app.getMcpClient().ping();
server = app.mcp!; server = app.mcp!;
server.setLogLevel("error"); server.setLogLevel("error");
server.onNotification((message) => { server.onNotification((message) => {
+1
View File
@@ -24,6 +24,7 @@ describe("mcp system", async () => {
}, },
}); });
await app.build(); await app.build();
await app.getMcpClient().ping();
server = app.mcp!; server = app.mcp!;
}); });
+1
View File
@@ -23,6 +23,7 @@ describe("mcp system", async () => {
}, },
}); });
await app.build(); await app.build();
await app.getMcpClient().ping();
server = app.mcp!; server = app.mcp!;
}); });
+36 -1
View File
@@ -1,5 +1,5 @@
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, spyOn, test } from "bun:test";
import { randomString } from "core/utils"; import { randomString } from "core/utils";
import { Entity, EntityManager } from "data/entities"; import { Entity, EntityManager } from "data/entities";
import { TextField, EntityIndex } from "data/fields"; import { TextField, EntityIndex } from "data/fields";
@@ -268,4 +268,39 @@ describe("SchemaManager tests", async () => {
const diffAfter = await em.schema().getDiff(); const diffAfter = await em.schema().getDiff();
expect(diffAfter.length).toBe(0); expect(diffAfter.length).toBe(0);
}); });
test("returns statements", async () => {
const amount = 5;
const entities = new Array(amount)
.fill(0)
.map(() => new Entity(randomString(16), [new TextField("text")]));
const em = new EntityManager(entities, dummyConnection);
const statements = await em.schema().sync({ force: true });
expect(statements.length).toBe(amount);
expect(statements.every((stmt) => Object.keys(stmt).join(",") === "sql,parameters")).toBe(
true,
);
});
test("batches statements", async () => {
const { dummyConnection } = getDummyConnection();
const entities = new Array(20)
.fill(0)
.map(() => new Entity(randomString(16), [new TextField("text")]));
const em = new EntityManager(entities, dummyConnection);
const spy = spyOn(em.connection, "executeQueries");
const statements = await em.schema().sync();
expect(statements.length).toBe(entities.length);
expect(statements.every((stmt) => Object.keys(stmt).join(",") === "sql,parameters")).toBe(
true,
);
await em.schema().sync({ force: true });
expect(spy).toHaveBeenCalledTimes(1);
const tables = await em.connection.kysely
.selectFrom("sqlite_master")
.where("type", "=", "table")
.selectAll()
.execute();
expect(tables.length).toBe(entities.length + 1); /* 1+ for sqlite_sequence */
});
}); });
@@ -1,18 +1,22 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { type InitialModuleConfigs, createApp } from "../../../src"; import { App, type InitialModuleConfigs, createApp } from "/";
import { type Kysely, sql } from "kysely"; import { type Kysely, sql } from "kysely";
import { getDummyConnection } from "../../helper"; import { getDummyConnection } from "../../helper";
import v7 from "./samples/v7.json"; import v7 from "./samples/v7.json";
import v8 from "./samples/v8.json"; import v8 from "./samples/v8.json";
import v8_2 from "./samples/v8-2.json"; import v8_2 from "./samples/v8-2.json";
import v9 from "./samples/v9.json";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test"; import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(() => disableConsoleLog()); beforeAll(() => disableConsoleLog());
afterAll(enableConsoleLog); afterAll(enableConsoleLog);
// app expects migratable config to be present in database // app expects migratable config to be present in database
async function createVersionedApp(config: InitialModuleConfigs | any) { async function createVersionedApp(
config: InitialModuleConfigs | any,
opts?: { beforeCreateApp?: (db: Kysely<any>) => Promise<void> },
) {
const { dummyConnection } = getDummyConnection(); const { dummyConnection } = getDummyConnection();
if (!("version" in config)) throw new Error("config must have a version"); if (!("version" in config)) throw new Error("config must have a version");
@@ -38,6 +42,10 @@ async function createVersionedApp(config: InitialModuleConfigs | any) {
}) })
.execute(); .execute();
if (opts?.beforeCreateApp) {
await opts.beforeCreateApp(db);
}
const app = createApp({ const app = createApp({
connection: dummyConnection, connection: dummyConnection,
}); });
@@ -45,6 +53,19 @@ async function createVersionedApp(config: InitialModuleConfigs | any) {
return app; return app;
} }
async function getRawConfig(
app: App,
opts?: { version?: number; types?: ("config" | "diff" | "backup" | "secrets")[] },
) {
const db = app.em.connection.kysely;
return await db
.selectFrom("__bknd")
.selectAll()
.$if(!!opts?.version, (qb) => qb.where("version", "=", opts?.version))
.$if((opts?.types?.length ?? 0) > 0, (qb) => qb.where("type", "in", opts?.types))
.execute();
}
describe("Migrations", () => { describe("Migrations", () => {
/** /**
* updated auth strategies to have "enabled" prop * updated auth strategies to have "enabled" prop
@@ -82,4 +103,30 @@ describe("Migrations", () => {
// @ts-expect-error // @ts-expect-error
expect(app.toJSON(true).server.admin).toBeUndefined(); expect(app.toJSON(true).server.admin).toBeUndefined();
}); });
test("migration from 9 to 10", async () => {
expect(v9.version).toBe(9);
const app = await createVersionedApp(v9);
expect(app.version()).toBeGreaterThan(9);
// @ts-expect-error
expect(app.toJSON(true).media.adapter.config.secret_access_key).toBe(
"^^s3.secret_access_key^^",
);
const [config, secrets] = (await getRawConfig(app, {
version: 10,
types: ["config", "secrets"],
})) as any;
expect(config.json.auth.jwt.secret).toBe("");
expect(config.json.media.adapter.config.access_key).toBe("");
expect(config.json.media.adapter.config.secret_access_key).toBe("");
expect(secrets.json["auth.jwt.secret"]).toBe("^^jwt.secret^^");
expect(secrets.json["media.adapter.config.access_key"]).toBe("^^s3.access_key^^");
expect(secrets.json["media.adapter.config.secret_access_key"]).toBe(
"^^s3.secret_access_key^^",
);
});
}); });
@@ -0,0 +1,612 @@
{
"version": 9,
"server": {
"cors": {
"origin": "*",
"allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"],
"allow_headers": [
"Content-Type",
"Content-Length",
"Authorization",
"Accept"
],
"allow_credentials": true
},
"mcp": { "enabled": false, "path": "/api/system/mcp" }
},
"data": {
"basepath": "/api/data",
"default_primary_format": "integer",
"entities": {
"media": {
"type": "system",
"fields": {
"id": {
"type": "primary",
"config": {
"format": "integer",
"fillable": false,
"required": false
}
},
"path": { "type": "text", "config": { "required": true } },
"folder": {
"type": "boolean",
"config": {
"default_value": false,
"hidden": true,
"fillable": ["create"],
"required": false
}
},
"mime_type": { "type": "text", "config": { "required": false } },
"size": { "type": "number", "config": { "required": false } },
"scope": {
"type": "text",
"config": {
"hidden": true,
"fillable": ["create"],
"required": false
}
},
"etag": { "type": "text", "config": { "required": false } },
"modified_at": {
"type": "date",
"config": { "type": "datetime", "required": false }
},
"reference": { "type": "text", "config": { "required": false } },
"entity_id": { "type": "number", "config": { "required": false } },
"metadata": { "type": "json", "config": { "required": false } }
},
"config": { "sort_field": "id", "sort_dir": "asc" }
},
"users": {
"type": "system",
"fields": {
"id": {
"type": "primary",
"config": {
"format": "integer",
"fillable": false,
"required": false
}
},
"email": { "type": "text", "config": { "required": true } },
"strategy": {
"type": "enum",
"config": {
"options": { "type": "strings", "values": ["password"] },
"required": true,
"hidden": ["update", "form"],
"fillable": ["create"]
}
},
"strategy_value": {
"type": "text",
"config": {
"fillable": ["create"],
"hidden": ["read", "table", "update", "form"],
"required": true
}
},
"role": {
"type": "enum",
"config": {
"options": { "type": "strings", "values": ["admin", "guest"] },
"required": false
}
},
"age": {
"type": "enum",
"config": {
"options": {
"type": "strings",
"values": ["18-24", "25-34", "35-44", "45-64", "65+"]
},
"required": false
}
},
"height": { "type": "number", "config": { "required": false } },
"gender": {
"type": "enum",
"config": {
"options": { "type": "strings", "values": ["male", "female"] },
"required": false
}
}
},
"config": { "sort_field": "id", "sort_dir": "asc" }
},
"avatars": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"format": "integer",
"fillable": false,
"required": false
}
},
"identifier": { "type": "text", "config": { "required": false } },
"payload": {
"type": "json",
"config": { "required": false, "hidden": ["table"] }
},
"created_at": {
"type": "date",
"config": { "type": "datetime", "required": false }
},
"started_at": {
"type": "date",
"config": { "type": "datetime", "required": false }
},
"completed_at": {
"type": "date",
"config": { "type": "datetime", "required": false }
},
"input": {
"type": "media",
"config": {
"required": false,
"fillable": ["update"],
"hidden": false,
"mime_types": [],
"virtual": true,
"entity": "avatars"
}
},
"output": {
"type": "media",
"config": {
"required": false,
"fillable": ["update"],
"hidden": false,
"mime_types": [],
"virtual": true,
"entity": "avatars"
}
},
"users_id": {
"type": "relation",
"config": {
"label": "Users",
"required": false,
"reference": "users",
"target": "users",
"target_field": "id",
"target_field_type": "integer",
"on_delete": "set null"
}
}
},
"config": { "sort_field": "id", "sort_dir": "desc" }
},
"tryons": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"format": "integer",
"fillable": false,
"required": false
}
},
"created_at": {
"type": "date",
"config": { "type": "datetime", "required": false }
},
"completed_at": {
"type": "date",
"config": { "type": "datetime", "required": false }
},
"avatars_id": {
"type": "relation",
"config": {
"label": "Avatars",
"required": false,
"reference": "avatars",
"target": "avatars",
"target_field": "id",
"target_field_type": "integer",
"on_delete": "set null"
}
},
"users_id": {
"type": "relation",
"config": {
"label": "Users",
"required": false,
"reference": "users",
"target": "users",
"target_field": "id",
"target_field_type": "integer",
"on_delete": "set null"
}
},
"output": {
"type": "media",
"config": {
"required": false,
"fillable": ["update"],
"hidden": false,
"mime_types": [],
"virtual": true,
"entity": "tryons",
"max_items": 1
}
},
"products_id": {
"type": "relation",
"config": {
"label": "Products",
"required": false,
"reference": "products",
"target": "products",
"target_field": "id",
"target_field_type": "integer",
"on_delete": "set null"
}
},
"payload": {
"type": "json",
"config": { "required": false, "hidden": ["table"] }
}
},
"config": { "sort_field": "id", "sort_dir": "desc" }
},
"products": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"format": "integer",
"fillable": false,
"required": false
}
},
"enabled": { "type": "boolean", "config": { "required": false } },
"title": { "type": "text", "config": { "required": false } },
"url": { "type": "text", "config": { "required": false } },
"image": {
"type": "media",
"config": {
"required": false,
"fillable": ["update"],
"hidden": false,
"mime_types": [],
"virtual": true,
"entity": "products",
"max_items": 1
}
},
"created_at": {
"type": "date",
"config": { "type": "datetime", "required": false }
},
"sites_id": {
"type": "relation",
"config": {
"label": "Sites",
"required": false,
"reference": "sites",
"target": "sites",
"target_field": "id",
"target_field_type": "integer",
"on_delete": "set null"
}
},
"garment_type": {
"type": "enum",
"config": {
"options": {
"type": "strings",
"values": ["auto", "tops", "bottoms", "one-pieces"]
},
"required": false
}
}
},
"config": { "sort_field": "id", "sort_dir": "desc" }
},
"sites": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"format": "integer",
"fillable": false,
"required": false
}
},
"origin": {
"type": "text",
"config": {
"pattern": "^(https?):\\/\\/([a-zA-Z0-9.-]+)(:\\d+)?$",
"required": true
}
},
"name": { "type": "text", "config": { "required": false } },
"active": { "type": "boolean", "config": { "required": false } },
"logo": {
"type": "media",
"config": {
"required": false,
"fillable": ["update"],
"hidden": false,
"mime_types": [],
"virtual": true,
"entity": "sites",
"max_items": 1
}
},
"instructions": {
"type": "text",
"config": {
"html_config": {
"element": "textarea",
"props": { "rows": "2" }
},
"required": false,
"hidden": ["table"]
}
}
},
"config": { "sort_field": "id", "sort_dir": "desc" }
},
"sessions": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": { "format": "uuid", "fillable": false, "required": false }
},
"created_at": {
"type": "date",
"config": { "type": "datetime", "required": true }
},
"claimed_at": {
"type": "date",
"config": { "type": "datetime", "required": false }
},
"url": { "type": "text", "config": { "required": false } },
"sites_id": {
"type": "relation",
"config": {
"label": "Sites",
"required": false,
"reference": "sites",
"target": "sites",
"target_field": "id",
"target_field_type": "integer",
"on_delete": "set null"
}
},
"users_id": {
"type": "relation",
"config": {
"label": "Users",
"required": false,
"reference": "users",
"target": "users",
"target_field": "id",
"target_field_type": "integer",
"on_delete": "set null"
}
}
},
"config": { "sort_field": "id", "sort_dir": "desc" }
}
},
"relations": {
"poly_avatars_media_input": {
"type": "poly",
"source": "avatars",
"target": "media",
"config": { "mappedBy": "input" }
},
"poly_avatars_media_output": {
"type": "poly",
"source": "avatars",
"target": "media",
"config": { "mappedBy": "output" }
},
"n1_avatars_users": {
"type": "n:1",
"source": "avatars",
"target": "users",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": false,
"with_limit": 5
}
},
"n1_tryons_avatars": {
"type": "n:1",
"source": "tryons",
"target": "avatars",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": false,
"with_limit": 5
}
},
"n1_tryons_users": {
"type": "n:1",
"source": "tryons",
"target": "users",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": false,
"with_limit": 5
}
},
"poly_tryons_media_output": {
"type": "poly",
"source": "tryons",
"target": "media",
"config": { "mappedBy": "output", "targetCardinality": 1 }
},
"poly_products_media_image": {
"type": "poly",
"source": "products",
"target": "media",
"config": { "mappedBy": "image", "targetCardinality": 1 }
},
"n1_tryons_products": {
"type": "n:1",
"source": "tryons",
"target": "products",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": false,
"with_limit": 5
}
},
"poly_sites_media_logo": {
"type": "poly",
"source": "sites",
"target": "media",
"config": { "mappedBy": "logo", "targetCardinality": 1 }
},
"n1_sessions_sites": {
"type": "n:1",
"source": "sessions",
"target": "sites",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": false,
"with_limit": 5
}
},
"n1_sessions_users": {
"type": "n:1",
"source": "sessions",
"target": "users",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": false,
"with_limit": 5
}
},
"n1_products_sites": {
"type": "n:1",
"source": "products",
"target": "sites",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": false,
"with_limit": 5
}
}
},
"indices": {
"idx_unique_media_path": {
"entity": "media",
"fields": ["path"],
"unique": true
},
"idx_media_reference": {
"entity": "media",
"fields": ["reference"],
"unique": false
},
"idx_media_entity_id": {
"entity": "media",
"fields": ["entity_id"],
"unique": false
},
"idx_unique_users_email": {
"entity": "users",
"fields": ["email"],
"unique": true
},
"idx_users_strategy": {
"entity": "users",
"fields": ["strategy"],
"unique": false
},
"idx_users_strategy_value": {
"entity": "users",
"fields": ["strategy_value"],
"unique": false
},
"idx_sites_origin_active": {
"entity": "sites",
"fields": ["origin", "active"],
"unique": false
},
"idx_sites_active": {
"entity": "sites",
"fields": ["active"],
"unique": false
},
"idx_products_url": {
"entity": "products",
"fields": ["url"],
"unique": false
}
}
},
"auth": {
"enabled": true,
"basepath": "/api/auth",
"entity_name": "users",
"allow_register": true,
"jwt": {
"secret": "^^jwt.secret^^",
"alg": "HS256",
"expires": 999999999,
"issuer": "issuer",
"fields": ["id", "email", "role"]
},
"cookie": {
"path": "/",
"sameSite": "none",
"secure": true,
"httpOnly": true,
"expires": 604800,
"partitioned": false,
"renew": true,
"pathSuccess": "/admin",
"pathLoggedOut": "/"
},
"strategies": {
"password": {
"enabled": true,
"type": "password",
"config": { "hashing": "sha256" }
}
},
"guard": { "enabled": false },
"roles": {
"admin": { "implicit_allow": true },
"guest": { "is_default": true }
}
},
"media": {
"enabled": true,
"basepath": "/api/media",
"entity_name": "media",
"storage": { "body_max_size": 0 },
"adapter": {
"type": "s3",
"config": {
"access_key": "^^s3.access_key^^",
"secret_access_key": "^^s3.secret_access_key^^",
"url": "https://1234.r2.cloudflarestorage.com/bucket-name"
}
}
},
"flows": { "basepath": "/api/flows", "flows": {} }
}
+3 -3
View File
@@ -3,7 +3,7 @@
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"version": "0.18.0-rc.4", "version": "0.18.0-rc.7",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "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", "homepage": "https://bknd.io",
"repository": { "repository": {
@@ -13,7 +13,7 @@
"bugs": { "bugs": {
"url": "https://github.com/bknd-io/bknd/issues" "url": "https://github.com/bknd-io/bknd/issues"
}, },
"packageManager": "bun@1.2.19", "packageManager": "bun@1.2.22",
"engines": { "engines": {
"node": ">=22.13" "node": ">=22.13"
}, },
@@ -65,7 +65,7 @@
"hono": "4.8.3", "hono": "4.8.3",
"json-schema-library": "10.0.0-rc7", "json-schema-library": "10.0.0-rc7",
"json-schema-to-ts": "^3.1.1", "json-schema-to-ts": "^3.1.1",
"jsonv-ts": "0.8.2", "jsonv-ts": "0.8.4",
"kysely": "0.27.6", "kysely": "0.27.6",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"oauth4webapi": "^2.11.1", "oauth4webapi": "^2.11.1",
+7 -3
View File
@@ -244,6 +244,10 @@ export class App<
} }
get fetch(): Hono["fetch"] { get fetch(): Hono["fetch"] {
if (!this.isBuilt()) {
throw new Error("App is not built yet, run build() first");
}
return this.server.fetch as any; return this.server.fetch as any;
} }
@@ -302,13 +306,13 @@ export class App<
} }
getMcpClient() { getMcpClient() {
if (!this.mcp) { const config = this.modules.get("server").config.mcp;
if (!config.enabled) {
throw new Error("MCP is not enabled"); throw new Error("MCP is not enabled");
} }
const mcpPath = this.modules.get("server").config.mcp.path;
return new McpClient({ return new McpClient({
url: "http://localhost" + mcpPath, url: "http://localhost" + config.path,
fetch: this.server.request, fetch: this.server.request,
}); });
} }
@@ -5,8 +5,8 @@ import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test"; import { bunTestRunner } from "adapter/bun/test";
import { type CloudflareBkndConfig, createApp } from "./cloudflare-workers.adapter"; import { type CloudflareBkndConfig, createApp } from "./cloudflare-workers.adapter";
/* beforeAll(disableConsoleLog); beforeAll(disableConsoleLog);
afterAll(enableConsoleLog); */ afterAll(enableConsoleLog);
describe("cf adapter", () => { describe("cf adapter", () => {
const DB_URL = ":memory:"; const DB_URL = ":memory:";
+2 -2
View File
@@ -17,7 +17,7 @@ export async function serveStatic(server: Platform): Promise<MiddlewareHandler>
case "node": { case "node": {
const m = await import("@hono/node-server/serve-static"); const m = await import("@hono/node-server/serve-static");
const root = getRelativeDistPath() + "/static"; const root = getRelativeDistPath() + "/static";
$console.log("Serving static files from", root); $console.debug("Serving static files from", root);
return m.serveStatic({ return m.serveStatic({
// somehow different for node // somehow different for node
root, root,
@@ -27,7 +27,7 @@ export async function serveStatic(server: Platform): Promise<MiddlewareHandler>
case "bun": { case "bun": {
const m = await import("hono/bun"); const m = await import("hono/bun");
const root = path.resolve(getRelativeDistPath(), "static"); const root = path.resolve(getRelativeDistPath(), "static");
$console.log("Serving static files from", root); $console.debug("Serving static files from", root);
return m.serveStatic({ return m.serveStatic({
root, root,
onNotFound, onNotFound,
+34 -2
View File
@@ -12,6 +12,7 @@ export {
getMcpServer, getMcpServer,
stdioTransport, stdioTransport,
McpClient, McpClient,
logLevels as mcpLogLevels,
type McpClientConfig, type McpClientConfig,
type ToolAnnotation, type ToolAnnotation,
type ToolHandlerCtx, type ToolHandlerCtx,
@@ -21,8 +22,35 @@ export { secret, SecretSchema } from "./secret";
export { s }; export { s };
export const stripMark = <O extends object>(o: O): O => o; const symbol = Symbol("bknd-validation-mark");
export const mark = <O extends object>(o: O): O => o;
export function stripMark<O = any>(obj: O) {
const newObj = structuredClone(obj);
mark(newObj, false);
return newObj as O;
}
export function mark(obj: any, validated = true) {
try {
if (typeof obj === "object" && obj !== null && !Array.isArray(obj)) {
if (validated) {
obj[symbol] = true;
} else {
delete obj[symbol];
}
for (const key in obj) {
if (typeof obj[key] === "object" && obj[key] !== null) {
mark(obj[key], validated);
}
}
}
} catch (e) {}
}
export function isMarked(obj: any) {
if (typeof obj !== "object" || obj === null) return false;
return obj[symbol] === true;
}
export const stringIdentifier = s.string({ export const stringIdentifier = s.string({
pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$", pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
@@ -74,6 +102,10 @@ export function parse<S extends s.Schema, Options extends ParseOptions = ParseOp
v: unknown, v: unknown,
opts?: Options, opts?: Options,
): Options extends { coerce: true } ? s.StaticCoerced<S> : s.Static<S> { ): Options extends { coerce: true } ? s.StaticCoerced<S> : s.Static<S> {
if (!opts?.forceParse && !opts?.coerce && isMarked(v)) {
return v as any;
}
const schema = (opts?.clone ? cloneSchema(_schema as any) : _schema) as s.Schema; const schema = (opts?.clone ? cloneSchema(_schema as any) : _schema) as s.Schema;
let value = let value =
opts?.coerce !== false opts?.coerce !== false
+12
View File
@@ -230,3 +230,15 @@ export function customIntrospector<T extends Constructor<Dialect>>(
}, },
}; };
} }
export class DummyConnection extends Connection {
override name = "dummy";
constructor() {
super(undefined as any);
}
override getFieldSchema(): SchemaResponse {
throw new Error("Method not implemented.");
}
}
+1 -6
View File
@@ -34,7 +34,6 @@ export class EntityManager<TBD extends object = DefaultDB> {
private _entities: Entity[] = []; private _entities: Entity[] = [];
private _relations: EntityRelation[] = []; private _relations: EntityRelation[] = [];
private _indices: EntityIndex[] = []; private _indices: EntityIndex[] = [];
private _schema?: SchemaManager;
readonly emgr: EventManager<typeof EntityManager.Events>; readonly emgr: EventManager<typeof EntityManager.Events>;
static readonly Events = { ...MutatorEvents, ...RepositoryEvents }; static readonly Events = { ...MutatorEvents, ...RepositoryEvents };
@@ -249,11 +248,7 @@ export class EntityManager<TBD extends object = DefaultDB> {
} }
schema() { schema() {
if (!this._schema) { return new SchemaManager(this);
this._schema = new SchemaManager(this);
}
return this._schema;
} }
// @todo: centralize and add tests // @todo: centralize and add tests
@@ -0,0 +1,24 @@
import { describe, it, expect } from "bun:test";
import { EntityTypescript } from "./EntityTypescript";
import * as proto from "../prototype";
import { DummyConnection } from "../connection/Connection";
describe("EntityTypescript", () => {
it("should generate correct typescript for system entities", () => {
const schema = proto.em(
{
test: proto.entity("test", {
name: proto.text(),
}),
users: proto.systemEntity("users", {
name: proto.text(),
}),
},
({ relation }, { test, users }) => {
relation(test).manyToOne(users);
},
);
const et = new EntityTypescript(schema.proto.withConnection(new DummyConnection()));
expect(et.toString()).toContain('users?: DB["users"];');
});
});
+1 -1
View File
@@ -40,7 +40,7 @@ const systemEntities = {
export class EntityTypescript { export class EntityTypescript {
constructor( constructor(
protected em: EntityManager, protected em: EntityManager<any>,
protected _options: EntityTypescriptOptions = {}, protected _options: EntityTypescriptOptions = {},
) {} ) {}
+2 -2
View File
@@ -210,8 +210,8 @@ type SystemEntities = {
export function systemEntity< export function systemEntity<
E extends keyof SystemEntities, E extends keyof SystemEntities,
Fields extends Record<string, Field<any, any, any>>, Fields extends Record<string, Field<any, any, any>>,
>(name: E, fields: Fields) { >(name: E, fields: Fields, config?: EntityConfig) {
return entity<E, SystemEntities[E] & Fields>(name, fields as any); return entity<E, SystemEntities[E] & Fields>(name, fields as any, config, "system");
} }
export function relation<Local extends Entity>(local: Local) { export function relation<Local extends Entity>(local: Local) {
+17 -29
View File
@@ -247,20 +247,16 @@ export class SchemaManager {
async sync(config: { force?: boolean; drop?: boolean } = { force: false, drop: false }) { async sync(config: { force?: boolean; drop?: boolean } = { force: false, drop: false }) {
const diff = await this.getDiff(); const diff = await this.getDiff();
let updates: number = 0;
const statements: { sql: string; parameters: readonly unknown[] }[] = []; const statements: { sql: string; parameters: readonly unknown[] }[] = [];
const schema = this.em.connection.kysely.schema; const schema = this.em.connection.kysely.schema;
const qbs: { compile(): CompiledQuery; execute(): Promise<void> }[] = [];
for (const table of diff) { for (const table of diff) {
const qbs: { compile(): CompiledQuery; execute(): Promise<void> }[] = [];
let local_updates: number = 0;
const addFieldSchemas = this.collectFieldSchemas(table.name, table.columns.add); const addFieldSchemas = this.collectFieldSchemas(table.name, table.columns.add);
const dropFields = table.columns.drop; const dropFields = table.columns.drop;
const dropIndices = table.indices.drop; const dropIndices = table.indices.drop;
if (table.isDrop) { if (table.isDrop) {
updates++;
local_updates++;
if (config.drop) { if (config.drop) {
qbs.push(schema.dropTable(table.name)); qbs.push(schema.dropTable(table.name));
} }
@@ -268,8 +264,6 @@ export class SchemaManager {
let createQb = schema.createTable(table.name); let createQb = schema.createTable(table.name);
// add fields // add fields
for (const fieldSchema of addFieldSchemas) { for (const fieldSchema of addFieldSchemas) {
updates++;
local_updates++;
// @ts-ignore // @ts-ignore
createQb = createQb.addColumn(...fieldSchema); createQb = createQb.addColumn(...fieldSchema);
} }
@@ -280,8 +274,6 @@ export class SchemaManager {
if (addFieldSchemas.length > 0) { if (addFieldSchemas.length > 0) {
// add fields // add fields
for (const fieldSchema of addFieldSchemas) { for (const fieldSchema of addFieldSchemas) {
updates++;
local_updates++;
// @ts-ignore // @ts-ignore
qbs.push(schema.alterTable(table.name).addColumn(...fieldSchema)); qbs.push(schema.alterTable(table.name).addColumn(...fieldSchema));
} }
@@ -291,8 +283,6 @@ export class SchemaManager {
if (config.drop && dropFields.length > 0) { if (config.drop && dropFields.length > 0) {
// drop fields // drop fields
for (const column of dropFields) { for (const column of dropFields) {
updates++;
local_updates++;
qbs.push(schema.alterTable(table.name).dropColumn(column)); qbs.push(schema.alterTable(table.name).dropColumn(column));
} }
} }
@@ -310,35 +300,33 @@ export class SchemaManager {
qb = qb.unique(); qb = qb.unique();
} }
qbs.push(qb); qbs.push(qb);
local_updates++;
updates++;
} }
// drop indices // drop indices
if (config.drop) { if (config.drop) {
for (const index of dropIndices) { for (const index of dropIndices) {
qbs.push(schema.dropIndex(index)); qbs.push(schema.dropIndex(index));
local_updates++;
updates++;
} }
} }
}
if (local_updates === 0) continue; if (qbs.length > 0) {
statements.push(
...qbs.map((qb) => {
const { sql, parameters } = qb.compile();
return { sql, parameters };
}),
);
// iterate through built qbs $console.debug(
// @todo: run in batches "[SchemaManager]",
for (const qb of qbs) { `${qbs.length} statements\n${statements.map((stmt) => stmt.sql).join(";\n")}`,
const { sql, parameters } = qb.compile(); );
statements.push({ sql, parameters });
if (config.force) { try {
try { await this.em.connection.executeQueries(...qbs);
$console.debug("[SchemaManager]", sql); } catch (e) {
await qb.execute(); throw new Error(`Failed to execute batch: ${String(e)}`);
} catch (e) {
throw new Error(`Failed to execute query: ${sql}: ${(e as any).message}`);
}
}
} }
} }
+16 -3
View File
@@ -1,4 +1,12 @@
import { objectEach, transformObject, McpServer, type s, SecretSchema, setPath } from "bknd/utils"; import {
objectEach,
transformObject,
McpServer,
type s,
SecretSchema,
setPath,
mark,
} from "bknd/utils";
import { DebugLogger } from "core/utils/DebugLogger"; import { DebugLogger } from "core/utils/DebugLogger";
import { Guard } from "auth/authorize/Guard"; import { Guard } from "auth/authorize/Guard";
import { env } from "core/env"; import { env } from "core/env";
@@ -65,7 +73,7 @@ export type ModuleManagerOptions = {
// callback after server was created // callback after server was created
onServerInit?: (server: Hono<ServerEnv>) => void; onServerInit?: (server: Hono<ServerEnv>) => void;
// doesn't perform validity checks for given/fetched config // doesn't perform validity checks for given/fetched config
trustFetched?: boolean; skipValidation?: boolean;
// runs when initial config provided on a fresh database // runs when initial config provided on a fresh database
seed?: (ctx: ModuleBuildContext) => Promise<void>; seed?: (ctx: ModuleBuildContext) => Promise<void>;
// called right after modules are built, before finish // called right after modules are built, before finish
@@ -124,7 +132,12 @@ export class ModuleManager {
this.emgr = new EventManager({ ...ModuleManagerEvents }); this.emgr = new EventManager({ ...ModuleManagerEvents });
this.logger = new DebugLogger(debug_modules); this.logger = new DebugLogger(debug_modules);
this.createModules(options?.initial ?? {}); const config = options?.initial ?? {};
if (options?.skipValidation) {
mark(config, true);
}
this.createModules(config);
} }
protected onModuleConfigUpdated(key: string, config: any) {} protected onModuleConfigUpdated(key: string, config: any) {}
+5 -5
View File
@@ -205,7 +205,7 @@ export class DbModuleManager extends ModuleManager {
if (store_secrets) { if (store_secrets) {
updates.push({ updates.push({
version: state.configs.version, version: version,
type: "secrets", type: "secrets",
json: secrets as any, json: secrets as any,
}); });
@@ -252,7 +252,7 @@ export class DbModuleManager extends ModuleManager {
if (store_secrets) { if (store_secrets) {
if (!state.secrets || state.secrets?.version !== version) { if (!state.secrets || state.secrets?.version !== version) {
await this.mutator().insertOne({ await this.mutator().insertOne({
version: state.configs.version, version,
type: "secrets", type: "secrets",
json: secrets, json: secrets,
created_at: date, created_at: date,
@@ -380,8 +380,8 @@ export class DbModuleManager extends ModuleManager {
} }
} }
if (this.options?.trustFetched === true) { if (this.options?.skipValidation === true) {
this.logger.log("trusting fetched config (mark)"); this.logger.log("skipping validation (mark)");
mark(result.configs.json); mark(result.configs.json);
} }
@@ -393,7 +393,7 @@ export class DbModuleManager extends ModuleManager {
const version_before = this.version(); const version_before = this.version();
const [_version, _configs] = await migrate(version_before, result.configs.json, { const [_version, _configs] = await migrate(version_before, result.configs.json, {
db: this.db, db: this.db
}); });
this._version = _version; this._version = _version;
+8
View File
@@ -99,6 +99,14 @@ export const migrations: Migration[] = [
}; };
}, },
}, },
{
// remove secrets, automatic
// change media table `entity_id` from integer to text
version: 10,
up: async (config) => {
return config;
},
},
]; ];
export const CURRENT_VERSION = migrations[migrations.length - 1]?.version ?? 0; export const CURRENT_VERSION = migrations[migrations.length - 1]?.version ?? 0;
+1 -1
View File
@@ -6,7 +6,7 @@ import { getVersion } from "core/env";
export function getSystemMcp(app: App) { export function getSystemMcp(app: App) {
const middlewareServer = getMcpServer(app.server); const middlewareServer = getMcpServer(app.server);
const appConfig = app.modules.configs(); //const appConfig = app.modules.configs();
const { version, ...appSchema } = app.getSchema(); const { version, ...appSchema } = app.getSchema();
const schema = s.strictObject(appSchema); const schema = s.strictObject(appSchema);
const result = [...schema.walk({ maxDepth: 3 })]; const result = [...schema.walk({ maxDepth: 3 })];
+5 -1
View File
@@ -1,6 +1,6 @@
import { Exception } from "core/errors"; import { Exception } from "core/errors";
import { isDebug } from "core/env"; import { isDebug } from "core/env";
import { $console, s } from "bknd/utils"; import { $console, mcpLogLevels, s } from "bknd/utils";
import { $object } from "modules/mcp"; import { $object } from "modules/mcp";
import { cors } from "hono/cors"; import { cors } from "hono/cors";
import { Module } from "modules/Module"; import { Module } from "modules/Module";
@@ -25,6 +25,10 @@ export const serverConfigSchema = $object(
mcp: s.strictObject({ mcp: s.strictObject({
enabled: s.boolean({ default: false }), enabled: s.boolean({ default: false }),
path: s.string({ default: "/api/system/mcp" }), path: s.string({ default: "/api/system/mcp" }),
logLevel: s.string({
enum: mcpLogLevels,
default: "warning",
}),
}), }),
}, },
{ {
+31 -23
View File
@@ -70,33 +70,41 @@ export class SystemController extends Controller {
this.registerMcp(); this.registerMcp();
this._mcpServer = getSystemMcp(app);
this._mcpServer.onNotification((message) => {
if (message.method === "notification/message") {
const consoleMap = {
emergency: "error",
alert: "error",
critical: "error",
error: "error",
warning: "warn",
notice: "log",
info: "info",
debug: "debug",
};
const level = consoleMap[message.params.level];
if (!level) return;
$console[level]("MCP notification", message.params.message ?? message.params);
}
});
app.server.use( app.server.use(
mcpMiddleware({ mcpMiddleware({
server: this._mcpServer, setup: async () => {
if (!this._mcpServer) {
this._mcpServer = getSystemMcp(app);
this._mcpServer.onNotification((message) => {
if (message.method === "notification/message") {
const consoleMap = {
emergency: "error",
alert: "error",
critical: "error",
error: "error",
warning: "warn",
notice: "log",
info: "info",
debug: "debug",
};
const level = consoleMap[message.params.level];
if (!level) return;
$console[level](
"MCP notification",
message.params.message ?? message.params,
);
}
});
}
return {
server: this._mcpServer,
};
},
sessionsEnabled: true, sessionsEnabled: true,
debug: { debug: {
logLevel: "debug", logLevel: config.mcp.logLevel as any,
explainEndpoint: true, explainEndpoint: true,
}, },
endpoint: { endpoint: {
+1
View File
@@ -156,6 +156,7 @@ export const useEntityQuery = <
// mutate all keys of entity by default // mutate all keys of entity by default
if (options?.revalidateOnMutate !== false) { if (options?.revalidateOnMutate !== false) {
// don't use the id, to also update lists
await mutateFn(); await mutateFn();
} }
return res; return res;
@@ -121,6 +121,8 @@ export function EntityForm({
return custom; return custom;
} }
} }
if (field.isHidden(action)) return;
return ( return (
<EntityFormField <EntityFormField
field={field} field={field}
@@ -77,6 +77,9 @@ function DataEntityUpdateImpl({ params }) {
message: `Successfully updated ID ${entityId}`, message: `Successfully updated ID ${entityId}`,
color: "green", color: "green",
}); });
// make sure form picks up the latest data
Form.reset();
} catch (e) { } catch (e) {
setError(e instanceof Error ? e.message : "Failed to update"); setError(e instanceof Error ? e.message : "Failed to update");
} }
+3 -3
View File
@@ -15,7 +15,7 @@
}, },
"app": { "app": {
"name": "bknd", "name": "bknd",
"version": "0.18.0-rc.4", "version": "0.18.0-rc.6",
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"dependencies": { "dependencies": {
"@cfworker/json-schema": "^4.1.1", "@cfworker/json-schema": "^4.1.1",
@@ -35,7 +35,7 @@
"hono": "4.8.3", "hono": "4.8.3",
"json-schema-library": "10.0.0-rc7", "json-schema-library": "10.0.0-rc7",
"json-schema-to-ts": "^3.1.1", "json-schema-to-ts": "^3.1.1",
"jsonv-ts": "0.8.2", "jsonv-ts": "0.8.4",
"kysely": "0.27.6", "kysely": "0.27.6",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"oauth4webapi": "^2.11.1", "oauth4webapi": "^2.11.1",
@@ -2529,7 +2529,7 @@
"jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="],
"jsonv-ts": ["jsonv-ts@0.8.2", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-1Z7+maCfoGGqBPu5vN8rU9gIsW7OatYmn+STBTPkybbtNqeMzAoJDDrXHjsZ89x5dPH9W+OgMpNLtN0ouwiMYg=="], "jsonv-ts": ["jsonv-ts@0.8.4", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-TZOyAVGBZxHuzk09NgJCx2dbeh0XqVWVKHU1PtIuvjT9XO7zhvAD02RcVisJoUdt2rJNt3zlyeNQ2b8MMPc+ug=="],
"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=="], "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=="],
@@ -116,6 +116,36 @@ export default {
} satisfies BkndConfig; } satisfies BkndConfig;
``` ```
### `syncSecrets`
A simple plugin that writes down the app secrets on boot and each build.
```typescript title="bknd.config.ts"
import { syncSecrets } from "bknd/plugins";
import { writeFile } from "node:fs/promises";
export default {
options: {
plugins: [
syncSecrets({
// whether to enable the plugin, make sure to disable in production
enabled: true,
// your writing function (required)
write: async (secrets) => {
// apply your writing logic, e.g. writing a template file in an env format
await writeFile(
".env.example",
Object.entries(secrets)
.map(([key]) => `${key}=`)
.join("\n")
);
},
}),
]
},
} satisfies BkndConfig;
```
### `showRoutes` ### `showRoutes`
A simple plugin that logs the routes of your app in the console. A simple plugin that logs the routes of your app in the console.
+32 -11
View File
@@ -6,29 +6,34 @@ tags: ["documentation"]
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import { examples } from "@/app/_components/StackBlitz"; import { examples } from "@/app/_components/StackBlitz";
import { SquareMousePointer, Code, Blend } from 'lucide-react';
Glad you're here! **bknd** is a lightweight, infrastructure agnostic and feature-rich backend that runs in any JavaScript environment. Glad you're here! **bknd** is a lightweight, infrastructure agnostic and feature-rich backend that runs in any JavaScript environment.
- Instant backend with full REST API - Instant backend with full REST API
- Built on Web Standards for maximum compatibility - Built on Web Standards for maximum compatibility
- Multiple run modes (standalone, runtime, framework) - Multiple ready-made [integrations](/integration/introduction) (standalone, runtime, framework)
- Official API and React SDK with type-safety - Official [API SDK](/usage/sdk) and [React SDK](/usage/react) with type-safety
- React elements for auto-configured authentication and media components - [React elements](/usage/elements) for auto-configured authentication and media components
- Built-in [MCP server](/usage/mcp/overview) for controlling your backend
- Multiple run [modes](/usage/introduction#modes) (ui-only, code-only, hybrid)
## Preview ## Preview
Here is a preview of **bknd** in StackBlitz: Here is a preview of **bknd** in StackBlitz:
<StackBlitz path="github/bknd-io/bknd-demo" initialPath="/" /> <Card className="p-0 pb-1">
<StackBlitz path="github/bknd-io/bknd-demo" initialPath="/" />
<Accordions className="m-1">
<Accordion title="What's going on?">
The example shown is starting a [node server](/integration/node) using an [in-memory database](/usage/database#sqlite-in-memory). To ensure there are a few entities defined, it is using an [initial structure](/usage/database#initial-structure) using the prototype methods. Furthermore it uses the [seed option](/usage/database#seeding-the-database) to seed some data in the structure created.
<Accordions> To ensure there are users defined on first boot, it hooks into the `App.Events.AppFirstBoot` event to create them (documentation pending).
<Accordion title="What's going on?">
The example shown is starting a [node server](/integration/node) using an [in-memory database](/usage/database#sqlite-in-memory). To ensure there are a few entities defined, it is using an [initial structure](/usage/database#initial-structure) using the prototype methods. Furthermore it uses the [seed option](/usage/database#seeding-the-database) to seed some data in the structure created.
To ensure there are users defined on first boot, it hooks into the `App.Events.AppFirstBoot` event to create them (documentation pending). </Accordion>
</Accordions>
</Accordion> </Card>
</Accordions>
## Quickstart ## Quickstart
@@ -156,3 +161,19 @@ The following databases are currently supported. Request a new integration if yo
Create a new issue to request a new database integration. Create a new issue to request a new database integration.
</Card> </Card>
</Cards> </Cards>
## Choose a mode
**bknd** supports multiple modes to suit your needs.
<Cards className="grid-cols-3">
<Card title="UI-only" href="/usage/introduction#ui-only-mode" icon={<SquareMousePointer className="text-fd-primary !size-6" />}>
Configure your backend and manage your data visually with the built-in Admin UI.
</Card>
<Card title="Code-only" href="/usage/introduction#code-only-mode" icon={<Code className="text-fd-primary !size-6" />}>
Configure your backend programmatically with a Drizzle-like API, manage your data with the Admin UI.
</Card>
<Card title="Hybrid" href="/usage/introduction#hybrid-mode" icon={<Blend className="text-fd-primary !size-6" />}>
Configure your backend visually while in development, use a read-only configuration in production.
</Card>
</Cards>
+88 -13
View File
@@ -1,23 +1,22 @@
--- ---
title: "Using the CLI" title: "Using the CLI"
description: "How to start a bknd instance using the CLI." description: "How to start a bknd instance using the CLI."
icon: Terminal
tags: ["documentation"] tags: ["documentation"]
--- ---
The bknd package includes a command-line interface (CLI) that allows you to run a bknd instance and perform various tasks. The bknd package includes a command-line interface (CLI) that allows you to run a bknd instance and perform various tasks.
``` ```sh
npx bknd npx bknd
``` ```
Here is the output: Here is the output:
``` ```sh
$ npx bknd $ npx bknd
Usage: bknd [options] [command] Usage: bknd [options] [command]
⚡ bknd cli v0.17.0
Options: Options:
-V, --version output the version number -V, --version output the version number
-h, --help display help for command -h, --help display help for command
@@ -40,7 +39,7 @@ Commands:
To see all available `run` options, execute `npx bknd run --help`. To see all available `run` options, execute `npx bknd run --help`.
``` ```sh
$ npx bknd run --help $ npx bknd run --help
Usage: bknd run [options] Usage: bknd run [options]
@@ -98,7 +97,7 @@ The `app` function is useful if you need a cross-platform way to access the envi
If you're using `npx bknd run`, make sure to create a file in a file format that `node` can load, otherwise you may run into an error that the file couldn't be found: If you're using `npx bknd run`, make sure to create a file in a file format that `node` can load, otherwise you may run into an error that the file couldn't be found:
```sh ```
[INF] 2025-03-28 18:02:21 Using config from bknd.config.ts [INF] 2025-03-28 18:02:21 Using config from bknd.config.ts
[ERR] 2025-03-28 18:02:21 Failed to load config: Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'bknd.config.ts' imported from [...] [ERR] 2025-03-28 18:02:21 Failed to load config: Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'bknd.config.ts' imported from [...]
at packageResolve (node:internal/modules/esm/resolve:857:9) at packageResolve (node:internal/modules/esm/resolve:857:9)
@@ -123,7 +122,7 @@ npx tsx node_modules/.bin/bknd run
To start an instance with a Turso/LibSQL database, run the following: To start an instance with a Turso/LibSQL database, run the following:
``` ```sh
npx bknd run --db-url libsql://your-db.turso.io --db-token <your-token> npx bknd run --db-url libsql://your-db.turso.io --db-token <your-token>
``` ```
@@ -133,7 +132,7 @@ The `--db-token` option is optional and only required if the database is protect
To start an instance with an ephemeral in-memory database, run the following: To start an instance with an ephemeral in-memory database, run the following:
``` ```sh
npx bknd run --memory npx bknd run --memory
``` ```
@@ -143,7 +142,7 @@ Keep in mind that the database is not persisted and will be lost when the proces
To see all available `types` options, execute `npx bknd types --help`. To see all available `types` options, execute `npx bknd types --help`.
``` ```sh
$ npx bknd types --help $ npx bknd types --help
Usage: bknd types [options] Usage: bknd types [options]
@@ -157,13 +156,13 @@ Options:
To generate types for the database, run the following: To generate types for the database, run the following:
``` ```sh
npx bknd types npx bknd types
``` ```
This will generate types for your database schema in `bknd-types.d.ts`. The generated file could look like this: This will generate types for your database schema in `bknd-types.d.ts`. The generated file could look like this:
```typescript bknd-types.d.ts ```typescript title="bknd-types.d.ts"
import type { DB } from "bknd"; import type { DB } from "bknd";
import type { Insertable, Selectable, Updateable, Generated } from "kysely"; import type { Insertable, Selectable, Updateable, Generated } from "kysely";
@@ -190,7 +189,7 @@ declare module "bknd" {
Make sure to add the generated file in your `tsconfig.json` file: Make sure to add the generated file in your `tsconfig.json` file:
```json tsconfig.json ```json title="tsconfig.json"
{ {
"include": ["bknd-types.d.ts"] "include": ["bknd-types.d.ts"]
} }
@@ -204,5 +203,81 @@ import type { DB } from "bknd";
type Todo = DB["todos"]; type Todo = DB["todos"];
``` ```
All bknd methods that involve your database schema will be automatically typed. All bknd methods that involve your database schema will be automatically typed. You may use the [`syncTypes`](/extending/plugins/#synctypes) plugin to automatically write the types to a file.
## Getting the configuration (`config`)
To see all available `config` options, execute `npx bknd config --help`.
```sh
$ npx bknd config --help
Usage: bknd config [options]
get app config
Options:
-c, --config <config> config file
--db-url <db> database url, can be any valid sqlite url
--pretty pretty print
--default use default config
--secrets include secrets in output
--out <file> output file
-h, --help display help for command
```
To get the configuration of your app, and to write it to a file, run the following:
```sh
npx bknd config --out appconfig.json
```
To get a template configuration instead, run the following:
```sh
npx bknd config --default
```
To automatically sync your configuration to a file, you may also use the [`syncConfig`](/extending/plugins/#syncconfig) plugin.
## Getting the secrets (`secrets`)
To see all available `secrets` options, execute `npx bknd secrets --help`.
```sh
$ npx bknd secrets --help
Usage: bknd secrets [options]
get app secrets
Options:
-c, --config <config> config file
--db-url <db> database url, can be any valid sqlite url
--template template output without the actual secrets
--format <format> format output (choices: "json", "env", default:
"json")
--out <file> output file
-h, --help display help for command
```
To automatically sync your secrets to a file, you may also use the [`syncSecrets`](/extending/plugins/#syncsecrets) plugin.
## Syncing the database (`sync`)
Sync your database can be useful when running in [`code`](/usage/introduction/#code-only-mode) mode. When you're ready to deploy, you can point to the production configuration and sync the database. Schema mutations are only applied when running with the `--force` option.
```bash
$ npx bknd sync --help
Usage: bknd sync [options]
sync database
Options:
-c, --config <config> config file
--db-url <db> database url, can be any valid sqlite url
--force perform database syncing operations
--drop include destructive DDL operations
--out <file> output file
--sql use sql output
-h, --help display help for command
```
@@ -1,6 +1,7 @@
--- ---
title: "Database" title: "Database"
description: "Choosing the right database configuration" description: "Choosing the right database configuration"
icon: Database
tags: ["documentation"] tags: ["documentation"]
--- ---
@@ -1,6 +1,7 @@
--- ---
title: "React Elements" title: "React Elements"
description: "Speed up your frontend development" description: "Speed up your frontend development"
icon: Box
tags: ["documentation"] tags: ["documentation"]
--- ---
@@ -1,9 +1,13 @@
--- ---
title: "Introduction" title: "Introduction"
description: "Setting up bknd" description: "Setting up bknd"
icon: Pin
tags: ["documentation"] tags: ["documentation"]
--- ---
import { TypeTable } from "fumadocs-ui/components/type-table";
import { SquareMousePointer, Code, Blend } from 'lucide-react';
There are several methods to get **bknd** up and running. You can choose between these options: There are several methods to get **bknd** up and running. You can choose between these options:
1. [Run it using the CLI](/usage/cli): That's the easiest and fastest way to get started. 1. [Run it using the CLI](/usage/cli): That's the easiest and fastest way to get started.
@@ -22,12 +26,12 @@ Regardless of the method you choose, at the end all adapters come down to the ac
instantiation of the `App`, which in raw looks like this: instantiation of the `App`, which in raw looks like this:
```typescript ```typescript
import { createApp, type CreateAppConfig } from "bknd"; import { createApp, type BkndConfig } from "bknd";
// create the app // create the app
const config = { const config = {
/* ... */ /* ... */
} satisfies CreateAppConfig; } satisfies BkndConfig;
const app = createApp(config); const app = createApp(config);
// build the app // build the app
@@ -40,13 +44,153 @@ export default app;
In Web API compliant environments, all you have to do is to default exporting the app, as it In Web API compliant environments, all you have to do is to default exporting the app, as it
implements the `Fetch` API. implements the `Fetch` API.
## Configuration (`CreateAppConfig`) ## Modes
The `CreateAppConfig` type is the main configuration object for the `createApp` function. It has Main project goal is to provide a backend that can be configured visually with the built-in Admin UI. However, you may instead want to configure your backend programmatically, and define your data structure with a Drizzle-like API:
<Cards className="grid-cols-1 sm:grid-cols-2 md:grid-cols-3">
<Card title="UI-only" href="/usage/introduction#ui-only-mode" icon={<SquareMousePointer className="text-fd-primary !size-6" />}>
This is the default mode, it allows visual configuration and saves the configuration to the database. Expects you to deploy your backend separately from your frontend.
</Card>
<Card title="Code-only" href="/usage/introduction#code-only-mode" icon={<Code className="text-fd-primary !size-6" />}>
This mode allows you to configure your backend programmatically, and define your data structure with a Drizzle-like API. Visual configuration controls are disabled.
</Card>
<Card title={"Hybrid"} href="/usage/introduction#hybrid-mode" icon={<Blend className="text-fd-primary !size-6" />}>
This mode allows you to configure your backend visually while in development, and uses the produced configuration in a code-only mode for maximum performance.
</Card>
</Cards>
In the following sections, we'll cover the different modes in more detail. The configuration properties involved are the following:
```typescript title="bknd.config.ts"
import type { BkndConfig } from "bknd";
export default {
config: { /* ... */ }
options: {
mode: "db", // or "code"
manager: {
secrets: { /* ... */ },
storeSecrets: true,
},
}
} satisfies BkndConfig;
```
<TypeTable type={{
config: {
description: "The initial configuration when `mode` is `\"db\"`, and as the produced configuration when `mode` is `\"code\"`.",
type: "object",
properties: {
/* ... */
}
},
["options.mode"]: {
description: "The options for the app.",
type: '"db" | "code"',
default: '"db"'
},
["options.manager.secrets"]: {
description: "The app secrets to be provided when using `\"db\"` mode. This is required since secrets are extracted and stored separately to the database.",
type: "object",
properties: {
/* ... */
}
},
["options.manager.storeSecrets"]: {
description: "Whether to store secrets in the database when using `\"db\"` mode.",
type: "boolean",
default: "true"
}
}} />
### UI-only mode
This mode is the default mode. It allows you to configure your backend visually with the built-in Admin UI. It expects that you deploy your backend separately from your frontend, and make changes there. No configuration is needed, however, if you want to provide an initial configuration, you can do so by passing a `config` object.
```typescript
import type { BkndConfig } from "bknd";
export default {
// this will only be applied if the database is empty
config: { /* ... */ },
} satisfies BkndConfig;
```
### Code-only mode
This mode allows you to configure your backend programmatically, and define your data structure with a Drizzle-like API. Visual configuration controls are disabled.
```typescript title="bknd.config.ts"
import { type BkndConfig, em, entity, text, boolean } from "bknd";
import { secureRandomString } from "bknd/utils";
const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
export default {
// example configuration
config: {
data: schema.toJSON(),
auth: {
enabled: true,
jwt: {
secret: secureRandomString(64),
},
}
},
options: {
// this ensures that the provided configuration is always used
mode: "code",
},
} satisfies BkndConfig;
```
### Hybrid mode
This mode allows you to configure your backend visually while in development, and uses the produced configuration in a code-only mode for maximum performance. It gives you the best of both worlds.
While in development, we set the mode to `"db"` where the configuration is stored in the database. When it's time to deploy, we export the configuration, and set the mode to `"code"`. While in `"db"` mode, the `config` property interprets the value as an initial configuration to use when the database is empty.
```typescript title="bknd.config.ts"
import type { BkndConfig } from "bknd";
// import your produced configuration
import appConfig from "./appconfig.json" with { type: "json" };
export default {
config: appConfig,
options: {
mode: process.env.NODE_ENV === "development" ? "db" : "code",
manager: {
secrets: process.env
}
},
} satisfies BkndConfig;
```
To keep your config, secrets and types in sync, you can either use the CLI or the plugins.
| Type | Plugin | CLI Command |
|----------------|-----------------------------------------------------------------------|----------------------------|
| Configuration | [`syncConfig`](/extending/plugins/#syncconfig) | [`config`](/usage/cli/#getting-the-configuration-config) |
| Secrets | [`syncSecrets`](/extending/plugins/#syncsecrets) | [`secrets`](/usage/cli/#getting-the-secrets-secrets) |
| Types | [`syncTypes`](/extending/plugins/#synctypes) | [`types`](/usage/cli/#generating-types-types) |
## Configuration (`BkndConfig`)
The `BkndConfig` type is the main configuration object for the `createApp` function. It has
the following properties: the following properties:
```typescript ```typescript
import type { App, InitialModuleConfigs, ModuleBuildContext, Connection } from "bknd"; import type { App, InitialModuleConfigs, ModuleBuildContext, Connection, MaybePromise } from "bknd";
import type { Config } from "@libsql/client"; import type { Config } from "@libsql/client";
type AppPlugin = (app: App) => Promise<void> | void; type AppPlugin = (app: App) => Promise<void> | void;
@@ -57,13 +201,19 @@ type ManagerOptions = {
seed?: (ctx: ModuleBuildContext) => Promise<void>; seed?: (ctx: ModuleBuildContext) => Promise<void>;
}; };
type CreateAppConfig = { type BkndConfig<Args = any> = {
connection?: Connection | Config; connection?: Connection | Config;
initialConfig?: InitialModuleConfigs; config?: InitialModuleConfigs;
options?: { options?: {
plugins?: AppPlugin[]; plugins?: AppPlugin[];
manager?: ManagerOptions; manager?: ManagerOptions;
}; };
app?: BkndConfig<Args> | ((args: Args) => MaybePromise<BkndConfig<Args>>);
onBuilt?: (app: App) => Promise<void>;
beforeBuild?: (app?: App) => Promise<void>;
buildConfig?: {
sync?: boolean;
}
}; };
``` ```
@@ -72,18 +222,31 @@ type CreateAppConfig = {
The `connection` property is the main connection object to the database. It can be either an object with libsql config or the actual `Connection` class. The `connection` property is the main connection object to the database. It can be either an object with libsql config or the actual `Connection` class.
```ts ```ts
const connection = { // uses the default SQLite connection depending on the runtime
url: "<url>", const connection = { url: "<url>" };
authToken: "<token>",
}; // the same as above, but more explicit
import { sqlite } from "bknd/adapter/sqlite";
const connection = sqlite({ url: "<url>" });
// Node.js SQLite, default on Node.js
import { nodeSqlite } from "bknd/adapter/node";
const connection = nodeSqlite({ url: "<url>" });
// Bun SQLite, default on Bun
import { bunSqlite } from "bknd/adapter/bun";
const connection = bunSqlite({ url: "<url>" });
// LibSQL, default on Cloudflare
import { libsql } from "bknd";
const connection = libsql({ url: "<url>" });
``` ```
Alternatively, you can pass an instance of a `Connection` class directly, See a full list of available connections in the [Database](/usage/database) section. Alternatively, you can pass an instance of a `Connection` class directly, see [Custom Connection](/usage/database#custom-connection) as a reference.
see [Custom Connection](/usage/database#custom-connection) as a reference.
If the connection object is omitted, the app will try to use an in-memory database. If the connection object is omitted, the app will try to use an in-memory database.
### `initialConfig` ### `config`
As [initial configuration](/usage/database#initial-structure), you can either pass a partial configuration object or a complete one As [initial configuration](/usage/database#initial-structure), you can either pass a partial configuration object or a complete one
with a version number. The version number is used to automatically migrate the configuration up with a version number. The version number is used to automatically migrate the configuration up
@@ -157,10 +320,10 @@ to the latest version upon boot. The default configuration looks like this:
} }
``` ```
You can use the CLI to get the default configuration: You can use the [CLI](/usage/cli/#getting-the-configuration-config) to get the default configuration:
```sh ```sh
npx bknd config --pretty npx bknd config --default --pretty
``` ```
To validate your configuration against a JSON schema, you can also dump the schema using the CLI: To validate your configuration against a JSON schema, you can also dump the schema using the CLI:
@@ -191,6 +354,10 @@ make sure to only run trusted ones.
### `options.seed` ### `options.seed`
<Callout type="info">
The seed function will only be executed on app's first boot in `"db"` mode. If a configuration already exists in the database, or in `"code"` mode, it will not be executed.
</Callout>
The `seed` property is a function that is called when the app is booted for the first time. It is used to seed the database with initial data. The function is passed a `ModuleBuildContext` object: The `seed` property is a function that is called when the app is booted for the first time. It is used to seed the database with initial data. The function is passed a `ModuleBuildContext` object:
```ts ```ts
@@ -211,18 +378,3 @@ const seed = async (ctx: ModuleBuildContext) => {
}; };
``` ```
### `options.manager`
This object is passed to the `ModuleManager` which is responsible for:
- validating and maintaining configuration of all modules
- building all modules (data, auth, media, flows)
- maintaining the `ModuleBuildContext` used by the modules
The `options.manager` object has the following properties:
- `basePath` (`string`): The base path for the Hono instance. This is used to prefix all routes.
- `trustFetched` (`boolean`): If set to `true`, the app will not perform any validity checks for
the given or fetched configuration.
- `onFirstBoot` (`() => Promise<void>`): A function that is called when the app is booted for
the first time.
@@ -1,4 +1,5 @@
{ {
"title": "MCP", "title": "MCP",
"icon": "Mcp",
"pages": ["overview", "tools-resources"] "pages": ["overview", "tools-resources"]
} }
@@ -1,5 +1,5 @@
--- ---
title: "MCP" title: "Tools & Resources"
description: "Tools & Resources of the built-in full featured MCP server." description: "Tools & Resources of the built-in full featured MCP server."
tags: ["documentation"] tags: ["documentation"]
--- ---
@@ -1,6 +1,7 @@
--- ---
title: "SDK (React)" title: "SDK (React)"
description: "Use the bknd SDK for React" description: "Use the bknd SDK for React"
icon: React
tags: ["documentation"] tags: ["documentation"]
--- ---
@@ -1,6 +1,7 @@
--- ---
title: "SDK (TypeScript)" title: "SDK (TypeScript)"
description: "Use the bknd SDK in TypeScript" description: "Use the bknd SDK in TypeScript"
icon: TypeScript
tags: ["documentation"] tags: ["documentation"]
--- ---
+25
View File
@@ -0,0 +1,25 @@
import { icons } from "lucide-react";
import { TbBrandReact, TbBrandTypescript } from "react-icons/tb";
const McpIcon = () => (
<svg
fill="currentColor"
fillRule="evenodd"
height="1em"
style={{ flex: "none", lineHeight: "1" }}
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<title>ModelContextProtocol</title>
<path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z" />
<path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z" />
</svg>
);
export default {
...icons,
React: TbBrandReact,
TypeScript: TbBrandTypescript,
Mcp: McpIcon,
};
+20 -14
View File
@@ -1,23 +1,29 @@
import { loader } from "fumadocs-core/source"; import { loader } from "fumadocs-core/source";
import { docs } from "@/.source"; import { docs } from "@/.source";
import { createOpenAPI, attachFile } from "fumadocs-openapi/server"; import { createOpenAPI, attachFile } from "fumadocs-openapi/server";
import { icons } from "lucide-react"; import icons from "./icons";
import { createElement } from "react"; import { createElement } from "react";
import { TbBrandReact, TbBrandTypescript } from "react-icons/tb";
const add_icons = {
TypeScript: TbBrandTypescript,
React: TbBrandReact,
};
export const source = loader({ export const source = loader({
baseUrl: "/", baseUrl: "/",
source: docs.toFumadocsSource(), source: docs.toFumadocsSource(),
pageTree: { pageTree: {
// adds a badge to each page item in page tree // adds a badge to each page item in page tree
attachFile, attachFile,
}, },
icon(icon) { icon(icon) {
if (!icon) { if (!icon) {
// You may set a default icon // You may set a default icon
return; return;
} }
if (icon in icons) return createElement(icons[icon as keyof typeof icons]); if (icon in icons) return createElement(icons[icon as keyof typeof icons]);
}, },
}); });
export const openapi = createOpenAPI(); export const openapi = createOpenAPI();
+10
View File
@@ -23,6 +23,7 @@
"next": "15.3.5", "next": "15.3.5",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"twoslash": "^0.3.2" "twoslash": "^0.3.2"
}, },
@@ -10352,6 +10353,15 @@
"react": "^16.8.0 || ^17 || ^18 || ^19" "react": "^16.8.0 || ^17 || ^18 || ^19"
} }
}, },
"node_modules/react-icons": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
"license": "MIT",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+1
View File
@@ -31,6 +31,7 @@
"next": "15.3.5", "next": "15.3.5",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"twoslash": "^0.3.2" "twoslash": "^0.3.2"
}, },
+24 -30
View File
@@ -1,11 +1,5 @@
import { remarkInstall } from "fumadocs-docgen"; import { remarkInstall } from "fumadocs-docgen";
import { import { defineConfig, defineDocs, frontmatterSchema, metaSchema } from "fumadocs-mdx/config";
defineConfig,
defineDocs,
frontmatterSchema,
metaSchema,
} from "fumadocs-mdx/config";
import { transformerTwoslash } from "fumadocs-twoslash"; import { transformerTwoslash } from "fumadocs-twoslash";
import { createFileSystemTypesCache } from "fumadocs-twoslash/cache-fs"; import { createFileSystemTypesCache } from "fumadocs-twoslash/cache-fs";
import { rehypeCodeDefaultOptions } from "fumadocs-core/mdx-plugins"; import { rehypeCodeDefaultOptions } from "fumadocs-core/mdx-plugins";
@@ -14,31 +8,31 @@ import { remarkAutoTypeTable } from "fumadocs-typescript";
// You can customise Zod schemas for frontmatter and `meta.json` here // You can customise Zod schemas for frontmatter and `meta.json` here
// see https://fumadocs.vercel.app/docs/mdx/collections#define-docs // see https://fumadocs.vercel.app/docs/mdx/collections#define-docs
export const docs = defineDocs({ export const docs = defineDocs({
docs: { docs: {
schema: frontmatterSchema, schema: frontmatterSchema,
}, },
meta: { meta: {
schema: metaSchema, schema: metaSchema,
}, },
}); });
export default defineConfig({ export default defineConfig({
mdxOptions: { mdxOptions: {
remarkPlugins: [remarkInstall, remarkAutoTypeTable], remarkPlugins: [remarkInstall, remarkAutoTypeTable],
rehypeCodeOptions: { rehypeCodeOptions: {
lazy: true, lazy: true,
experimentalJSEngine: true, experimentalJSEngine: true,
langs: ["ts", "js", "html", "tsx", "mdx"], langs: ["ts", "js", "html", "tsx", "mdx"],
themes: { themes: {
light: "light-plus", light: "light-plus",
dark: "dark-plus", dark: "dark-plus",
},
transformers: [
...(rehypeCodeDefaultOptions.transformers ?? []),
transformerTwoslash({
typesCache: createFileSystemTypesCache(),
}),
],
}, },
transformers: [ },
...(rehypeCodeDefaultOptions.transformers ?? []),
transformerTwoslash({
typesCache: createFileSystemTypesCache(),
}),
],
},
},
}); });