26 changed files with 2467 additions and 1735 deletions
+3 -3
View File
@@ -25,15 +25,15 @@ jobs:
--health-retries 5
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v7
uses: actions/setup-node@v4
with:
node-version: "22.x"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
uses: oven-sh/setup-bun@v1
with:
bun-version: "1.3.3"
+2 -2
View File
@@ -107,7 +107,7 @@ bknd/
index.ts # Main package exports
__test__/ # Unit tests (mirrors src/ structure)
e2e/ # End-to-end tests (Playwright)
build.ts # Build script (tsdown/rolldown)
build.ts # Build script (tsup/esbuild)
build.cli.ts # CLI build script
packages/ # Small satellite packages
cli/ # Standalone CLI package (bknd-cli)
@@ -146,7 +146,7 @@ Plugins (in `app/src/plugins/`) hook into the app lifecycle via callbacks like `
### Build System
The build is handled by a custom `app/build.ts` script using tsdown (rolldown under the hood). It builds four targets in parallel:
The build is handled by a custom `app/build.ts` script using tsup (esbuild under the hood). It builds four targets in parallel:
- **API** -- the core backend
- **UI** -- the admin interface
+19 -1
View File
@@ -4,7 +4,7 @@ import { Entity, EntityManager } from "data/entities";
import {
ManyToOneRelation,
OneToOneRelation,
RelationField,
type RelationField,
RelationMutator,
} from "data/relations";
import { NumberField, TextField } from "data/fields";
@@ -41,6 +41,24 @@ describe("[data] Mutator (base)", async () => {
// but expect additional fields to be present
expect((res.data as any).not_fillable).toBeDefined();
expect((res.data as any).id).toBeDefined();
});
test("insertMany", async () => {
expect(em.mutator(entity).getValidatedData(payload, "create")).resolves.toEqual(payload);
const res = await em.mutator(entity).insertMany([
{ label: "item 1", count: 1 },
{ label: "item 2", count: 2 },
{ label: "item 3", count: 3 },
{ label: "item 4", count: 4 },
]);
expect(res.data.length).toBe(4);
res.data.forEach((entry) => {
expect((entry as any).id).toBeDefined();
expect((entry as any).label).toStartWith("item ");
expect(entry.count).toBeNumber();
});
});
test("updateOne", async () => {
+328 -349
View File
@@ -1,9 +1,9 @@
import { build, type UserConfig } from "tsdown";
import { $ } from "bun";
import * as tsup from "tsup";
import pkg from "./package.json" with { type: "json" };
import c from "picocolors";
import { watch as fsWatch, readdirSync, rmSync } from "node:fs";
import { join, resolve } from "node:path";
import { buildTypes as runTypesBuild } from "./build.types";
import { join } from "node:path";
const args = process.argv.slice(2);
const watch = args.includes("--watch");
@@ -11,19 +11,18 @@ const minify = args.includes("--minify");
const types = args.includes("--types");
const sourcemap = args.includes("--sourcemap");
const clean = args.includes("--clean");
const dev = args.includes("--dev");
// silence tsdown
// silence tsup
const oldConsole = {
log: console.log,
warn: console.warn,
log: console.log,
warn: console.warn,
};
console.log = () => { };
console.warn = () => { };
console.log = () => {};
console.warn = () => {};
const define = {
__isDev: "0",
__version: JSON.stringify(pkg.version),
__isDev: "0",
__version: JSON.stringify(pkg.version),
};
if (clean) {
@@ -37,167 +36,156 @@ if (clean) {
const entryPath = join(distPath, entry);
rmSync(entryPath, { recursive: true, force: true });
}
} catch {
} catch (e) {
// dist may not exist yet, ignore
}
}
let types_running = false;
async function buildTypes() {
if (types_running || !types) return;
types_running = true;
function buildTypes() {
if (types_running || !types) return;
types_running = true;
try {
await runTypesBuild();
Bun.spawn(["bun", "build:types"], {
stdout: "inherit",
onExit: () => {
oldConsole.log(c.cyan("[Types]"), c.green("built"));
} finally {
types_running = false;
}
Bun.spawn(["bun", "tsc-alias"], {
stdout: "inherit",
onExit: () => {
oldConsole.log(c.cyan("[Types]"), c.green("aliased"));
types_running = false;
},
});
},
});
}
if (types && !watch) {
buildTypes();
}
let watcher_timeout: any;
function delayTypes(delayTime = 1000) {
if (!watch || !types) return;
if (watcher_timeout) {
clearTimeout(watcher_timeout);
}
watcher_timeout = setTimeout(() => {
void buildTypes().catch((error) => {
oldConsole.warn(c.cyan("[Types]"), c.red("build failed"), error);
});
}, delayTime);
function delayTypes() {
if (!watch || !types) return;
if (watcher_timeout) {
clearTimeout(watcher_timeout);
}
watcher_timeout = setTimeout(buildTypes, 1000);
}
const dependencies = Object.keys(pkg.dependencies);
// collection of always-neverBundle packages
const neverBundle = [
...dependencies,
"bun:test",
"node:test",
"node:assert/strict",
"@libsql/client",
"bknd",
/^bknd\/.*/,
"jsonv-ts",
/^jsonv-ts\/.*/,
// collection of always-external packages
const external = [
...dependencies,
"bun:test",
"node:test",
"node:assert/strict",
"@libsql/client",
"bknd",
/^bknd\/.*/,
"jsonv-ts",
/^jsonv-ts\/.*/,
] as const;
/**
* Building backend and general API
*/
async function buildApi() {
await build({
minify,
sourcemap,
// don't use tsdown's broken watch, we'll handle it ourselves
watch: false,
// cleaning is handled manually (--clean) to preserve dist/static
clean: false,
dts: false,
define,
entry: [
"src/index.ts",
"src/core/utils/index.ts",
"src/plugins/index.ts",
"src/modes/index.ts",
],
unbundle: false,
outDir: "dist",
deps: {
neverBundle: [
...neverBundle
],
},
target: "esnext",
platform: "browser",
nodeProtocol: true,
format: "esm",
loader: {
".svg": "dataurl",
},
onSuccess: async () => {
delayTypes();
oldConsole.log(c.cyan("[API]"), c.green("built"));
},
});
await tsup.build({
minify,
sourcemap,
// don't use tsup's broken watch, we'll handle it ourselves
watch: false,
define,
entry: [
"src/index.ts",
"src/core/utils/index.ts",
"src/plugins/index.ts",
"src/modes/index.ts",
],
outDir: "dist",
external: [...external],
metafile: true,
target: "esnext",
platform: "browser",
removeNodeProtocol: false,
format: ["esm"],
splitting: false,
loader: {
".svg": "dataurl",
},
onSuccess: async () => {
delayTypes();
oldConsole.log(c.cyan("[API]"), c.green("built"));
},
});
}
async function rewriteClient(path: string) {
const bundle = await Bun.file(path).text();
await Bun.write(path, '"use client";\n' + bundle.replaceAll("ui/client", "bknd/client"));
const bundle = await Bun.file(path).text();
await Bun.write(
path,
'"use client";\n' + bundle.replaceAll("ui/client", "bknd/client"),
);
}
/**
* Building UI for direct imports
*/
async function buildUi() {
const base = {
minify,
sourcemap,
watch: false,
// cleaning is handled manually (--clean) to preserve dist/static
clean: false,
dts: false,
define,
css: {
transformer: "postcss",
postcss: "postcss.config.js",
minify: true,
splitting: false
},
deps: {
neverBundle: [
...neverBundle,
"react",
"react-dom",
"react/jsx-runtime",
"react/jsx-dev-runtime",
"use-sync-external-store",
/codemirror/,
"@xyflow/react",
"@mantine/core",
],
},
devtools: dev,
platform: "browser",
format: "esm",
unbundle: false,
treeshake: true,
loader: {
".svg": "dataurl",
},
const base = {
minify,
sourcemap,
watch: false,
define,
external: [
...external,
"react",
"react-dom",
"react/jsx-runtime",
"react/jsx-dev-runtime",
"use-sync-external-store",
/codemirror/,
"@xyflow/react",
"@mantine/core",
],
metafile: true,
platform: "browser",
format: ["esm"],
splitting: false,
bundle: true,
treeshake: true,
loader: {
".svg": "dataurl",
},
esbuildOptions: (options) => {
options.logLevel = "silent";
},
} satisfies tsup.Options;
logLevel: "silent",
} satisfies UserConfig;
await tsup.build({
...base,
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
outDir: "dist/ui",
onSuccess: async () => {
await rewriteClient("./dist/ui/index.js");
delayTypes();
oldConsole.log(c.cyan("[UI]"), c.green("built"));
},
});
await Promise.all([
build({
...base,
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
outDir: "dist/ui",
onSuccess: async () => {
await rewriteClient("./dist/ui/index.js");
delayTypes();
oldConsole.log(c.cyan("[UI]"), c.green("built"));
},
}),
build({
...base,
entry: ["src/ui/client/index.ts"],
outDir: "dist/ui/client",
onSuccess: async () => {
await rewriteClient("./dist/ui/client/index.js");
delayTypes();
oldConsole.log(c.cyan("[UI]"), "Client", c.green("built"));
},
}),
]);
await tsup.build({
...base,
entry: ["src/ui/client/index.ts"],
outDir: "dist/ui/client",
onSuccess: async () => {
await rewriteClient("./dist/ui/client/index.js");
delayTypes();
oldConsole.log(c.cyan("[UI]"), "Client", c.green("built"));
},
});
}
/**
@@ -206,247 +194,238 @@ async function buildUi() {
* - ui/client is external, and after built replaced with "bknd/client"
*/
async function buildUiElements() {
await build({
minify,
sourcemap,
watch: false,
// cleaning is handled manually (--clean) to preserve dist/static
clean: false,
dts: false,
define,
entry: ["src/ui/elements/index.ts"],
outDir: "dist/ui/elements",
deps: {
neverBundle: [
"ui/client",
"bknd",
/^bknd\/.*/,
"wouter",
"react",
"react-dom",
"react/jsx-runtime",
"react/jsx-dev-runtime",
"use-sync-external-store",
],
},
platform: "browser",
format: "esm",
bundle: true,
treeshake: true,
loader: {
".svg": "dataurl",
},
alias: {
// not important for elements, mock to reduce bundle
"tailwind-merge": resolve(import.meta.dir, "src/ui/elements/mocks/tailwind-merge.ts"),
},
onSuccess: async () => {
await rewriteClient("./dist/ui/elements/index.js");
delayTypes();
oldConsole.log(c.cyan("[UI]"), "Elements", c.green("built"));
},
});
await tsup.build({
minify,
sourcemap,
watch: false,
define,
entry: ["src/ui/elements/index.ts"],
outDir: "dist/ui/elements",
external: [
"ui/client",
"bknd",
/^bknd\/.*/,
"wouter",
"react",
"react-dom",
"react/jsx-runtime",
"react/jsx-dev-runtime",
"use-sync-external-store",
],
metafile: true,
platform: "browser",
format: ["esm"],
splitting: false,
bundle: true,
treeshake: true,
loader: {
".svg": "dataurl",
},
esbuildOptions: (options) => {
options.alias = {
// not important for elements, mock to reduce bundle
"tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts",
};
},
onSuccess: async () => {
await rewriteClient("./dist/ui/elements/index.js");
delayTypes();
oldConsole.log(c.cyan("[UI]"), "Elements", c.green("built"));
},
});
}
/**
* Building adapters
*/
function baseConfig(adapter: string, overrides: Partial<UserConfig> = {}): UserConfig {
return {
minify,
sourcemap,
watch: false,
// cleaning is handled manually (--clean) to preserve dist/static
clean: false,
dts: false,
entry: [`src/adapter/${adapter}/index.ts`],
format: "esm",
platform: "neutral",
outDir: `dist/adapter/${adapter}`,
devtools: dev,
nodeProtocol: true,
onSuccess: async () => {
delayTypes();
oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built"));
},
...overrides,
define: {
...define,
...overrides.define,
},
deps: {
neverBundle: [
/^cloudflare*/,
/^@?hono.*?/,
/^(bknd|react|next|node).*?/,
/.*\.(html)$/,
...neverBundle,
...(Array.isArray(overrides.deps?.neverBundle) ? overrides.deps.neverBundle : []),
],
},
};
function baseConfig(
adapter: string,
overrides: Partial<tsup.Options> = {},
): tsup.Options {
return {
minify,
sourcemap,
watch: false,
entry: [`src/adapter/${adapter}/index.ts`],
format: ["esm"],
platform: "neutral",
outDir: `dist/adapter/${adapter}`,
metafile: true,
splitting: false,
removeNodeProtocol: false,
onSuccess: async () => {
delayTypes();
oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built"));
},
...overrides,
define: {
...define,
...overrides.define,
},
external: [
/^cloudflare*/,
/^@?hono.*?/,
/^(bknd|react|next|node).*?/,
/.*\.(html)$/,
...external,
...(Array.isArray(overrides.external) ? overrides.external : []),
],
};
}
async function buildAdapters() {
await Promise.all([
// base adapter handles
build({
...baseConfig(""),
target: "esnext",
platform: "neutral",
entry: ["src/adapter/index.ts"],
outDir: "dist/adapter",
// only way to keep @vite-ignore comments
minify: false,
dts: false,
await Promise.all([
// base adapter handles
tsup.build({
...baseConfig(""),
target: "esnext",
platform: "neutral",
entry: ["src/adapter/index.ts"],
outDir: "dist/adapter",
// only way to keep @vite-ignore comments
minify: false,
}),
// specific adatpers
tsup.build(baseConfig("react-router")),
tsup.build(
baseConfig("browser", {
external: [/^sqlocal\/?.*?/, "wouter"],
}),
// specific adapters
build(baseConfig("react-router")),
build(
baseConfig("browser", {
deps: {
neverBundle: [/^sqlocal\/?.*?/, "wouter"],
},
}),
),
build(
baseConfig("bun", {
deps: {
neverBundle: [/^bun:.*/],
},
}),
),
build(baseConfig("astro")),
build(baseConfig("aws")),
build(
baseConfig("cloudflare", {
deps: {
neverBundle: ["wrangler", "node:process", "miniflare"],
},
}),
),
build(
baseConfig("cloudflare/proxy", {
target: "esnext",
entry: ["src/adapter/cloudflare/proxy.ts"],
outDir: "dist/adapter/cloudflare",
devtools: false,
deps: {
neverBundle: [/bknd/, "wrangler", "node:process", "miniflare"],
},
}),
),
build({
...baseConfig("vite"),
platform: "node",
),
tsup.build(
baseConfig("bun", {
external: [/^bun\:.*/],
}),
build({
...baseConfig("nextjs"),
platform: "node",
),
tsup.build(baseConfig("astro")),
tsup.build(baseConfig("aws")),
tsup.build(
baseConfig("cloudflare", {
external: ["wrangler", "node:process"],
}),
build({
...baseConfig("tanstack-start"),
platform: "node",
),
tsup.build(
baseConfig("cloudflare/proxy", {
target: "esnext",
entry: ["src/adapter/cloudflare/proxy.ts"],
outDir: "dist/adapter/cloudflare",
metafile: false,
external: [/bknd/, "wrangler", "node:process"],
}),
),
build({
...baseConfig("sveltekit"),
platform: "node",
}),
tsup.build({
...baseConfig("vite"),
platform: "node",
}),
build({
...baseConfig("nuxt"),
platform: "node",
}),
tsup.build({
...baseConfig("nextjs"),
platform: "node",
}),
build({
...baseConfig("node"),
platform: "node",
}),
tsup.build({
...baseConfig("tanstack-start"),
platform: "node",
}),
build({
...baseConfig("sqlite/edge"),
entry: ["src/adapter/sqlite/edge.ts"],
outDir: "dist/adapter/sqlite",
devtools: false,
}),
tsup.build({
...baseConfig("sveltekit"),
platform: "node",
}),
build({
...baseConfig("sqlite/node"),
entry: ["src/adapter/sqlite/node.ts"],
outDir: "dist/adapter/sqlite",
platform: "node",
devtools: false,
}),
tsup.build({
...baseConfig("nuxt"),
platform: "node",
}),
build(
baseConfig("sqlite/bun", {
entry: ["src/adapter/sqlite/bun.ts"],
outDir: "dist/adapter/sqlite",
devtools: false,
deps: {
neverBundle: [/^bun:.*/],
},
}),
),
]);
tsup.build({
...baseConfig("node"),
platform: "node",
}),
tsup.build({
...baseConfig("sqlite/edge"),
entry: ["src/adapter/sqlite/edge.ts"],
outDir: "dist/adapter/sqlite",
metafile: false,
}),
tsup.build({
...baseConfig("sqlite/node"),
entry: ["src/adapter/sqlite/node.ts"],
outDir: "dist/adapter/sqlite",
platform: "node",
metafile: false,
}),
tsup.build({
...baseConfig("sqlite/bun"),
entry: ["src/adapter/sqlite/bun.ts"],
outDir: "dist/adapter/sqlite",
metafile: false,
external: [/^bun\:.*/],
}),
]);
}
const buildPipeline = [buildApi, buildUi, buildUiElements, buildAdapters];
// Run type generation in parallel, since it can be slow and doesn't block the main build
if (types) {
buildPipeline.push(async () => await buildTypes());
async function buildAll() {
await Promise.all([
buildApi(),
buildUi(),
buildUiElements(),
buildAdapters(),
]);
}
// initial build
await buildAll();
async function buildAll() {
await Promise.all(buildPipeline.map(fn => fn()));
}
// custom watcher since tsdown's watch is broken in v0.21.7
// custom watcher since tsup's watch is broken in 8.3.5+
if (watch) {
oldConsole.log(c.cyan("[Watch]"), "watching for changes in src/...");
oldConsole.log(c.cyan("[Watch]"), "watching for changes in src/...");
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
let isBuilding = false;
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
let isBuilding = false;
const rebuild = async () => {
if (isBuilding) return;
isBuilding = true;
oldConsole.log(c.cyan("[Watch]"), "rebuilding...");
try {
await buildAll();
oldConsole.log(c.cyan("[Watch]"), c.green("done"));
} catch (e) {
oldConsole.warn(c.cyan("[Watch]"), c.red("build failed"), e);
}
isBuilding = false;
};
const rebuild = async () => {
if (isBuilding) return;
isBuilding = true;
oldConsole.log(c.cyan("[Watch]"), "rebuilding...");
try {
await buildAll();
oldConsole.log(c.cyan("[Watch]"), c.green("done"));
} catch (e) {
oldConsole.warn(c.cyan("[Watch]"), c.red("build failed"), e);
}
isBuilding = false;
};
const debouncedRebuild = () => {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(rebuild, 100);
};
const debouncedRebuild = () => {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(rebuild, 100);
};
// watch src directory recursively
fsWatch(join(import.meta.dir, "src"), { recursive: true }, (event, filename) => {
// watch src directory recursively
fsWatch(
join(import.meta.dir, "src"),
{ recursive: true },
(event, filename) => {
if (!filename) return;
// ignore non-source files
if (!filename.endsWith(".ts") && !filename.endsWith(".tsx") && !filename.endsWith(".css"))
return;
if (
!filename.endsWith(".ts") &&
!filename.endsWith(".tsx") &&
!filename.endsWith(".css")
)
return;
oldConsole.log(c.cyan("[Watch]"), c.dim(`${event}: ${filename}`));
debouncedRebuild();
});
},
);
// keep process alive
await new Promise(() => { });
// keep process alive
await new Promise(() => {});
}
-35
View File
@@ -1,35 +0,0 @@
import { build } from "tsdown";
import c from "picocolors";
export async function buildTypes() {
await build({
tsconfig: "tsconfig.build.json",
watch: false,
clean: true,
logLevel: "silent",
unbundle: true,
root: "src",
outDir: "dist/types",
entry: ["./src/**/*.ts", "./src/**/*.tsx"],
format: "esm",
dts: {
emitDtsOnly: true,
resolver: "oxc",
eager: true,
incremental: true,
},
deps: {
skipNodeModulesBundle: true
},
});
}
if (import.meta.main) {
try {
await buildTypes();
console.log(c.cyan("[Types]"), c.green("built"));
} catch (error) {
console.error(c.cyan("[Types]"), c.red("build failed"), error);
process.exitCode = 1;
}
}
+78 -107
View File
@@ -15,8 +15,7 @@
},
"packageManager": "bun@1.3.3",
"engines": {
"node": ">=22.18.0",
"bun": ">=1.3.3"
"node": ">=22.13"
},
"scripts": {
"dev": "BKND_CLI_LOG_LEVEL=debug vite",
@@ -24,11 +23,11 @@
"build:all": "rm -rf dist && bun run build:static && NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
"build:ci": "mkdir -p dist/static/.vite && echo '{}' > dist/static/.vite/manifest.json && NODE_ENV=production bun run build.ts",
"build:cli": "bun run build.cli.ts",
"build:types": "bun run build.types.ts",
"build:static": "vite build",
"watch": "bun run build.ts --types --watch",
"types": "bun tsc -p tsconfig.build.json --noEmit",
"clean:types": "find ./dist -name '*.d.ts' -delete && rm -f ./dist/tsconfig.tsbuildinfo",
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
"updater": "bun x npm-check-updates -ui",
"cli": "LOCAL=1 bun src/cli/index.ts",
"prepublishOnly": "bun run types && bun run test && bun run test:node && NODE_NO_WARNINGS=1 VITE_DB_URL=:memory: bun run test:e2e && bun run build:all && cp ../README.md ./",
@@ -94,18 +93,17 @@
"@rjsf/utils": "5.22.0",
"@standard-schema/spec": "^1.0.0",
"@tabler/icons-react": "3.35.0",
"@tailwindcss/postcss": "^4.2.2",
"@tailwindcss/vite": "^4.2.4",
"@tailwindcss/postcss": "^4.1.16",
"@tailwindcss/vite": "^4.1.16",
"@tanstack/react-store": "^0.8.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@tsdown/css": "^0.22.14",
"@types/node": "^24.13.3",
"@types/node": "^24.10.0",
"@types/pg": "^8.15.6",
"@types/react": "^19.2.16",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^6.0.2",
"@vitest/coverage-v8": "4.1.10",
"@vitejs/plugin-react": "^5.1.0",
"@vitest/coverage-v8": "3.0.9",
"autoprefixer": "^10.4.21",
"clsx": "^2.1.1",
"commander": "^14.0.2",
@@ -116,87 +114,89 @@
"kysely-postgres-js": "^2.0.0",
"libsql": "^0.5.22",
"libsql-stateless-easy": "^1.8.0",
"miniflare": "^4.20260317.3",
"miniflare": "^4.20251011.2",
"open": "^10.2.0",
"openapi-types": "^12.1.3",
"pg": "^8.16.3",
"postcss": "^8.5.26",
"postcss-import": "^16.1.1",
"postcss": "^8.5.3",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"postgres": "^3.4.7",
"posthog-js-lite": "^3.6.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.66.0",
"react-icons": "5.5.0",
"react-json-view-lite": "^2.5.0",
"sql-formatter": "^15.6.10",
"sqlocal": "^0.16.0",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.3.3",
"tailwindcss": "^4.1.16",
"tailwindcss-animate": "^1.0.7",
"tsdown": "^0.22.14",
"tsc-alias": "^1.8.16",
"tsup": "^8.5.0",
"tsx": "^4.20.6",
"uuid": "^13.0.0",
"vite": "^8.0.16",
"vite-plugin-circular-dependency": "^0.6.0",
"vitest": "4.1.10",
"vite": "^7.1.12",
"vite-plugin-circular-dependency": "^0.5.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "3.0.9",
"wouter": "^3.7.1",
"wrangler": "^4.78.0"
"wrangler": "^4.45.4"
},
"optionalDependencies": {
"@hono/node-server": "^1.19.6"
},
"peerDependencies": {
"react": ">=19.2.6",
"react-dom": ">=19.2.6"
"react": ">=19",
"react-dom": ">=19"
},
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/types/index.d.mts",
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"types": "./dist/types/index.d.mts",
"types": "./dist/types/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.js"
},
"./ui": {
"types": "./dist/types/ui/index.d.mts",
"types": "./dist/types/ui/index.d.ts",
"import": "./dist/ui/index.js",
"require": "./dist/ui/index.js"
},
"./elements": {
"types": "./dist/types/ui/elements/index.d.mts",
"types": "./dist/types/ui/elements/index.d.ts",
"import": "./dist/ui/elements/index.js",
"require": "./dist/ui/elements/index.js"
},
"./client": {
"types": "./dist/types/ui/client/index.d.mts",
"types": "./dist/types/ui/client/index.d.ts",
"import": "./dist/ui/client/index.js",
"require": "./dist/ui/client/index.js"
},
"./utils": {
"types": "./dist/types/core/utils/index.d.mts",
"types": "./dist/types/core/utils/index.d.ts",
"import": "./dist/core/utils/index.js",
"require": "./dist/core/utils/index.js"
},
"./cli": {
"types": "./dist/types/cli/index.d.mts",
"types": "./dist/types/cli/index.d.ts",
"import": "./dist/cli/index.js",
"require": "./dist/cli/index.js"
},
"./plugins": {
"types": "./dist/types/plugins/index.d.mts",
"types": "./dist/types/plugins/index.d.ts",
"import": "./dist/plugins/index.js",
"require": "./dist/plugins/index.js"
},
"./modes": {
"types": "./dist/types/modes/index.d.mts",
"types": "./dist/types/modes/index.d.ts",
"import": "./dist/modes/index.js",
"require": "./dist/modes/index.js"
},
"./adapter/sqlite": {
"types": "./dist/types/adapter/sqlite/edge.d.mts",
"types": "./dist/types/adapter/sqlite/edge.d.ts",
"import": {
"workerd": "./dist/adapter/sqlite/edge.js",
"edge-light": "./dist/adapter/sqlite/edge.js",
@@ -204,130 +204,101 @@
"vercel": "./dist/adapter/sqlite/edge.js",
"browser": "./dist/adapter/sqlite/edge.js",
"bun": "./dist/adapter/sqlite/bun.js",
"node": "./dist/adapter/sqlite/node.mjs",
"default": "./dist/adapter/sqlite/node.mjs"
"node": "./dist/adapter/sqlite/node.js",
"default": "./dist/adapter/sqlite/node.js"
},
"require": "./dist/adapter/sqlite/node.mjs"
"require": "./dist/adapter/sqlite/node.js"
},
"./adapter/cloudflare": {
"types": "./dist/types/adapter/cloudflare/index.d.mts",
"types": "./dist/types/adapter/cloudflare/index.d.ts",
"import": "./dist/adapter/cloudflare/index.js",
"require": "./dist/adapter/cloudflare/index.js"
},
"./adapter/cloudflare/proxy": {
"types": "./dist/types/adapter/cloudflare/proxy.d.mts",
"types": "./dist/types/adapter/cloudflare/proxy.d.ts",
"import": "./dist/adapter/cloudflare/proxy.js",
"require": "./dist/adapter/cloudflare/proxy.js"
},
"./adapter": {
"types": "./dist/types/adapter/index.d.mts",
"types": "./dist/types/adapter/index.d.ts",
"import": "./dist/adapter/index.js"
},
"./adapter/vite": {
"types": "./dist/types/adapter/vite/index.d.mts",
"import": "./dist/adapter/vite/index.mjs",
"require": "./dist/adapter/vite/index.mjs"
"types": "./dist/types/adapter/vite/index.d.ts",
"import": "./dist/adapter/vite/index.js",
"require": "./dist/adapter/vite/index.js"
},
"./adapter/nextjs": {
"types": "./dist/types/adapter/nextjs/index.d.mts",
"import": "./dist/adapter/nextjs/index.mjs",
"require": "./dist/adapter/nextjs/index.mjs"
"types": "./dist/types/adapter/nextjs/index.d.ts",
"import": "./dist/adapter/nextjs/index.js",
"require": "./dist/adapter/nextjs/index.js"
},
"./adapter/nuxt": {
"types": "./dist/types/adapter/nuxt/index.d.mts",
"import": "./dist/adapter/nuxt/index.mjs",
"require": "./dist/adapter/nuxt/index.mjs"
"types": "./dist/types/adapter/nuxt/index.d.ts",
"import": "./dist/adapter/nuxt/index.js",
"require": "./dist/adapter/nuxt/index.js"
},
"./adapter/react-router": {
"types": "./dist/types/adapter/react-router/index.d.mts",
"types": "./dist/types/adapter/react-router/index.d.ts",
"import": "./dist/adapter/react-router/index.js",
"require": "./dist/adapter/react-router/index.js"
},
"./adapter/bun": {
"types": "./dist/types/adapter/bun/index.d.mts",
"types": "./dist/types/adapter/bun/index.d.ts",
"import": "./dist/adapter/bun/index.js",
"require": "./dist/adapter/bun/index.js"
},
"./adapter/node": {
"types": "./dist/types/adapter/node/index.d.mts",
"import": "./dist/adapter/node/index.mjs",
"require": "./dist/adapter/node/index.mjs"
"types": "./dist/types/adapter/node/index.d.ts",
"import": "./dist/adapter/node/index.js",
"require": "./dist/adapter/node/index.js"
},
"./adapter/astro": {
"types": "./dist/types/adapter/astro/index.d.mts",
"types": "./dist/types/adapter/astro/index.d.ts",
"import": "./dist/adapter/astro/index.js",
"require": "./dist/adapter/astro/index.js"
},
"./adapter/sveltekit": {
"types": "./dist/types/adapter/sveltekit/index.d.mts",
"import": "./dist/adapter/sveltekit/index.mjs",
"require": "./dist/adapter/sveltekit/index.mjs"
"types": "./dist/types/adapter/sveltekit/index.d.ts",
"import": "./dist/adapter/sveltekit/index.js",
"require": "./dist/adapter/sveltekit/index.js"
},
"./adapter/aws": {
"types": "./dist/types/adapter/aws/index.d.mts",
"types": "./dist/types/adapter/aws/index.d.ts",
"import": "./dist/adapter/aws/index.js",
"require": "./dist/adapter/aws/index.js"
},
"./adapter/browser": {
"types": "./dist/types/adapter/browser/index.d.mts",
"types": "./dist/types/adapter/browser/index.d.ts",
"import": "./dist/adapter/browser/index.js",
"require": "./dist/adapter/browser/index.js"
},
"./adapter/tanstack-start": {
"types": "./dist/types/adapter/tanstack-start/index.d.mts",
"import": "./dist/adapter/tanstack-start/index.mjs",
"require": "./dist/adapter/tanstack-start/index.mjs"
"types": "./dist/types/adapter/tanstack-start/index.d.ts",
"import": "./dist/adapter/tanstack-start/index.js",
"require": "./dist/adapter/tanstack-start/index.js"
},
"./dist/main.css": "./dist/ui/style.css",
"./dist/styles.css": "./dist/ui/style.css",
"./dist/style.css": "./dist/ui/style.css",
"./dist/main.css": "./dist/ui/main.css",
"./dist/styles.css": "./dist/ui/styles.css",
"./dist/manifest.json": "./dist/static/.vite/manifest.json",
"./static/*": "./dist/static/*"
},
"typesVersions": {
"*": {
"utils": [
"./dist/types/core/utils/index.d.mts"
],
"cli": [
"./dist/types/cli/index.d.mts"
],
"plugins": [
"./dist/types/plugins/index.d.mts"
],
"adapter": [
"./dist/types/adapter/index.d.mts"
],
"adapter/cloudflare": [
"./dist/types/adapter/cloudflare/index.d.mts"
],
"adapter/vite": [
"./dist/types/adapter/vite/index.d.mts"
],
"adapter/nextjs": [
"./dist/types/adapter/nextjs/index.d.mts"
],
"adapter/nuxt": [
"./dist/types/adapter/nuxt/index.d.mts"
],
"adapter/react-router": [
"./dist/types/adapter/react-router/index.d.mts"
],
"adapter/bun": [
"./dist/types/adapter/bun/index.d.mts"
],
"adapter/node": [
"./dist/types/adapter/node/index.d.mts"
],
"adapter/sveltekit": [
"./dist/types/adapter/sveltekit/index.d.mts"
],
"adapter/tanstack-start": [
"./dist/types/adapter/tanstack-start/index.d.mts"
],
"adapter/sqlite": [
"./dist/types/adapter/sqlite/edge.d.mts"
]
"utils": ["./dist/types/core/utils/index.d.ts"],
"cli": ["./dist/types/cli/index.d.ts"],
"plugins": ["./dist/types/plugins/index.d.ts"],
"adapter": ["./dist/types/adapter/index.d.ts"],
"adapter/cloudflare": ["./dist/types/adapter/cloudflare/index.d.ts"],
"adapter/vite": ["./dist/types/adapter/vite/index.d.ts"],
"adapter/nextjs": ["./dist/types/adapter/nextjs/index.d.ts"],
"adapter/nuxt": ["./dist/types/adapter/nuxt/index.d.ts"],
"adapter/react-router": ["./dist/types/adapter/react-router/index.d.ts"],
"adapter/bun": ["./dist/types/adapter/bun/index.d.ts"],
"adapter/node": ["./dist/types/adapter/node/index.d.ts"],
"adapter/sveltekit": ["./dist/types/adapter/sveltekit/index.d.ts"],
"adapter/tanstack-start": ["./dist/types/adapter/tanstack-start/index.d.ts"],
"adapter/sqlite": ["./dist/types/adapter/sqlite/edge.d.ts"]
}
},
"publishConfig": {
@@ -8,12 +8,6 @@ import { $console } from "bknd/utils";
import { createRuntimeApp } from "bknd/adapter";
import { registerAsyncsExecutionContext, makeConfig, type CloudflareContext } from "./config";
// Statically import the admin UI build manifest so the bundler (wrangler/esbuild)
// resolves and bundles it at build time, rather than leaving a runtime dynamic
// import that workerd cannot resolve via package.json exports maps.
// @ts-ignore
import adminUiManifest from "bknd/dist/manifest.json";
declare global {
namespace Cloudflare {
interface Env {}
@@ -44,19 +38,6 @@ export async function createApp<Env extends CloudflareEnv = CloudflareEnv>(
ctx: Partial<CloudflareContext<Env>> = {},
) {
const appConfig = await makeConfig(config, ctx);
// The AdminController can't dynamically import "bknd/dist/manifest.json"
// at runtime in the Workers runtime (workerd ignores package.json exports
// for runtime imports). Statically import it here so esbuild bundles the
// manifest into the Worker and pass it as the admin controller's manifest
// unless the caller explicitly provides one (or disables the admin).
if (appConfig.adminOptions !== false && !(appConfig.adminOptions as any)?.manifest) {
appConfig.adminOptions = {
...(appConfig.adminOptions as any),
manifest: adminUiManifest,
};
}
return await createRuntimeApp<Env>(
{
...appConfig,
+1 -7
View File
@@ -14,12 +14,6 @@ export interface EmitsEvents {
emgr: EventManager;
}
export type EventRegistryMap<
RegisteredEvents extends Record<string, EventClass> = Record<string, EventClass>,
> = {
[K in keyof RegisteredEvents]: RegisteredEvents[K];
};
// for compatibility, moved it to Event.ts
export type { EventClass };
@@ -71,7 +65,7 @@ export class EventManager<
return [...this.listeners];
}
get Events(): EventRegistryMap<RegisteredEvents> {
get Events(): { [K in keyof RegisteredEvents]: RegisteredEvents[K] } {
// proxy class to access events
return new Proxy(this, {
get: (_, prop: string) => {
+1 -1
View File
@@ -31,7 +31,7 @@ export type IndexMetadata = {
};
export interface SelectQueryBuilderExpression<O> extends AliasableExpression<O> {
readonly isSelectQueryBuilder: true;
get isSelectQueryBuilder(): true;
toOperationNode(): SelectQueryNode;
}
+11 -1
View File
@@ -312,7 +312,7 @@ export class Mutator<
const validated: any[] = [];
for (const row of data) {
const validatedData = {
let validatedData = {
...entity.getDefaultObject(),
...(await this.getValidatedData(row, "create")),
};
@@ -328,6 +328,16 @@ export class Mutator<
}
}
// primary
const primary = entity.getPrimaryField();
const primary_value = primary.getNewValue();
if (primary_value) {
validatedData = {
[primary.name]: primary_value,
...validatedData,
};
}
validated.push(validatedData);
}
+2 -5
View File
@@ -82,7 +82,7 @@ export * as AuthPermissions from "auth/auth-permissions";
*/
export { getExtensionFromName, getRandomizedFilename } from "media/utils";
import * as StorageEvents from "media/storage/events";
export const MediaEvents: typeof StorageEvents = {
export const MediaEvents = {
...StorageEvents,
};
export * as MediaPermissions from "media/media-permissions";
@@ -104,10 +104,7 @@ export { StorageCloudinaryAdapter } from "media/storage/adapters/cloudinary/Stor
* Data
*/
import { MutatorEvents, RepositoryEvents } from "data/events";
export const DatabaseEvents: typeof MutatorEvents & typeof RepositoryEvents = {
...MutatorEvents,
...RepositoryEvents,
};
export const DatabaseEvents = { ...MutatorEvents, ...RepositoryEvents };
export type {
RepoQuery,
RepoQueryIn,
@@ -1,11 +1,12 @@
import { retry, type TestRunner } from "core/test";
import type { StorageAdapter } from "media/storage/StorageAdapter";
import { randomString } from "bknd/utils";
import type { BunFile } from "bun";
export async function adapterTestSuite(
testRunner: TestRunner,
_adapter: StorageAdapter | (() => StorageAdapter),
file: File | import("bun").BunFile,
file: File | BunFile,
opts?: {
retries?: number;
retryTimeout?: number;
-15
View File
@@ -55,21 +55,6 @@ export class AppServer extends Module<AppServerConfig> {
const origin = this.config.cors.origin ?? "*";
const origins = origin.includes(",") ? origin.split(",").map((o) => o.trim()) : [origin];
const all_origins = origins.includes("*");
// Normalize trailing slashes so that /admin/ matches /admin.
// Hono's app.route doesn't match trailing slashes; host frameworks
// like Nuxt/Nitro redirect /admin -> /admin/. When running standalone
// we emit a 308 ourselves so the route layer sees a clean path without
// re-running the middleware chain (which would re-consume c.req.raw).
this.client.use("*", async (c, next) => {
const url = new URL(c.req.url);
if (url.pathname.length > 1 && url.pathname.endsWith("/")) {
url.pathname = url.pathname.replace(/\/+$/, "");
return c.redirect(url.toString(), 308);
}
return next();
});
this.client.use(
"*",
cors({
+1 -1
View File
@@ -1,6 +1,6 @@
import { MantineProvider } from "@mantine/core";
import { Notifications } from "@mantine/notifications";
import { type ReactNode } from "react";
import React, { type ReactNode } from "react";
import { BkndProvider } from "ui/client/bknd";
import { useTheme, type AppTheme } from "ui/client/use-theme";
import { Logo } from "ui/components/display/Logo";
+3 -4
View File
@@ -102,10 +102,9 @@ body,
/* Chrome, Edge, and Safari */
& *::-webkit-scrollbar {
@apply w-1;
}
& *::-webkit-scrollbar:horizontal {
@apply h-px;
&:horizontal {
@apply h-px;
}
}
& *::-webkit-scrollbar-track {
+1 -3
View File
@@ -3,10 +3,8 @@
"compilerOptions": {
"outDir": "./dist/types",
"rootDir": "./src",
"declaration": true,
"incremental": true,
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
},
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"exclude": ["node_modules", "__test__", "e2e"]
"exclude": ["./node_modules", "./__test__", "./e2e"]
}
+3 -4
View File
@@ -10,8 +10,7 @@
"noImplicitAny": false,
"allowJs": true,
"verbatimModuleSyntax": true,
"declaration": false,
"declarationMap": false,
"declaration": true,
"strict": true,
"allowUnusedLabels": false,
"allowUnreachableCode": false,
@@ -27,7 +26,7 @@
"esModuleInterop": true,
"skipLibCheck": true,
"rootDir": ".",
"outDir": "./dist",
"outDir": "./dist/types",
"paths": {
"*": ["./src/*"],
"bknd": ["./src/index.ts"],
@@ -48,5 +47,5 @@
"__test__",
"e2e/**/*.ts"
],
"exclude": ["node_modules", "dist", "**/*.d.ts"]
"exclude": ["node_modules", "dist", "dist/types", "**/*.d.ts"]
}
+8 -10
View File
@@ -1,17 +1,13 @@
import devServer from "@hono/vite-dev-server";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { devServerConfig } from "./src/adapter/vite/dev-server-config";
import tailwindcss from '@tailwindcss/postcss'
import tailwindcss from "@tailwindcss/vite";
import pkg from "./package.json" with { type: "json" };
// https://vitejs.dev/config/
export default defineConfig({
css: {
postcss: {
plugins: [tailwindcss()],
},
},
define: {
__isDev: process.env.NODE_ENV === "production" ? "0" : "1",
__version: JSON.stringify(pkg.version),
@@ -25,21 +21,23 @@ export default defineConfig({
overlay: true,
},
},
resolve: {
tsconfigPaths: true
},
plugins: [
react(),
tsconfigPaths({
// otherwise it'll throw an error because of examples/astro
ignoreConfigErrors: true,
}),
devServer({
...devServerConfig,
entry: "./vite.dev.ts",
//entry: "./vite.dev.code.ts",
}),
tailwindcss(),
],
build: {
manifest: true,
outDir: "./dist/static",
rolldownOptions: {
rollupOptions: {
input: "./src/ui/main.tsx",
},
},
+7 -3
View File
@@ -1,9 +1,13 @@
import { defineConfig } from "vitest/config";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
resolve: {
tsconfigPaths: true,
},
plugins: [
tsconfigPaths({
root: ".",
ignoreConfigErrors: true,
}) as any,
],
test: {
include: ["**/*.vi-test.ts", "**/*.vitest.ts"],
},
+1 -1
View File
@@ -35,7 +35,7 @@
"files": {
"includes": [
"**",
"!!**/.tsdown",
"!!**/.tsup",
"!!**/node_modules",
"!!**/.cache",
"!!**/.wrangler",
+1983 -1148
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -10,11 +10,11 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.9",
"@astrojs/react": "^6.0.1",
"@astrojs/check": "^0.9.4",
"@astrojs/react": "^4.2.2",
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"astro": "^7.0.7",
"astro": "^5.16.9",
"bknd": "file:../../app",
"react": "^19.1.0",
"react-dom": "^19.1.0",
+1
View File
@@ -1,4 +1,5 @@
---
import Card from "../components/Card.astro";
interface Props {
title: string;
}
+2 -2
View File
@@ -5,7 +5,7 @@
"type": "module",
"packageManager": "bun@1.3.3",
"engines": {
"node": ">=22.18"
"node": ">=22.13"
},
"scripts": {
"updater": "bun x npm-check-updates -ui",
@@ -18,7 +18,7 @@
"devDependencies": {
"@biomejs/biome": "2.3.3",
"@tsconfig/strictest": "^2.0.8",
"@types/bun": "^1.3.13",
"@types/bun": "^1.3.1",
"@types/lodash-es": "^4.17.12",
"miniflare": "^3.20250718.2",
"typescript": "^6.0.2",
+2 -3
View File
@@ -12,14 +12,13 @@
"README.md"
],
"dependencies": {
"@libsql/client": "^0.17.2",
"@libsql/client": "^0.14.0",
"hono": "^4.6.7"
},
"tsdown": {
"tsup": {
"entry": [
"src/index.ts"
],
"dts": false,
"minify": true,
"outDir": "dist",
"format": [
+5 -7
View File
@@ -4,15 +4,14 @@
"type": "module",
"sideEffects": false,
"scripts": {
"build": "rm -rf dist && bun tsdown && bun run build:types",
"watch": "bun tsdown --watch --onSuccess 'bun run build:types'",
"build:only": "rm -rf dist && bun tsdown",
"build": "rm -rf dist && bun tsup && bun run build:types",
"watch": "bun tsup --watch --onSuccess 'bun run build:types'",
"build:only": "rm -rf dist && bun tsup",
"types": "bun tsc -p tsconfig.json --noEmit --skipLibCheck",
"build:types": "bun tsc --emitDeclarationOnly",
"updater": "bun x npm-check-updates -ui",
"pack": "rm -rf *.tgz && npm pack && mv *.tgz latest.tgz",
"prepublishOnly": "bun run build",
"tsdown": "bunx tsdown"
"prepublishOnly": "bun run build"
},
"publishConfig": {
"access": "public"
@@ -34,12 +33,11 @@
"optionalDependencies": {
"@plasmicapp/loader-nextjs": "^1.0.0"
},
"tsdown": {
"tsup": {
"entry": [
"src/index.ts",
"src/nextjs/index.tsx"
],
"dts": false,
"minify": true,
"clean": true,
"external": [