Compare commits

..

17 Commits

Author SHA1 Message Date
dswbx 8c26d646bd fix env test, prefer source over process 2025-11-11 14:02:43 +01:00
dswbx 4859b99954 feat: remove cloudflare adapter dependency to nodejs_compat
- adjusted bindings extraction to not rely on node inspect anymore
- added new vite export that uses nodejs apis
2025-11-11 13:33:04 +01:00
dswbx 3f55c0f2c5 fix imports and adjust biome and linting configuration
remove unused `SecretSchema` import, adjust biome settings to modify linting behavior, and clean formatting in `slugify` and other functions.
2025-11-11 13:29:24 +01:00
dswbx 9ec31373e1 Merge pull request #295 from bknd-io/fix/hybrid-schema-sync
fix hybrid schema sync
2025-11-10 10:36:52 +01:00
dswbx 70b25a9f9b hybrid: fix timing for automatic schema syncs to work with plugins 2025-11-10 10:34:50 +01:00
dswbx 300d86ff7c Merge pull request #293 from bknd-io/chore/biome-upgrade
upgrade biome and vscode settings
2025-11-07 09:49:22 +01:00
dswbx 4c9b662f6f fix bknd/util imports 2025-11-07 09:47:43 +01:00
dswbx a862cfdcf1 add .vscode/settings.json 2025-11-07 09:45:12 +01:00
dswbx de62f4e729 upgrade biome config 2025-11-07 09:39:02 +01:00
dswbx 4575d89cfe Merge pull request #290 from bknd-io/chore/upgrade-deps
upgrade deps
2025-11-05 10:03:36 +01:00
dswbx 0ca0828581 fix examples 2025-11-05 09:37:49 +01:00
dswbx 07a57ebf67 fix cli build script, add commander, fix types 2025-11-05 08:27:47 +01:00
dswbx e9f1241ec3 fix tests: remove .only 2025-11-05 08:20:36 +01:00
dswbx 80903d4ffa upgrade bun, remove hoisting 2025-11-05 08:15:41 +01:00
dswbx c8290802e9 upgrade biome, typescript, icons, other dev deps. also keep hoisting for now 2025-11-05 07:48:33 +01:00
dswbx ac6cd4a900 upgrade vite + fix types 2025-10-31 21:40:55 +01:00
dswbx a91ddb1ec3 update deps 2025-10-31 21:33:11 +01:00
38 changed files with 1294 additions and 910 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.22" bun-version: "1.3.1"
- name: Install dependencies - name: Install dependencies
working-directory: ./app working-directory: ./app
+2 -1
View File
@@ -27,7 +27,8 @@ packages/media/.env
.npmrc .npmrc
/.verdaccio /.verdaccio
.idea .idea
.vscode .vscode/*
!.vscode/settings.json
.git_old .git_old
docker/tmp docker/tmp
.debug .debug
+19
View File
@@ -0,0 +1,19 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"biome.enabled": true,
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit",
"source.fixAll.biome": "explicit"
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.autoImportFileExcludePatterns": [
"**/dist/**",
"**/node_modules/**/dist/**",
"**/node_modules/**/!(src|lib|esm)/**" // optional, stricter
],
"typescript.preferences.includePackageJsonAutoImports": "on",
"typescript.tsserver.watchOptions": {
"excludeDirectories": ["**/dist", "**/node_modules/**/dist"]
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"typescript.preferences.includePackageJsonAutoImports": "off",
"typescript.suggest.autoImports": true,
"typescript.preferences.importModuleSpecifier": "relative",
"search.exclude": {
"**/dist/**": true,
"**/node_modules/**": true
},
"files.exclude": {
"**/dist/**": true
}
}
+1 -1
View File
@@ -108,7 +108,7 @@ describe("App tests", async () => {
expect(Array.from(app.plugins.keys())).toEqual(["test"]); expect(Array.from(app.plugins.keys())).toEqual(["test"]);
}); });
test.only("drivers", async () => { test("drivers", async () => {
const called: string[] = []; const called: string[] = [];
const app = new App(dummyConnection, undefined, { const app = new App(dummyConnection, undefined, {
drivers: { drivers: {
+1 -1
View File
@@ -15,7 +15,7 @@ const mockedBackend = new Hono()
.get("/file/:name", async (c) => { .get("/file/:name", async (c) => {
const { name } = c.req.param(); const { name } = c.req.param();
const file = Bun.file(`${assetsPath}/${name}`); const file = Bun.file(`${assetsPath}/${name}`);
return new Response(file, { return new Response(new File([await file.bytes()], name, { type: file.type }), {
headers: { headers: {
"Content-Type": file.type, "Content-Type": file.type,
"Content-Length": file.size.toString(), "Content-Length": file.size.toString(),
+1 -1
View File
@@ -76,7 +76,7 @@ describe("repros", async () => {
expect(app.em.entities.map((e) => e.name)).toEqual(["media", "test"]); expect(app.em.entities.map((e) => e.name)).toEqual(["media", "test"]);
}); });
test.only("verify inversedBy", async () => { test("verify inversedBy", async () => {
const schema = proto.em( const schema = proto.em(
{ {
products: proto.entity("products", { products: proto.entity("products", {
-20
View File
@@ -1,30 +1,10 @@
import pkg from "./package.json" with { type: "json" }; import pkg from "./package.json" with { type: "json" };
import c from "picocolors"; import c from "picocolors";
import { formatNumber } from "bknd/utils"; import { formatNumber } from "bknd/utils";
import * as esbuild from "esbuild";
const deps = Object.keys(pkg.dependencies); const deps = Object.keys(pkg.dependencies);
const external = ["jsonv-ts/*", "wrangler", "bknd", "bknd/*", ...deps]; const external = ["jsonv-ts/*", "wrangler", "bknd", "bknd/*", ...deps];
if (process.env.DEBUG) {
const result = await esbuild.build({
entryPoints: ["./src/cli/index.ts"],
outdir: "./dist/cli",
platform: "node",
minify: true,
format: "esm",
metafile: true,
bundle: true,
external,
define: {
__isDev: "0",
__version: JSON.stringify(pkg.version),
},
});
await Bun.write("./dist/cli/metafile-esm.json", JSON.stringify(result.metafile, null, 2));
process.exit(0);
}
const result = await Bun.build({ const result = await Bun.build({
entrypoints: ["./src/cli/index.ts"], entrypoints: ["./src/cli/index.ts"],
target: "node", target: "node",
+13 -2
View File
@@ -1,3 +1,4 @@
/** biome-ignore-all lint/suspicious/noConsole: . */
import { $ } from "bun"; import { $ } from "bun";
import * as tsup from "tsup"; import * as tsup from "tsup";
import pkg from "./package.json" with { type: "json" }; import pkg from "./package.json" with { type: "json" };
@@ -96,6 +97,7 @@ async function buildApi() {
metafile: true, metafile: true,
target: "esnext", target: "esnext",
platform: "browser", platform: "browser",
removeNodeProtocol: false,
format: ["esm"], format: ["esm"],
splitting: false, splitting: false,
loader: { loader: {
@@ -228,6 +230,7 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
outDir: `dist/adapter/${adapter}`, outDir: `dist/adapter/${adapter}`,
metafile: true, metafile: true,
splitting: false, splitting: false,
removeNodeProtocol: false,
onSuccess: async () => { onSuccess: async () => {
delayTypes(); delayTypes();
oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built")); oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built"));
@@ -265,7 +268,7 @@ async function buildAdapters() {
tsup.build(baseConfig("react-router")), tsup.build(baseConfig("react-router")),
tsup.build( tsup.build(
baseConfig("bun", { baseConfig("bun", {
external: [/^bun\:.*/], external: [/^bun:.*/],
}), }),
), ),
tsup.build(baseConfig("astro")), tsup.build(baseConfig("astro")),
@@ -284,6 +287,14 @@ async function buildAdapters() {
external: [/bknd/, "wrangler", "node:process"], external: [/bknd/, "wrangler", "node:process"],
}), }),
), ),
tsup.build(
baseConfig("cloudflare/vite", {
target: "esnext",
entry: ["src/adapter/cloudflare/vite.ts"],
outDir: "dist/adapter/cloudflare",
metafile: false,
}),
),
tsup.build({ tsup.build({
...baseConfig("vite"), ...baseConfig("vite"),
@@ -320,7 +331,7 @@ async function buildAdapters() {
entry: ["src/adapter/sqlite/bun.ts"], entry: ["src/adapter/sqlite/bun.ts"],
outDir: "dist/adapter/sqlite", outDir: "dist/adapter/sqlite",
metafile: false, metafile: false,
external: [/^bun\:.*/], external: [/^bun:.*/],
}), }),
]); ]);
} }
-6
View File
@@ -1,6 +0,0 @@
[install]
#registry = "http://localhost:4873"
[test]
coverageSkipTestFiles = true
console.depth = 10
+59 -49
View File
@@ -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.22", "packageManager": "bun@1.3.1",
"engines": { "engines": {
"node": ">=22.13" "node": ">=22.13"
}, },
@@ -49,93 +49,98 @@
"license": "FSL-1.1-MIT", "license": "FSL-1.1-MIT",
"dependencies": { "dependencies": {
"@cfworker/json-schema": "^4.1.1", "@cfworker/json-schema": "^4.1.1",
"@codemirror/lang-html": "^6.4.9", "@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.2",
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
"@hono/swagger-ui": "^0.5.1", "@hono/swagger-ui": "^0.5.2",
"@mantine/core": "^7.17.1", "@mantine/core": "^7.17.1",
"@mantine/hooks": "^7.17.1", "@mantine/hooks": "^7.17.1",
"@tanstack/react-form": "^1.0.5", "@tanstack/react-form": "^1.0.5",
"@uiw/react-codemirror": "^4.23.10", "@uiw/react-codemirror": "^4.25.2",
"@xyflow/react": "^12.4.4", "@xyflow/react": "^12.9.2",
"aws4fetch": "^1.0.20", "aws4fetch": "^1.0.20",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.3",
"dayjs": "^1.11.13", "dayjs": "^1.11.19",
"fast-xml-parser": "^5.0.8", "fast-xml-parser": "^5.3.1",
"hono": "4.8.3", "hono": "4.10.4",
"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.9.1", "jsonv-ts": "0.9.3",
"kysely": "0.27.6", "kysely": "0.28.8",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"oauth4webapi": "^2.11.1", "oauth4webapi": "^2.11.1",
"object-path-immutable": "^4.1.2", "object-path-immutable": "^4.1.2",
"radix-ui": "^1.1.3",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"swr": "^2.3.3" "radix-ui": "^1.1.3",
"swr": "^2.3.6",
"use-sync-external-store": "^1.6.0",
"zustand": "^4"
}, },
"devDependencies": { "devDependencies": {
"@aws-sdk/client-s3": "^3.758.0", "@aws-sdk/client-s3": "^3.922.0",
"@bluwy/giget-core": "^0.1.2", "@bluwy/giget-core": "^0.1.6",
"@clack/prompts": "^0.11.0", "@clack/prompts": "^0.11.0",
"@cloudflare/vitest-pool-workers": "^0.9.3", "@cloudflare/vitest-pool-workers": "^0.10.4",
"@cloudflare/workers-types": "^4.20250606.0", "@cloudflare/workers-types": "^4.20251014.0",
"@dagrejs/dagre": "^1.1.4", "@dagrejs/dagre": "^1.1.4",
"@hono/vite-dev-server": "^0.21.0", "@hono/vite-dev-server": "^0.23.0",
"@hookform/resolvers": "^4.1.3", "@hookform/resolvers": "^5.2.2",
"@libsql/client": "^0.15.9", "@libsql/client": "^0.15.15",
"@mantine/modals": "^7.17.1", "@mantine/modals": "^7.17.1",
"@mantine/notifications": "^7.17.1", "@mantine/notifications": "^7.17.1",
"@playwright/test": "^1.51.1", "@playwright/test": "^1.56.1",
"@rjsf/core": "5.22.2", "@rjsf/core": "5.22.2",
"@rjsf/utils": "5.22.0",
"@standard-schema/spec": "^1.0.0", "@standard-schema/spec": "^1.0.0",
"@tabler/icons-react": "3.18.0", "@tabler/icons-react": "3.35.0",
"@tailwindcss/postcss": "^4.0.12", "@tailwindcss/postcss": "^4.1.16",
"@tailwindcss/vite": "^4.0.12", "@tailwindcss/vite": "^4.1.16",
"@tanstack/react-store": "^0.8.0",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0", "@testing-library/react": "^16.2.0",
"@types/node": "^22.13.10", "@types/node": "^24.10.0",
"@types/react": "^19.0.10", "@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^5.1.0",
"@vitest/coverage-v8": "^3.0.9", "@vitest/coverage-v8": "3.0.9",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "^16.4.7", "commander": "^14.0.2",
"dotenv": "^17.2.3",
"jotai": "^2.12.2", "jotai": "^2.12.2",
"jsdom": "^26.0.0", "jsdom": "^26.1.0",
"kysely-d1": "^0.3.0",
"kysely-generic-sqlite": "^1.2.1", "kysely-generic-sqlite": "^1.2.1",
"libsql": "^0.5.22",
"libsql-stateless-easy": "^1.8.0", "libsql-stateless-easy": "^1.8.0",
"open": "^10.1.0", "miniflare": "^4.20251011.2",
"open": "^10.2.0",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"postcss-preset-mantine": "^1.17.0", "postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"posthog-js-lite": "^3.4.2", "posthog-js-lite": "^3.6.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.66.0",
"react-icons": "5.2.1", "react-icons": "5.5.0",
"react-json-view-lite": "^2.4.1", "react-json-view-lite": "^2.5.0",
"sql-formatter": "^15.4.11", "sql-formatter": "^15.6.10",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.12", "tailwindcss": "^4.1.16",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tsc-alias": "^1.8.11", "tsc-alias": "^1.8.16",
"tsup": "^8.4.0", "tsup": "^8.5.0",
"tsx": "^4.19.3", "tsx": "^4.20.6",
"uuid": "^11.1.0", "uuid": "^13.0.0",
"vite": "^6.3.5", "vite": "^7.1.12",
"vite-plugin-circular-dependency": "^0.5.0", "vite-plugin-circular-dependency": "^0.5.0",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.9", "vitest": "3.0.9",
"wouter": "^3.6.0", "wouter": "^3.7.1",
"wrangler": "^4.37.1", "wrangler": "^4.45.4"
"miniflare": "^4.20250913.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@hono/node-server": "^1.14.3" "@hono/node-server": "^1.19.6"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=19", "react": ">=19",
@@ -209,6 +214,11 @@
"import": "./dist/adapter/cloudflare/proxy.js", "import": "./dist/adapter/cloudflare/proxy.js",
"require": "./dist/adapter/cloudflare/proxy.js" "require": "./dist/adapter/cloudflare/proxy.js"
}, },
"./adapter/cloudflare/vite": {
"types": "./dist/types/adapter/cloudflare/vite.d.ts",
"import": "./dist/adapter/cloudflare/vite.js",
"require": "./dist/adapter/cloudflare/vite.js"
},
"./adapter": { "./adapter": {
"types": "./dist/types/adapter/index.d.ts", "types": "./dist/types/adapter/index.d.ts",
"import": "./dist/adapter/index.js" "import": "./dist/adapter/index.js"
+3 -2
View File
@@ -5,7 +5,6 @@ import type { em as prototypeEm } from "data/prototype";
import { Connection } from "data/connection/Connection"; import { Connection } from "data/connection/Connection";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { import {
type InitialModuleConfigs,
type ModuleConfigs, type ModuleConfigs,
type Modules, type Modules,
ModuleManager, ModuleManager,
@@ -381,8 +380,10 @@ export class App<
if (results.length > 0) { if (results.length > 0) {
for (const { name, result } of results) { for (const { name, result } of results) {
if (result) { if (result) {
$console.log(`[Plugin:${name}] schema`);
ctx.helper.ensureSchema(result); ctx.helper.ensureSchema(result);
if (ctx.flags.sync_required) {
$console.log(`[Plugin:${name}] schema, sync required`);
}
} }
} }
} }
+3 -5
View File
@@ -1,14 +1,12 @@
/// <reference types="bun-types" />
import path from "node:path"; import path from "node:path";
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter"; import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
import { registerLocalMediaAdapter } from "."; import { registerLocalMediaAdapter } from ".";
import { config, type App } from "bknd"; import { config, type App } from "bknd";
import type { ServeOptions } from "bun";
import { serveStatic } from "hono/bun"; import { serveStatic } from "hono/bun";
type BunEnv = Bun.Env; type BunEnv = Bun.Env;
export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOptions, "fetch">; export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> &
Omit<Bun.Serve.Options<undefined, string>, "fetch">;
export async function createApp<Env = BunEnv>( export async function createApp<Env = BunEnv>(
{ distPath, serveStatic: _serveStatic, ...config }: BunBkndConfig<Env> = {}, { distPath, serveStatic: _serveStatic, ...config }: BunBkndConfig<Env> = {},
@@ -60,7 +58,7 @@ export function serve<Env = BunEnv>(
args: Env = Bun.env as Env, args: Env = Bun.env as Env,
) { ) {
Bun.serve({ Bun.serve({
...serveOptions, ...(serveOptions as any),
port, port,
fetch: createHandler( fetch: createHandler(
{ {
+31 -14
View File
@@ -1,5 +1,3 @@
import { inspect } from "node:util";
export type BindingTypeMap = { export type BindingTypeMap = {
D1Database: D1Database; D1Database: D1Database;
KVNamespace: KVNamespace; KVNamespace: KVNamespace;
@@ -10,21 +8,40 @@ export type BindingTypeMap = {
export type GetBindingType = keyof BindingTypeMap; export type GetBindingType = keyof BindingTypeMap;
export type BindingMap<T extends GetBindingType> = { key: string; value: BindingTypeMap[T] }; export type BindingMap<T extends GetBindingType> = { key: string; value: BindingTypeMap[T] };
export const bindingMatchers = {
// D1Database instance doesn't stringify to [object D1Database] (yet)
D1Database: (value: object): value is D1Database => {
if (typeof value !== "object" || value === null) return false;
if (String(value) === "[object D1Database]") return true;
if (Object.keys(value).length !== 2) return false;
return ["alwaysPrimarySession", "fetcher"].every((key) => key in value);
},
KVNamespace: (value: object): value is KVNamespace =>
String(value) === "[object KvNamespace]" && Object.keys(value).length === 0,
DurableObjectNamespace: (value: object): value is DurableObjectNamespace =>
String(value) === "[object DurableObjectNamespace]" && Object.keys(value).length === 0,
R2Bucket: (value: object): value is R2Bucket =>
String(value) === "[object R2Bucket]" && Object.keys(value).length === 0,
};
export function getBindings<T extends GetBindingType>(env: any, type: T): BindingMap<T>[] { export function getBindings<T extends GetBindingType>(env: any, type: T): BindingMap<T>[] {
const bindings: BindingMap<T>[] = []; const bindings: BindingMap<T>[] = [];
if (!(type in bindingMatchers)) {
console.error(`No binding matcher for type ${type}`);
return [];
}
const matcher = bindingMatchers[type];
for (const key in env) { for (const key in env) {
try { const value = env[key];
if ( if (typeof value !== "object" || value === null) continue;
(env[key] as any).constructor.name === type || if (!matcher(value)) continue;
String(env[key]) === `[object ${type}]` ||
inspect(env[key]).includes(type) bindings.push({
) { key,
bindings.push({ value,
key, } as any);
value: env[key] as BindingTypeMap[T],
});
}
} catch (e) {}
} }
return bindings; return bindings;
} }
@@ -0,0 +1,51 @@
import { describe, afterAll, test, expect, beforeAll } from "vitest";
import { Miniflare } from "miniflare";
import { getBindings, type GetBindingType } from "./bindings";
let mf: Miniflare | undefined;
beforeAll(() => {
mf = new Miniflare({
modules: true,
script: `import { DurableObject } from "cloudflare:workers";
export default { async fetch() { return new Response(null); } }
export class TestObject extends DurableObject {}
export class TestObject2 extends DurableObject {}`,
r2Buckets: ["BUCKET"],
d1Databases: ["DB"],
kvNamespaces: ["KV"],
durableObjects: {
DO_SQLITE: { className: "TestObject", useSQLite: true },
DO_KV: { className: "TestObject2" },
},
});
});
afterAll(() => {
mf?.dispose();
});
describe("bindings", () => {
test("extracts", async () => {
const env = {
...(await mf!.getBindings()),
some: "string",
another: 123,
array: [1, 2, 3],
};
const $getBindingKeys = (type: GetBindingType) => {
const bindings = getBindings(env, type);
const keys = bindings?.map((b) => b.key);
if (!keys || keys.length === 0) {
console.error(`No ${type} binding found`, { bindings });
throw new Error(`No ${type} binding found`);
}
return keys;
};
expect($getBindingKeys("D1Database")).toEqual(["DB"]);
expect($getBindingKeys("KVNamespace")).toEqual(["KV"]);
expect($getBindingKeys("DurableObjectNamespace")).toEqual(["DO_SQLITE", "DO_KV"]);
expect($getBindingKeys("R2Bucket")).toEqual(["BUCKET"]);
});
});
+6 -2
View File
@@ -16,10 +16,14 @@ export {
type GetBindingType, type GetBindingType,
type BindingMap, type BindingMap,
} from "./bindings"; } from "./bindings";
export { constants, makeConfig, type CloudflareContext } from "./config"; export {
constants,
makeConfig,
type CloudflareContext,
registerAsyncsExecutionContext,
} from "./config";
export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter"; export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter";
export { registries } from "bknd"; export { registries } from "bknd";
export { devFsVitePlugin, devFsWrite } from "./vite";
// for compatibility with old code // for compatibility with old code
export function d1<DB extends D1Database | D1DatabaseSession = D1Database>( export function d1<DB extends D1Database | D1DatabaseSession = D1Database>(
@@ -16,6 +16,7 @@ export function registerMedia(
env: Record<string, any>, env: Record<string, any>,
registries: typeof $registries = $registries, registries: typeof $registries = $registries,
) { ) {
if (registries.media.has("r2")) return;
const r2_bindings = getBindings(env, "R2Bucket"); const r2_bindings = getBindings(env, "R2Bucket");
registries.media.register( registries.media.register(
@@ -172,7 +173,7 @@ export class StorageR2Adapter extends StorageAdapter {
await this.bucket.delete(this.getKey(key)); await this.bucket.delete(this.getKey(key));
} }
getObjectUrl(key: string): string { getObjectUrl(): string {
throw new Error("Method getObjectUrl not implemented."); throw new Error("Method getObjectUrl not implemented.");
} }
@@ -183,7 +184,7 @@ export class StorageR2Adapter extends StorageAdapter {
return key; return key;
} }
toJSON(secrets?: boolean) { toJSON() {
return { return {
type: this.getName(), type: this.getName(),
config: {}, config: {},
+1 -2
View File
@@ -1,3 +1,4 @@
// biome-ignore-all lint: .
import type { Plugin } from "vite"; import type { Plugin } from "vite";
import { writeFile as nodeWriteFile } from "node:fs/promises"; import { writeFile as nodeWriteFile } from "node:fs/promises";
import { resolve } from "node:path"; import { resolve } from "node:path";
@@ -48,7 +49,6 @@ export function devFsVitePlugin({
// Skip our own debug output // Skip our own debug output
if (output.includes("[dev-fs-plugin]") || output.includes("[dev-fs-polyfill]")) { if (output.includes("[dev-fs-plugin]") || output.includes("[dev-fs-polyfill]")) {
// @ts-ignore // @ts-ignore
// biome-ignore lint/style/noArguments: <explanation>
return originalStdoutWrite.apply(process.stdout, arguments); return originalStdoutWrite.apply(process.stdout, arguments);
} }
@@ -178,7 +178,6 @@ export function devFsVitePlugin({
} }
// @ts-ignore // @ts-ignore
// biome-ignore lint:
return originalStdoutWrite.apply(process.stdout, arguments); return originalStdoutWrite.apply(process.stdout, arguments);
}; };
+4 -1
View File
@@ -1,5 +1,6 @@
import { createFrameworkApp, type FrameworkBkndConfig } from "bknd/adapter"; import { createFrameworkApp, type FrameworkBkndConfig } from "bknd/adapter";
import { isNode } from "bknd/utils"; import { isNode } from "bknd/utils";
// @ts-expect-error next is not installed
import type { NextApiRequest } from "next"; import type { NextApiRequest } from "next";
type NextjsEnv = NextApiRequest["env"]; type NextjsEnv = NextApiRequest["env"];
@@ -18,7 +19,9 @@ function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequ
if (!cleanRequest) return req; if (!cleanRequest) return req;
const url = new URL(req.url); const url = new URL(req.url);
cleanRequest?.searchParams?.forEach((k) => url.searchParams.delete(k)); cleanRequest?.searchParams?.forEach((k) => {
url.searchParams.delete(k);
});
if (isNode()) { if (isNode()) {
return new Request(url.toString(), { return new Request(url.toString(), {
+1 -1
View File
@@ -4,7 +4,7 @@ import { resendEmail } from "./resend";
const ALL_TESTS = !!process.env.ALL_TESTS; const ALL_TESTS = !!process.env.ALL_TESTS;
describe.skipIf(ALL_TESTS)("resend", () => { describe.skipIf(ALL_TESTS)("resend", () => {
it.only("should throw on failed", async () => { it("should throw on failed", async () => {
const driver = resendEmail({ apiKey: "invalid" } as any); const driver = resendEmail({ apiKey: "invalid" } as any);
expect(driver.send("foo@bar.com", "Test", "Test")).rejects.toThrow(); expect(driver.send("foo@bar.com", "Test", "Test")).rejects.toThrow();
}); });
+2 -1
View File
@@ -73,8 +73,9 @@ export const env = <
onValid?: (valid: R) => void; onValid?: (valid: R) => void;
}, },
): R extends undefined ? Fallback : R => { ): R extends undefined ? Fallback : R => {
const source = opts?.source ?? (typeof process !== "undefined" ? process?.env : {});
try { try {
const source = opts?.source ?? process.env;
const c = envs[key]; const c = envs[key];
const g = source[c.key]; const g = source[c.key];
const v = c.validate(g) as any; const v = c.validate(g) as any;
+8 -11
View File
@@ -120,17 +120,14 @@ export function patternMatch(target: string, pattern: RegExp | string): boolean
} }
export function slugify(str: string): string { export function slugify(str: string): string {
return ( return String(str)
String(str) .normalize("NFKD") // split accented characters into their base characters and diacritical marks
.normalize("NFKD") // split accented characters into their base characters and diacritical marks .replace(/[\u0300-\u036f]/g, "") // remove all the accents, which happen to be all in the \u03xx UNICODE block.
// biome-ignore lint/suspicious/noMisleadingCharacterClass: <explanation> .trim() // trim leading or trailing whitespace
.replace(/[\u0300-\u036f]/g, "") // remove all the accents, which happen to be all in the \u03xx UNICODE block. .toLowerCase() // convert to lowercase
.trim() // trim leading or trailing whitespace .replace(/[^a-z0-9 -]/g, "") // remove non-alphanumeric characters
.toLowerCase() // convert to lowercase .replace(/\s+/g, "-") // replace spaces with hyphens
.replace(/[^a-z0-9 -]/g, "") // remove non-alphanumeric characters .replace(/-+/g, "-"); // remove consecutive hyphens
.replace(/\s+/g, "-") // replace spaces with hyphens
.replace(/-+/g, "-") // remove consecutive hyphens
);
} }
export function truncate(str: string, length = 50, end = "..."): string { export function truncate(str: string, length = 50, end = "..."): string {
+4 -9
View File
@@ -17,6 +17,7 @@ import {
type Simplify, type Simplify,
sql, sql,
} from "kysely"; } from "kysely";
import type { jsonArrayFrom, jsonBuildObject, jsonObjectFrom } from "kysely/helpers/sqlite";
import type { BaseIntrospector, BaseIntrospectorConfig } from "./BaseIntrospector"; import type { BaseIntrospector, BaseIntrospectorConfig } from "./BaseIntrospector";
import type { DB } from "bknd"; import type { DB } from "bknd";
import type { Constructor } from "core/registry/Registry"; import type { Constructor } from "core/registry/Registry";
@@ -70,15 +71,9 @@ export type IndexSpec = {
}; };
export type DbFunctions = { export type DbFunctions = {
jsonObjectFrom<O>(expr: SelectQueryBuilderExpression<O>): RawBuilder<Simplify<O> | null>; jsonObjectFrom: typeof jsonObjectFrom;
jsonArrayFrom<O>(expr: SelectQueryBuilderExpression<O>): RawBuilder<Simplify<O>[]>; jsonArrayFrom: typeof jsonArrayFrom;
jsonBuildObject<O extends Record<string, Expression<unknown>>>( jsonBuildObject: typeof jsonBuildObject;
obj: O,
): RawBuilder<
Simplify<{
[K in keyof O]: O[K] extends Expression<infer V> ? V : never;
}>
>;
}; };
export type ConnQuery = CompiledQuery | Compilable; export type ConnQuery = CompiledQuery | Compilable;
@@ -18,7 +18,7 @@ export type LibsqlClientFns = {
function getClient(clientOrCredentials: Client | LibSqlCredentials | LibsqlClientFns): Client { function getClient(clientOrCredentials: Client | LibSqlCredentials | LibsqlClientFns): Client {
if (clientOrCredentials && "url" in clientOrCredentials) { if (clientOrCredentials && "url" in clientOrCredentials) {
const { url, authToken } = clientOrCredentials; const { url, authToken } = clientOrCredentials;
return createClient({ url, authToken }); return createClient({ url, authToken }) as unknown as Client;
} }
return clientOrCredentials as Client; return clientOrCredentials as Client;
+11 -13
View File
@@ -23,26 +23,17 @@ export type HybridMode<AdapterConfig extends BkndConfig> = AdapterConfig extends
? BkndModeConfig<Args, Merge<BkndHybridModeOptions & AdapterConfig>> ? BkndModeConfig<Args, Merge<BkndHybridModeOptions & AdapterConfig>>
: never; : never;
export function hybrid<Args>({ export function hybrid<Args>(hybridConfig: HybridBkndConfig<Args>): BkndConfig<Args> {
configFilePath = "bknd-config.json",
...rest
}: HybridBkndConfig<Args>): BkndConfig<Args> {
return { return {
...rest,
config: undefined,
app: async (args) => { app: async (args) => {
const { const {
config: appConfig, config: appConfig,
isProd, isProd,
plugins, plugins,
syncSchemaOptions, syncSchemaOptions,
} = await makeModeConfig( } = await makeModeConfig(hybridConfig, args);
{
...rest, const configFilePath = appConfig.configFilePath ?? "bknd-config.json";
configFilePath,
},
args,
);
if (appConfig?.options?.mode && appConfig?.options?.mode !== "db") { if (appConfig?.options?.mode && appConfig?.options?.mode !== "db") {
$console.warn("You should not set a different mode than `db` when using hybrid mode"); $console.warn("You should not set a different mode than `db` when using hybrid mode");
@@ -80,6 +71,13 @@ export function hybrid<Args>({
skipValidation: isProd, skipValidation: isProd,
// secrets are required for hybrid mode // secrets are required for hybrid mode
secrets: appConfig.secrets, secrets: appConfig.secrets,
onModulesBuilt: async (ctx) => {
if (ctx.flags.sync_required && !isProd && syncSchemaOptions.force) {
$console.log("[hybrid] syncing schema");
await ctx.em.schema().sync(syncSchemaOptions);
}
await appConfig?.options?.manager?.onModulesBuilt?.(ctx);
},
...appConfig?.options?.manager, ...appConfig?.options?.manager,
}, },
}, },
+1 -1
View File
@@ -1,4 +1,4 @@
import { mark, stripMark, $console, s, SecretSchema, setPath } from "bknd/utils"; import { mark, stripMark, $console, s, setPath } from "bknd/utils";
import { BkndError } from "core/errors"; import { BkndError } from "core/errors";
import * as $diff from "core/object/diff"; import * as $diff from "core/object/diff";
import type { Connection } from "data/connection"; import type { Connection } from "data/connection";
@@ -19,9 +19,10 @@ import type { RelationField } from "data/relations";
import { useEntityAdminOptions } from "ui/options"; import { useEntityAdminOptions } from "ui/options";
// simplify react form types 🤦 // simplify react form types 🤦
export type FormApi = ReactFormExtendedApi<any, any, any, any, any, any, any, any, any, any>;
// biome-ignore format: ... // biome-ignore format: ...
export type TFieldApi = FieldApi<any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any>; export type FormApi = ReactFormExtendedApi<any, any, any, any, any, any, any, any, any, any, any, any>;
// biome-ignore format: ...
export type TFieldApi = FieldApi<any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any>;
type EntityFormProps = { type EntityFormProps = {
entity: Entity; entity: Entity;
+1 -1
View File
@@ -207,7 +207,7 @@ function DataEntityUpdateImpl({ params }) {
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
fieldsDisabled={fieldsDisabled} fieldsDisabled={fieldsDisabled}
data={data ?? undefined} data={data ?? undefined}
Form={Form} Form={Form as any}
action="update" action="update"
className="flex flex-grow flex-col gap-3 p-3" className="flex flex-grow flex-col gap-3 p-3"
/> />
@@ -121,7 +121,7 @@ export function DataEntityCreate({ params }) {
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
fieldsDisabled={fieldsDisabled} fieldsDisabled={fieldsDisabled}
data={search.value} data={search.value}
Form={Form} Form={Form as any}
action="create" action="create"
className="flex flex-grow flex-col gap-3 p-3" className="flex flex-grow flex-col gap-3 p-3"
/> />
+2 -2
View File
@@ -1,4 +1,4 @@
import type { JSONSchema7 } from "json-schema"; import type { JSONSchema } from "json-schema-to-ts";
import { omitKeys, type s } from "bknd/utils"; import { omitKeys, type s } from "bknd/utils";
export function extractSchema< export function extractSchema<
@@ -10,7 +10,7 @@ export function extractSchema<
config: Config, config: Config,
keys: Keys[], keys: Keys[],
): [ ): [
JSONSchema7, Exclude<JSONSchema, boolean | null | undefined>,
Partial<Config>, Partial<Config>,
{ {
[K in Keys]: { [K in Keys]: {
+1 -1
View File
@@ -5,7 +5,7 @@ import path from "node:path";
export default defineConfig({ export default defineConfig({
plugins: [tsconfigPaths()], plugins: [tsconfigPaths()],
test: { test: {
projects: ["**/*.vitest.config.ts"], projects: ["**/*.vitest.config.ts", "**/*/vitest.config.ts"],
include: ["**/*.vi-test.ts", "**/*.vitest.ts"], include: ["**/*.vi-test.ts", "**/*.vitest.ts"],
}, },
}); });
+29 -22
View File
@@ -1,14 +1,13 @@
{ {
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", "$schema": "https://biomejs.dev/schemas/2.3.3/schema.json",
"organizeImports": { "assist": { "actions": { "source": { "organizeImports": "off" } } },
"enabled": true
},
"vcs": { "vcs": {
"defaultBranch": "main" "defaultBranch": "main"
}, },
"formatter": { "formatter": {
"enabled": true, "enabled": true,
"indentStyle": "space" "indentStyle": "space",
"formatWithErrors": true
}, },
"javascript": { "javascript": {
"formatter": { "formatter": {
@@ -30,32 +29,36 @@
} }
}, },
"files": { "files": {
"ignore": [ "includes": [
"**/node_modules/**", "**",
"node_modules/**", "!!**/node_modules",
"**/.cache/**", "!!**/.cache",
"**/.wrangler/**", "!!**/.wrangler",
"**/build/**", "!!**/build",
"**/dist/**", "!!**/dist",
"**/data.sqld/**", "!!**/data.sqld",
"data.sqld/**", "!!**/data.sqld",
"public/**", "!!**/public",
".history/**" "!!**/.history"
] ]
}, },
"linter": { "linter": {
"enabled": true, "enabled": true,
"ignore": ["**/*.spec.ts"], "includes": ["**"],
"rules": { "rules": {
"recommended": true, "recommended": true,
"a11y": { "a11y": {},
"all": false
},
"correctness": { "correctness": {
"useExhaustiveDependencies": "off", "useExhaustiveDependencies": "off",
"noUnreachable": "warn", "noUnreachable": "warn",
"noChildrenProp": "off", "noChildrenProp": "off",
"noSwitchDeclarations": "warn" "noSwitchDeclarations": "warn",
"noUnusedVariables": {
"options": {
"ignoreRestSiblings": true
},
"level": "warn"
}
}, },
"complexity": { "complexity": {
"noUselessFragments": "warn", "noUselessFragments": "warn",
@@ -70,7 +73,11 @@
"noArrayIndexKey": "off", "noArrayIndexKey": "off",
"noImplicitAnyLet": "warn", "noImplicitAnyLet": "warn",
"noConfusingVoidType": "off", "noConfusingVoidType": "off",
"noConsoleLog": "warn" "noConsole": {
"level": "warn",
"options": { "allow": ["error", "info"] }
},
"noTsIgnore": "off"
}, },
"security": { "security": {
"noDangerouslySetInnerHtml": "off" "noDangerouslySetInnerHtml": "off"
+1003 -720
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
[install]
#linker = "hoisted"
@@ -164,7 +164,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: 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 ```ts
import { devFsVitePlugin } from "bknd/adapter/cloudflare"; import { devFsVitePlugin } from "bknd/adapter/cloudflare/vite";
export default defineConfig({ export default defineConfig({
plugins: [devFsVitePlugin()], // [!code highlight] plugins: [devFsVitePlugin()], // [!code highlight]
@@ -174,7 +174,7 @@ export default defineConfig({
Now to use this polyfill, you can use the `devFsWrite` function to write files to the filesystem. Now to use this polyfill, you can use the `devFsWrite` function to write files to the filesystem.
```ts ```ts
import { devFsWrite } from "bknd/adapter/cloudflare"; // [!code highlight] import { devFsWrite } from "bknd/adapter/cloudflare/vite"; // [!code highlight]
import { syncTypes } from "bknd/plugins"; import { syncTypes } from "bknd/plugins";
export default { export default {
+4 -4
View File
@@ -6,15 +6,15 @@
"scripts": { "scripts": {
"deploy": "wrangler deploy", "deploy": "wrangler deploy",
"dev": "wrangler dev", "dev": "wrangler dev",
"bknd-typegen": "PROXY=1 npx bknd types", "bknd-typegen": "PROXY=1 node_modules/.bin/bknd types",
"typegen": "wrangler types && npm run bknd-typegen", "typegen": "wrangler types && npm run bknd-typegen",
"predev": "npm run typegen" "postinstall": "npm run typegen"
}, },
"dependencies": { "dependencies": {
"bknd": "file:../../app" "bknd": "file:../../app"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.9.2", "typescript": "^5.9.3",
"wrangler": "^4.34.0" "wrangler": "^4.46.0"
} }
} }
+1 -2
View File
@@ -2,8 +2,7 @@
"$schema": "node_modules/wrangler/config-schema.json", "$schema": "node_modules/wrangler/config-schema.json",
"name": "bknd-cf-worker-example", "name": "bknd-cf-worker-example",
"main": "src/index.ts", "main": "src/index.ts",
"compatibility_date": "2025-08-03", "compatibility_date": "2025-11-05",
"compatibility_flags": ["nodejs_compat"],
"workers_dev": true, "workers_dev": true,
"minify": true, "minify": true,
"assets": { "assets": {
+6 -6
View File
@@ -12,13 +12,13 @@
}, },
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "2.3.3",
"@tsconfig/strictest": "^2.0.5", "@tsconfig/strictest": "^2.0.7",
"@types/bun": "^1.3.1",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"bun-types": "^1.1.18", "miniflare": "^3.20250718.2",
"miniflare": "^3.20240806.0", "typescript": "^5.9.3",
"typescript": "^5.5.3", "verdaccio": "^6.2.1"
"verdaccio": "^5.32.1"
}, },
"engines": { "engines": {
"node": ">=22" "node": ">=22"