Merge remote-tracking branch 'origin/main' into fix/with-builder-respect-alias
# Conflicts: # bun.lock
267
CONTRIBUTING.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# Contributing to bknd
|
||||
|
||||
Thank you for your interest in contributing to bknd. This guide will help you get started, understand the codebase, and submit contributions that align with the project's direction.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Before You Start](#before-you-start)
|
||||
- [Important Resources](#important-resources)
|
||||
- [Development Setup](#development-setup)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Understanding the Codebase](#understanding-the-codebase)
|
||||
- [Running Tests](#running-tests)
|
||||
- [Code Style](#code-style)
|
||||
- [Submitting Changes](#submitting-changes)
|
||||
- [AI-Generated Code](#ai-generated-code)
|
||||
- [Contributing to Documentation](#contributing-to-documentation)
|
||||
- [Reporting Bugs](#reporting-bugs)
|
||||
- [Getting Help](#getting-help)
|
||||
- [Contributors](#contributors)
|
||||
- [Maintainers](#maintainers)
|
||||
|
||||
## Before You Start
|
||||
|
||||
**Open a GitHub Issue before writing code.** This is the preferred way to propose any change. It lets maintainers and the community align on the approach before time is spent on implementation. If you have discussed a contribution in the [Discord server](https://discord.com/invite/952SFk8Tb8) instead, include a link to the relevant Discord thread in your pull request. Pull requests submitted without a corresponding issue or Discord discussion may be closed.
|
||||
|
||||
**Unsolicited architectural changes will be closed.** The internal architecture of bknd is intentional. Refactors, restructuring, or changes to core patterns must be discussed and approved before any code is written.
|
||||
|
||||
Contributions that are generally welcome (after opening an issue):
|
||||
|
||||
- Bug fixes
|
||||
- New adapters, strategies, or storage backends
|
||||
- Documentation improvements
|
||||
- New examples
|
||||
- Test coverage improvements
|
||||
|
||||
**A note on versioning**: bknd is pre-1.0 and under active development. Full backward compatibility is not guaranteed before v1.0.0. Contributors should be aware that APIs and internal interfaces may change between releases.
|
||||
|
||||
## Important Resources
|
||||
|
||||
- **Documentation**: https://docs.bknd.io
|
||||
- **Issue Tracker**: https://github.com/bknd-io/bknd/issues
|
||||
- **Discord**: https://discord.com/invite/952SFk8Tb8
|
||||
- **FAQ / Search**: https://www.answeroverflow.com/c/1308395750564302952
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 22.13 or higher
|
||||
- Bun 1.3.3
|
||||
|
||||
### Getting Started
|
||||
|
||||
1. Fork the repository and clone your fork.
|
||||
|
||||
2. Install dependencies from the repo root:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
3. Navigate to the main app package:
|
||||
|
||||
```bash
|
||||
cd app
|
||||
```
|
||||
|
||||
4. Copy the example environment file:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
The default `.env.example` includes a file-based SQLite database (`file:.db/dev.db`). You can change `VITE_DB_URL` to `:memory:` for an in-memory database instead.
|
||||
|
||||
5. Start the dev server:
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
The dev server runs on `http://localhost:28623` using Vite with Hono.
|
||||
|
||||
## Project Structure
|
||||
|
||||
This is a Bun monorepo using workspaces. The vast majority of the code lives in `app/`.
|
||||
|
||||
```
|
||||
bknd/
|
||||
app/ # Main "bknd" npm package (this is where most work happens)
|
||||
src/
|
||||
adapter/ # Runtime/framework adapters (node, bun, cloudflare, nextjs, astro, etc.)
|
||||
auth/ # Authentication module (strategies, sessions, user pool)
|
||||
cli/ # CLI commands
|
||||
core/ # Shared utilities, event system, drivers, server helpers
|
||||
data/ # Data module (entities, fields, relations, queries, schema)
|
||||
flows/ # Workflow engine and tasks
|
||||
media/ # Media module (storage adapters, uploads)
|
||||
modes/ # Configuration modes (code, hybrid, db)
|
||||
modules/ # Module system, MCP server, permissions framework
|
||||
plugins/ # Built-in plugins (auth, data, cloudflare, dev)
|
||||
ui/ # All frontend code
|
||||
client/ # TypeScript SDK and React hooks (exported as bknd/client)
|
||||
elements/ # React components for auth/media (exported as bknd/elements)
|
||||
(everything else) # Admin UI (exported as bknd/ui)
|
||||
App.ts # Central application orchestrator
|
||||
index.ts # Main package exports
|
||||
__test__/ # Unit tests (mirrors src/ structure)
|
||||
e2e/ # End-to-end tests (Playwright)
|
||||
build.ts # Build script (tsup/esbuild)
|
||||
build.cli.ts # CLI build script
|
||||
packages/ # Small satellite packages
|
||||
cli/ # Standalone CLI package (bknd-cli)
|
||||
plasmic/ # Plasmic integration
|
||||
postgres/ # Postgres helper
|
||||
sqlocal/ # SQLocal helper
|
||||
docs/ # Documentation site (Next.js + Fumadocs, deployed to docs.bknd.io)
|
||||
examples/ # Example projects across runtimes and frameworks
|
||||
docker/ # Docker configuration
|
||||
```
|
||||
|
||||
## Understanding the Codebase
|
||||
|
||||
### Path Aliases
|
||||
|
||||
The project uses TypeScript path aliases defined in `app/tsconfig.json`. Imports like `core/utils`, `data/connection`, or `auth/authenticate` resolve to directories under `app/src/`. When reading source code, keep this in mind -- an import like `import { Connection } from "data/connection"` refers to `app/src/data/connection`, not an external package.
|
||||
|
||||
### Module System
|
||||
|
||||
bknd is built around four core modules, each with its own schema, API routes, and permissions:
|
||||
|
||||
- **Data** -- entity definitions, field types, relations, queries (backed by Kysely)
|
||||
- **Auth** -- authentication strategies, sessions, user management
|
||||
- **Media** -- file storage with pluggable adapters (S3, Cloudinary, local filesystem, etc.)
|
||||
- **Flows** -- workflow automation and task execution
|
||||
|
||||
These modules are managed by the `ModuleManager` (in `app/src/modules/`), which handles configuration, building, and lifecycle.
|
||||
|
||||
### Adapter Pattern
|
||||
|
||||
Adapters in `app/src/adapter/` allow bknd to run on different runtimes and frameworks. Each adapter provides the glue between bknd's Hono-based server and a specific environment (Node, Bun, Cloudflare Workers, Next.js, Astro, etc.).
|
||||
|
||||
### Plugin System
|
||||
|
||||
Plugins (in `app/src/plugins/`) hook into the app lifecycle via callbacks like `beforeBuild`, `onBuilt`, `onServerInit`, and `schema`. They can add entities, register routes, and extend behavior.
|
||||
|
||||
### Build System
|
||||
|
||||
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
|
||||
- **Elements** -- standalone React components
|
||||
- **Adapters** -- all runtime/framework adapters
|
||||
|
||||
## Running Tests
|
||||
|
||||
All test commands run from the `app/` directory.
|
||||
|
||||
| Command | Runner | What it runs |
|
||||
|---|---|---|
|
||||
| `bun run test` | Bun test | Unit tests (`*.spec.ts` files) |
|
||||
| `bun run test:node` | Vitest | Node-specific tests (`*.vi-test.ts`, `*.vitest.ts`) |
|
||||
| `bun run test:e2e` | Playwright | End-to-end tests (`*.e2e-spec.ts` in `e2e/`) |
|
||||
| `bun run types` | TypeScript | Type checking (no emit) |
|
||||
|
||||
CI runs both Bun and Node tests, with a Postgres 17 service for database integration tests.
|
||||
|
||||
## Code Style
|
||||
|
||||
The project uses **Biome** for formatting and linting.
|
||||
|
||||
- 3-space indentation for JavaScript/TypeScript
|
||||
- 100-character line width
|
||||
- Spaces (not tabs)
|
||||
|
||||
To format and lint, run from the repo root:
|
||||
|
||||
```bash
|
||||
bunx biome format --write ./app
|
||||
bunx biome lint --changed --write ./app
|
||||
```
|
||||
|
||||
Run both before submitting a PR. CI will catch style issues, but it is better to fix them locally first.
|
||||
|
||||
## Submitting Changes
|
||||
|
||||
1. Open a GitHub Issue describing your proposed change, or link to a Discord thread where it was discussed. Wait for feedback before writing code.
|
||||
2. Fork the repo and create a branch from `main`.
|
||||
3. Keep changes focused and minimal. One PR per issue.
|
||||
4. Add or update tests if applicable.
|
||||
5. Run the test suite and linter before pushing.
|
||||
6. Open a pull request against `main`. Reference the issue number in the PR description.
|
||||
|
||||
Expect a response from maintainers, but be patient -- this is an actively developed project and review may take some time.
|
||||
|
||||
## AI-Generated Code
|
||||
|
||||
If you use AI tools to write or assist with your code, you must:
|
||||
|
||||
- **Fully review all AI-generated code yourself** before submitting. You are responsible for understanding and standing behind every line in your PR.
|
||||
- **Include the prompts used** in the PR description. This gives maintainers additional context for reviewing the code.
|
||||
|
||||
Pull requests with unreviewed AI-generated code will be closed.
|
||||
|
||||
## Contributing to Documentation
|
||||
|
||||
The documentation site lives in the `docs/` directory and is a **separate Next.js application** built with [Fumadocs](https://fumadocs.dev). It uses **npm** (not Bun) as its package manager.
|
||||
|
||||
### Docs Setup
|
||||
|
||||
```bash
|
||||
cd docs
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The docs dev server runs on `http://localhost:3000`.
|
||||
|
||||
### Where Documentation Lives
|
||||
|
||||
Documentation content is written in MDX and located in `docs/content/docs/`. The directory is organized into:
|
||||
|
||||
- `(documentation)/` -- core documentation pages
|
||||
- `guide/` -- guides and tutorials
|
||||
- `api-reference/` -- API reference (partially auto-generated from OpenAPI)
|
||||
|
||||
To add or edit a page, create or modify an `.mdx` file in the appropriate directory. Page metadata is defined via frontmatter at the top of each file. Navigation order is controlled by `meta.json` files in each directory.
|
||||
|
||||
### Building the Docs
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
This generates the OpenAPI and MCP reference pages before building the Next.js site. Make sure the build succeeds locally before submitting a docs PR.
|
||||
|
||||
## Reporting Bugs
|
||||
|
||||
Open a GitHub Issue with:
|
||||
|
||||
- A clear title describing the problem.
|
||||
- Steps to reproduce the bug.
|
||||
- Expected behavior vs. actual behavior.
|
||||
- Your environment (runtime, database, adapter, bknd version).
|
||||
- Any relevant error messages or logs.
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Discord**: https://discord.com/invite/952SFk8Tb8
|
||||
- **FAQ / Search**: https://www.answeroverflow.com/c/1308395750564302952
|
||||
- **GitHub Issues**: https://github.com/bknd-io/bknd/issues
|
||||
|
||||
## Contributors
|
||||
|
||||
Thank you to everyone who has contributed to bknd.
|
||||
|
||||
<a href="https://github.com/bknd-io/bknd/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=bknd-io/bknd" />
|
||||
</a>
|
||||
|
||||
Made with [contrib.rocks](https://contrib.rocks).
|
||||
|
||||
## Maintainers
|
||||
|
||||
- **Dennis** ([@dswbx](https://github.com/dswbx)) -- Creator and maintainer of bknd
|
||||
- **Cameron Pak** ([@cameronapak](https://github.com/cameronapak)) -- Maintainer of bknd docs
|
||||
16
README.md
@@ -165,3 +165,19 @@ npx bknd run
|
||||
```bash
|
||||
npm install bknd
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! Please read our [CONTRIBUTING.md](CONTRIBUTING.md) before getting started. It covers dev setup, project structure, testing, code style, and how to submit changes.
|
||||
|
||||
Before writing code, please open a GitHub Issue or start a conversation in [Discord](https://discord.com/invite/952SFk8Tb8) so we can align on the approach.
|
||||
|
||||
## Contributors
|
||||
|
||||
Thank you to everyone who has contributed to bknd.
|
||||
|
||||
<a href="https://github.com/bknd-io/bknd/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=bknd-io/bknd" />
|
||||
</a>
|
||||
|
||||
Made with [contrib.rocks](https://contrib.rocks).
|
||||
|
||||
@@ -1,66 +1,76 @@
|
||||
import { expect, describe, it, beforeAll, afterAll, mock } from "bun:test";
|
||||
import * as adapter from "adapter";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import { disableConsoleLog, enableConsoleLog, omitKeys } from "core/utils";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { omitKeys } from "core/utils";
|
||||
|
||||
const stripConnection = <T extends Record<string, any>>(cfg: T) =>
|
||||
omitKeys(cfg, ["connection"]);
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("adapter", () => {
|
||||
it("makes config", async () => {
|
||||
expect(omitKeys(await adapter.makeConfig({}), ["connection"])).toEqual({});
|
||||
expect(
|
||||
omitKeys(await adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"]),
|
||||
).toEqual({});
|
||||
describe("makeConfig", () => {
|
||||
it("returns empty config for empty inputs", async () => {
|
||||
const cases: Array<Parameters<typeof adapter.makeConfig>> = [
|
||||
[{}],
|
||||
[{}, { env: { TEST: "test" } }],
|
||||
];
|
||||
|
||||
// merges everything returned from `app` with the config
|
||||
expect(
|
||||
omitKeys(
|
||||
await adapter.makeConfig(
|
||||
{ app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) },
|
||||
{ env: { TEST: "test" } },
|
||||
),
|
||||
["connection"],
|
||||
),
|
||||
).toEqual({
|
||||
config: { server: { cors: { origin: "test" } } },
|
||||
});
|
||||
});
|
||||
for (const args of cases) {
|
||||
const cfg = await adapter.makeConfig(...(args as any));
|
||||
expect(stripConnection(cfg)).toEqual({});
|
||||
}
|
||||
});
|
||||
|
||||
it("allows all properties in app function", async () => {
|
||||
const called = mock(() => null);
|
||||
const config = await adapter.makeConfig(
|
||||
{
|
||||
app: (env) => ({
|
||||
connection: { url: "test" },
|
||||
config: { server: { cors: { origin: "test" } } },
|
||||
options: {
|
||||
mode: "db",
|
||||
},
|
||||
onBuilt: () => {
|
||||
called();
|
||||
expect(env).toEqual({ foo: "bar" });
|
||||
},
|
||||
}),
|
||||
},
|
||||
{ foo: "bar" },
|
||||
it("merges app output into config", async () => {
|
||||
const cfg = await adapter.makeConfig(
|
||||
{ app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) },
|
||||
{ env: { TEST: "test" } },
|
||||
);
|
||||
expect(config.connection).toEqual({ url: "test" });
|
||||
expect(config.config).toEqual({ server: { cors: { origin: "test" } } });
|
||||
expect(config.options).toEqual({ mode: "db" });
|
||||
await config.onBuilt?.(null as any);
|
||||
expect(called).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
adapterTestSuite(bunTestRunner, {
|
||||
expect(stripConnection(cfg)).toEqual({
|
||||
config: { server: { cors: { origin: "test" } } },
|
||||
});
|
||||
});
|
||||
|
||||
it("allows all properties in app() result", async () => {
|
||||
const called = mock(() => null);
|
||||
|
||||
const cfg = await adapter.makeConfig(
|
||||
{
|
||||
app: (env) => ({
|
||||
connection: { url: "test" },
|
||||
config: { server: { cors: { origin: "test" } } },
|
||||
options: { mode: "db" as const },
|
||||
onBuilt: () => {
|
||||
called();
|
||||
expect(env).toEqual({ foo: "bar" });
|
||||
},
|
||||
}),
|
||||
},
|
||||
{ foo: "bar" },
|
||||
);
|
||||
|
||||
expect(cfg.connection).toEqual({ url: "test" });
|
||||
expect(cfg.config).toEqual({ server: { cors: { origin: "test" } } });
|
||||
expect(cfg.options).toEqual({ mode: "db" });
|
||||
|
||||
await cfg.onBuilt?.({} as any);
|
||||
expect(called).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("adapter test suites", () => {
|
||||
adapterTestSuite(bunTestRunner, {
|
||||
makeApp: adapter.createFrameworkApp,
|
||||
label: "framework app",
|
||||
});
|
||||
});
|
||||
|
||||
adapterTestSuite(bunTestRunner, {
|
||||
adapterTestSuite(bunTestRunner, {
|
||||
makeApp: adapter.createRuntimeApp,
|
||||
label: "runtime app",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
610
app/build.ts
@@ -2,7 +2,7 @@ import { $ } from "bun";
|
||||
import * as tsup from "tsup";
|
||||
import pkg from "./package.json" with { type: "json" };
|
||||
import c from "picocolors";
|
||||
import { watch as fsWatch } from "node:fs";
|
||||
import { watch as fsWatch, readdirSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
@@ -14,164 +14,178 @@ const clean = args.includes("--clean");
|
||||
|
||||
// silence tsup
|
||||
const oldConsole = {
|
||||
log: console.log,
|
||||
warn: console.warn,
|
||||
log: console.log,
|
||||
warn: console.warn,
|
||||
};
|
||||
console.log = () => {};
|
||||
console.warn = () => {};
|
||||
|
||||
const define = {
|
||||
__isDev: "0",
|
||||
__version: JSON.stringify(pkg.version),
|
||||
__isDev: "0",
|
||||
__version: JSON.stringify(pkg.version),
|
||||
};
|
||||
|
||||
if (clean) {
|
||||
console.info("Cleaning dist (w/o static)");
|
||||
await $`find dist -mindepth 1 ! -path "dist/static/*" ! -path "dist/static" -exec rm -rf {} +`;
|
||||
// Cross-platform clean: remove all files/folders in dist except static
|
||||
const distPath = join(import.meta.dir, "dist");
|
||||
try {
|
||||
const entries = readdirSync(distPath);
|
||||
for (const entry of entries) {
|
||||
if (entry === "static") continue;
|
||||
const entryPath = join(distPath, entry);
|
||||
rmSync(entryPath, { recursive: true, force: true });
|
||||
}
|
||||
} catch (e) {
|
||||
// dist may not exist yet, ignore
|
||||
}
|
||||
}
|
||||
|
||||
let types_running = false;
|
||||
function buildTypes() {
|
||||
if (types_running || !types) return;
|
||||
types_running = true;
|
||||
if (types_running || !types) return;
|
||||
types_running = true;
|
||||
|
||||
Bun.spawn(["bun", "build:types"], {
|
||||
stdout: "inherit",
|
||||
onExit: () => {
|
||||
oldConsole.log(c.cyan("[Types]"), c.green("built"));
|
||||
Bun.spawn(["bun", "tsc-alias"], {
|
||||
stdout: "inherit",
|
||||
onExit: () => {
|
||||
oldConsole.log(c.cyan("[Types]"), c.green("aliased"));
|
||||
types_running = false;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
Bun.spawn(["bun", "build:types"], {
|
||||
stdout: "inherit",
|
||||
onExit: () => {
|
||||
oldConsole.log(c.cyan("[Types]"), c.green("built"));
|
||||
Bun.spawn(["bun", "tsc-alias"], {
|
||||
stdout: "inherit",
|
||||
onExit: () => {
|
||||
oldConsole.log(c.cyan("[Types]"), c.green("aliased"));
|
||||
types_running = false;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (types && !watch) {
|
||||
buildTypes();
|
||||
buildTypes();
|
||||
}
|
||||
|
||||
let watcher_timeout: any;
|
||||
function delayTypes() {
|
||||
if (!watch || !types) return;
|
||||
if (watcher_timeout) {
|
||||
clearTimeout(watcher_timeout);
|
||||
}
|
||||
watcher_timeout = setTimeout(buildTypes, 1000);
|
||||
if (!watch || !types) return;
|
||||
if (watcher_timeout) {
|
||||
clearTimeout(watcher_timeout);
|
||||
}
|
||||
watcher_timeout = setTimeout(buildTypes, 1000);
|
||||
}
|
||||
|
||||
const dependencies = Object.keys(pkg.dependencies);
|
||||
|
||||
// collection of always-external packages
|
||||
const external = [
|
||||
...dependencies,
|
||||
"bun:test",
|
||||
"node:test",
|
||||
"node:assert/strict",
|
||||
"@libsql/client",
|
||||
"bknd",
|
||||
/^bknd\/.*/,
|
||||
"jsonv-ts",
|
||||
/^jsonv-ts\/.*/,
|
||||
...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 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"));
|
||||
},
|
||||
});
|
||||
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,
|
||||
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;
|
||||
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;
|
||||
|
||||
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 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 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"));
|
||||
},
|
||||
});
|
||||
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"));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,171 +194,185 @@ async function buildUi() {
|
||||
* - ui/client is external, and after built replaced with "bknd/client"
|
||||
*/
|
||||
async function buildUiElements() {
|
||||
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"));
|
||||
},
|
||||
});
|
||||
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<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 : []),
|
||||
],
|
||||
};
|
||||
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
|
||||
tsup.build({
|
||||
...baseConfig(""),
|
||||
target: "esnext",
|
||||
platform: "neutral",
|
||||
entry: ["src/adapter/index.ts"],
|
||||
outDir: "dist/adapter",
|
||||
// only way to keep @vite-ignore comments
|
||||
minify: 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"],
|
||||
}),
|
||||
),
|
||||
tsup.build(
|
||||
baseConfig("bun", {
|
||||
external: [/^bun\:.*/],
|
||||
}),
|
||||
),
|
||||
tsup.build(baseConfig("astro")),
|
||||
tsup.build(baseConfig("aws")),
|
||||
tsup.build(
|
||||
baseConfig("cloudflare", {
|
||||
external: ["wrangler", "node:process"],
|
||||
}),
|
||||
),
|
||||
tsup.build(
|
||||
baseConfig("cloudflare/proxy", {
|
||||
target: "esnext",
|
||||
entry: ["src/adapter/cloudflare/proxy.ts"],
|
||||
outDir: "dist/adapter/cloudflare",
|
||||
metafile: false,
|
||||
external: [/bknd/, "wrangler", "node:process"],
|
||||
}),
|
||||
),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("vite"),
|
||||
platform: "node",
|
||||
// specific adatpers
|
||||
tsup.build(baseConfig("react-router")),
|
||||
tsup.build(
|
||||
baseConfig("browser", {
|
||||
external: [/^sqlocal\/?.*?/, "wouter"],
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("nextjs"),
|
||||
platform: "node",
|
||||
),
|
||||
tsup.build(
|
||||
baseConfig("bun", {
|
||||
external: [/^bun\:.*/],
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("sveltekit"),
|
||||
platform: "node",
|
||||
),
|
||||
tsup.build(baseConfig("astro")),
|
||||
tsup.build(baseConfig("aws")),
|
||||
tsup.build(
|
||||
baseConfig("cloudflare", {
|
||||
external: ["wrangler", "node:process"],
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("node"),
|
||||
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"],
|
||||
}),
|
||||
),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("sqlite/edge"),
|
||||
entry: ["src/adapter/sqlite/edge.ts"],
|
||||
outDir: "dist/adapter/sqlite",
|
||||
metafile: false,
|
||||
}),
|
||||
tsup.build({
|
||||
...baseConfig("vite"),
|
||||
platform: "node",
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("sqlite/node"),
|
||||
entry: ["src/adapter/sqlite/node.ts"],
|
||||
outDir: "dist/adapter/sqlite",
|
||||
platform: "node",
|
||||
metafile: false,
|
||||
}),
|
||||
tsup.build({
|
||||
...baseConfig("nextjs"),
|
||||
platform: "node",
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("sqlite/bun"),
|
||||
entry: ["src/adapter/sqlite/bun.ts"],
|
||||
outDir: "dist/adapter/sqlite",
|
||||
metafile: false,
|
||||
external: [/^bun\:.*/],
|
||||
}),
|
||||
]);
|
||||
tsup.build({
|
||||
...baseConfig("tanstack-start"),
|
||||
platform: "node",
|
||||
}),
|
||||
|
||||
tsup.build({
|
||||
...baseConfig("sveltekit"),
|
||||
platform: "node",
|
||||
}),
|
||||
|
||||
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\:.*/],
|
||||
}),
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
async function buildAll() {
|
||||
await Promise.all([buildApi(), buildUi(), buildUiElements(), buildAdapters()]);
|
||||
await Promise.all([
|
||||
buildApi(),
|
||||
buildUi(),
|
||||
buildUiElements(),
|
||||
buildAdapters(),
|
||||
]);
|
||||
}
|
||||
|
||||
// initial build
|
||||
@@ -352,39 +380,47 @@ await buildAll();
|
||||
|
||||
// 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(() => {});
|
||||
}
|
||||
|
||||
@@ -1,44 +1,47 @@
|
||||
const adapter = process.env.TEST_ADAPTER;
|
||||
|
||||
const default_config = {
|
||||
media_adapter: "local",
|
||||
base_path: "",
|
||||
media_adapter: "local",
|
||||
base_path: "",
|
||||
} as const;
|
||||
|
||||
const configs = {
|
||||
cloudflare: {
|
||||
media_adapter: "r2",
|
||||
},
|
||||
"react-router": {
|
||||
base_path: "/admin",
|
||||
},
|
||||
nextjs: {
|
||||
base_path: "/admin",
|
||||
},
|
||||
astro: {
|
||||
base_path: "/admin",
|
||||
},
|
||||
node: {
|
||||
base_path: "",
|
||||
},
|
||||
bun: {
|
||||
base_path: "",
|
||||
},
|
||||
cloudflare: {
|
||||
media_adapter: "r2",
|
||||
},
|
||||
"react-router": {
|
||||
base_path: "/admin",
|
||||
},
|
||||
nextjs: {
|
||||
base_path: "/admin",
|
||||
},
|
||||
astro: {
|
||||
base_path: "/admin",
|
||||
},
|
||||
node: {
|
||||
base_path: "",
|
||||
},
|
||||
bun: {
|
||||
base_path: "",
|
||||
},
|
||||
"tanstack-start": {
|
||||
base_path: "/admin",
|
||||
},
|
||||
};
|
||||
|
||||
export function getAdapterConfig(): typeof default_config {
|
||||
if (adapter) {
|
||||
if (!configs[adapter]) {
|
||||
console.warn(
|
||||
`Adapter "${adapter}" not found. Available adapters: ${Object.keys(configs).join(", ")}`,
|
||||
);
|
||||
} else {
|
||||
return {
|
||||
...default_config,
|
||||
...configs[adapter],
|
||||
};
|
||||
}
|
||||
}
|
||||
if (adapter) {
|
||||
if (!configs[adapter]) {
|
||||
console.warn(
|
||||
`Adapter "${adapter}" not found. Available adapters: ${Object.keys(configs).join(", ")}`,
|
||||
);
|
||||
} else {
|
||||
return {
|
||||
...default_config,
|
||||
...configs[adapter],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return default_config;
|
||||
return default_config;
|
||||
}
|
||||
|
||||
@@ -268,6 +268,11 @@
|
||||
"import": "./dist/adapter/browser/index.js",
|
||||
"require": "./dist/adapter/browser/index.js"
|
||||
},
|
||||
"./adapter/tanstack-start": {
|
||||
"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/main.css",
|
||||
"./dist/styles.css": "./dist/ui/styles.css",
|
||||
"./dist/manifest.json": "./dist/static/.vite/manifest.json",
|
||||
@@ -286,6 +291,7 @@
|
||||
"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"]
|
||||
}
|
||||
},
|
||||
|
||||
1
app/src/adapter/tanstack-start/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./tanstack-start.adapter";
|
||||
@@ -0,0 +1,16 @@
|
||||
import { afterAll, beforeAll, describe } from "bun:test";
|
||||
import * as tanstackStart from "./tanstack-start.adapter";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import type { TanstackStartConfig } from "./tanstack-start.adapter";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("tanstack start adapter", () => {
|
||||
adapterTestSuite<TanstackStartConfig>(bunTestRunner, {
|
||||
makeApp: tanstackStart.getApp,
|
||||
makeHandler: tanstackStart.serve,
|
||||
});
|
||||
});
|
||||
33
app/src/adapter/tanstack-start/tanstack-start.adapter.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createFrameworkApp, type FrameworkBkndConfig } from "bknd/adapter";
|
||||
|
||||
export type TanstackStartEnv = NodeJS.ProcessEnv;
|
||||
|
||||
export type TanstackStartConfig<Env = TanstackStartEnv> =
|
||||
FrameworkBkndConfig<Env>;
|
||||
|
||||
/**
|
||||
* Get bknd app instance
|
||||
* @param config - bknd configuration
|
||||
* @param args - environment variables
|
||||
*/
|
||||
export async function getApp<Env = TanstackStartEnv>(
|
||||
config: TanstackStartConfig<Env> = {},
|
||||
args: Env = process.env as Env,
|
||||
) {
|
||||
return await createFrameworkApp(config, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create request handler for src/routes/api.$.ts
|
||||
* @param config - bknd configuration
|
||||
* @param args - environment variables
|
||||
*/
|
||||
export function serve<Env = TanstackStartEnv>(
|
||||
config: TanstackStartConfig<Env> = {},
|
||||
args: Env = process.env as Env,
|
||||
) {
|
||||
return async (request: Request) => {
|
||||
const app = await getApp(config, args);
|
||||
return app.fetch(request);
|
||||
};
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { TypeTable } from "fumadocs-ui/components/type-table";
|
||||
|
||||
|
||||
bknd features an integrated Admin UI that can be used to:
|
||||
- fully manage your backend visually when run in [`db` mode](/usage/introduction/#ui-only-mode)
|
||||
- fully manage your backend visually when run in [`db` mode](/usage/setup/#ui-only-mode)
|
||||
- manage your database contents
|
||||
- manage your media contents
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ If the connection object is omitted, the app will try to use an in-memory databa
|
||||
|
||||
As configuration, 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
|
||||
to the latest version upon boot ([`db` mode](/usage/introduction#ui-only-mode) only). The default configuration looks like this:
|
||||
to the latest version upon boot ([`db` mode](/usage/setup#ui-only-mode) only). The default configuration looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -170,7 +170,7 @@ export const prerender = false;
|
||||
theme: "dark",
|
||||
logo_return_path: "/../"
|
||||
}}
|
||||
client:only
|
||||
client:only="react"
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
{
|
||||
"pages": ["nextjs", "react-router", "astro", "sveltekit", "vite"]
|
||||
"pages": [
|
||||
"nextjs",
|
||||
"react-router",
|
||||
"astro",
|
||||
"sveltekit",
|
||||
"tanstack-start",
|
||||
"vite"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
---
|
||||
title: "Tanstack Start"
|
||||
description: "Run bknd inside Tanstack Start"
|
||||
tags: ["documentation"]
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
To get started with Tanstack Start and bknd, create a new Tanstack Start project by following the [official guide](https://tanstack.com/start/latest/docs/framework/react/getting-started#start-a-new-project-from-scratch), and then install bknd as a dependency:
|
||||
|
||||
<Tabs groupId='package-manager' persist items={[ 'npm', 'pnpm', 'yarn', 'bun']}>
|
||||
|
||||
```bash tab="npm"
|
||||
npm install bknd
|
||||
```
|
||||
|
||||
```bash tab="pnpm"
|
||||
pnpm install bknd
|
||||
```
|
||||
|
||||
```bash tab="yarn"
|
||||
yarn add bknd
|
||||
```
|
||||
|
||||
```bash tab="bun"
|
||||
bun add bknd
|
||||
```
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Configuration
|
||||
|
||||
<Callout type="warning">
|
||||
When run with Node.js, a version of 22 (LTS) or higher is required. Please
|
||||
verify your version by running `node -v`, and
|
||||
[upgrade](https://nodejs.org/en/download/) if necessary.
|
||||
</Callout>
|
||||
|
||||
Now create a `bknd.config.ts` file in the root of your project:
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import { type TanstackStartConfig } from "bknd/adapter/tanstack-start";
|
||||
import { em, entity, text, boolean } from "bknd";
|
||||
|
||||
const schema = em({
|
||||
todos: entity("todos", {
|
||||
title: text(),
|
||||
done: boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
export default {
|
||||
connection: {
|
||||
url: "file:data.db",
|
||||
},
|
||||
config: {
|
||||
data: schema.toJSON(),
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: {
|
||||
secret: "random_gibberish_please_change_this",
|
||||
// use something like `openssl rand -hex 32` for production
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
// the seed option is only executed if the database was empty
|
||||
seed: async (ctx) => {
|
||||
// create some entries
|
||||
await ctx.em.mutator("todos").insertMany([
|
||||
{ title: "Learn bknd", done: true },
|
||||
{ title: "Build something cool", done: false },
|
||||
]);
|
||||
|
||||
// and create a user
|
||||
await ctx.app.module.auth.createUser({
|
||||
email: "test@bknd.io",
|
||||
password: "12345678",
|
||||
});
|
||||
},
|
||||
},
|
||||
} satisfies TanstackStartConfig;
|
||||
```
|
||||
|
||||
For more information about the connection object, refer to the [Database](/usage/database) guide.
|
||||
|
||||
|
||||
See [bknd.config.ts](/extending/config) for more information on how to configure bknd. The `TanstackStartConfig` type extends the base config type with the following properties:
|
||||
|
||||
```typescript
|
||||
export type TanstackStartConfig<Env = TanstackStartEnv> = FrameworkBkndConfig<Env>;
|
||||
```
|
||||
|
||||
## Serve the API
|
||||
|
||||
The Tanstack Start adapter uses Tanstack Start's hooks mechanism to handle API requests. Create a `/src/routes/api.$.ts` file:
|
||||
|
||||
```typescript title="/src/routes/api.$.ts"
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import config from "../../bknd.config";
|
||||
import { serve } from "bknd/adapter/tanstack-start";
|
||||
|
||||
const handler = serve(config);
|
||||
|
||||
export const Route = createFileRoute("/api/$")({
|
||||
server: {
|
||||
handlers: {
|
||||
ANY: async ({ request }) => await handler(request),
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Create a helper file to instantiate the bknd instance and retrieve the API, importing the configuration from the `bknd.config.ts` file:
|
||||
|
||||
```ts title="src/bknd.ts"
|
||||
import config from "../bknd.config";
|
||||
import { getApp } from "bknd/adapter/tanstack-start";
|
||||
|
||||
export async function getApi({
|
||||
headers,
|
||||
verify,
|
||||
}: {
|
||||
verify?: boolean;
|
||||
headers?: Headers;
|
||||
}) {
|
||||
const app = await getApp(config, process.env);
|
||||
|
||||
if (verify) {
|
||||
const api = app.getApi({ headers });
|
||||
await api.verifyAuth();
|
||||
return api;
|
||||
}
|
||||
|
||||
return app.getApi();
|
||||
};
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
The adapter uses `process.env` to access environment variables, this works because Tanstack Start uses Nitro underneath and it will use polyfills for `process.env` making it platform/runtime agnostic.
|
||||
</Callout>
|
||||
|
||||
## Enabling the Admin UI
|
||||
|
||||
Create a page at /src/routes/admin.$.tsx:
|
||||
|
||||
```typescript title="/src/routes/admin.$.tsx"
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useAuth } from "bknd/client";
|
||||
import "bknd/dist/styles.css";
|
||||
import { Admin } from "bknd/ui";
|
||||
|
||||
export const Route = createFileRoute("/admin/$")({
|
||||
ssr: false, // [!code highlight] "data-only" works too
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { user } = useAuth();
|
||||
return (
|
||||
<Admin
|
||||
withProvider={{ user: user }}
|
||||
config={{
|
||||
basepath: "/admin",
|
||||
logo_return_path: "/../",
|
||||
theme: "system",
|
||||
}}
|
||||
baseUrl={import.meta.env.APP_URL}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
Admin routes are expected to run on the client not using `ssr: false` will cause errors like `✘ [ERROR] No matching export in "node_modules/json-schema-library/dist/index.mjs" for import "Draft2019"` and production build might fail because of this
|
||||
</Callout>
|
||||
|
||||
|
||||
## Example usage of the API
|
||||
|
||||
You can use the `getApp` function to access the bknd API in your app:
|
||||
These are a few examples how you can validate user and handle server-side requests using `createServerFn`.
|
||||
|
||||
```typescript title="src/routes/index.tsx"
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { getApi } from "@/bknd";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
|
||||
export const getTodo = createServerFn()
|
||||
.handler(async () => {
|
||||
const api = await getApi({});
|
||||
const limit = 5;
|
||||
const todos = await api.data.readMany("todos", { limit, sort: "-id" });
|
||||
return { todos };
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
ssr: false,
|
||||
component: App,
|
||||
loader: async () => {
|
||||
return await getTodo();
|
||||
},
|
||||
});
|
||||
|
||||
function App() {
|
||||
const { todos } = Route.useLoaderData();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Todos</h1>
|
||||
<ul>
|
||||
{todos.map((todo) => (
|
||||
<li key={todo.id}>{todo.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Using authentication
|
||||
|
||||
To use authentication in your app, pass the request headers to the API:
|
||||
|
||||
```typescript title="src/routes/user.tsx"
|
||||
import { getApi } from "@/bknd";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { getRequest } from "@tanstack/react-start/server";
|
||||
|
||||
export const getUser = createServerFn()
|
||||
.handler(async () => {
|
||||
const request = getRequest();
|
||||
const api = await getApi({ verify: true, headers: request.headers });
|
||||
const user = api.getUser();
|
||||
return { user };
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/user")({
|
||||
component: RouteComponent,
|
||||
loader: async () => {
|
||||
return { user: await getUser() };
|
||||
},
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { user } = Route.useLoaderData();
|
||||
return (
|
||||
<div>
|
||||
{user ? (
|
||||
<>
|
||||
Logged in as {user.email}.{" "}
|
||||
<Link
|
||||
className="font-medium underline"
|
||||
to={"/api/auth/logout" as string}
|
||||
>
|
||||
Logout
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
<p>
|
||||
Not logged in.
|
||||
<Link
|
||||
className="font-medium underline"
|
||||
to={"/admin/auth/login" as string}
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
</p>
|
||||
<p className="text-xs opacity-50">
|
||||
Sign in with:
|
||||
<b>
|
||||
<code>test@bknd.io</code>
|
||||
</b>
|
||||
/
|
||||
<b>
|
||||
<code>12345678</code>
|
||||
</b>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Check the [Tanstack Start repository example](https://github.com/bknd-io/bknd/tree/main/examples/tanstack-start) for more implementation details.
|
||||
@@ -33,6 +33,12 @@ bknd seamlessly integrates with popular frameworks, allowing you to use what you
|
||||
href="/integration/sveltekit"
|
||||
/>
|
||||
|
||||
<Card
|
||||
icon={<Icon icon="simple-icons:tanstack" className="text-fd-primary !size-6" />}
|
||||
title="Tanstack Start"
|
||||
href="/integration/tanstack-start"
|
||||
/>
|
||||
|
||||
<Card title="Yours missing?" href="https://github.com/bknd-io/bknd/issues/new">
|
||||
Create a new issue to request a guide for your framework.
|
||||
</Card>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"start",
|
||||
"motivation",
|
||||
"---Usage---",
|
||||
"./usage/introduction",
|
||||
"./usage/setup",
|
||||
"./usage/database",
|
||||
"./usage/cli",
|
||||
"./usage/sdk",
|
||||
|
||||
@@ -30,6 +30,25 @@ With a focus on flexibility and ease of integration, bknd's authentication syste
|
||||
|
||||
---
|
||||
|
||||
## Server Module: `module.auth`
|
||||
|
||||
### `auth.changePassword([userId], [password])`
|
||||
|
||||
To change a user's password, use the `changePassword` method:
|
||||
|
||||
```ts
|
||||
await app.module.auth.changePassword(user.id, password);
|
||||
```
|
||||
|
||||
This method updates the password for the specified user.
|
||||
- `userId`: The ID of the user whose password should be changed.
|
||||
- `password`: The new password value.
|
||||
|
||||
This method throws an error if:
|
||||
- The user with the given `userId` is not found.
|
||||
- The user's authentication strategy is not `"password"` (e.g. the user registered via an OAuth provider).
|
||||
|
||||
|
||||
## Securing Your Admin Portal
|
||||
|
||||
<Callout type="warn" title="Do not enable the Guard without an admin user">
|
||||
|
||||
@@ -6,26 +6,36 @@ tags: ["documentation"]
|
||||
|
||||
import { Icon } from "@iconify/react";
|
||||
import { examples } from "@/app/_components/StackBlitz";
|
||||
import { SquareMousePointer, Code, Blend } from 'lucide-react';
|
||||
import { SquareMousePointer, Code, Blend, Rocket } from 'lucide-react';
|
||||
|
||||
<Callout type="warning" title="bknd is in beta">
|
||||
## bknd is a lightweight batteries-included backend that embeds into your frontend app
|
||||
|
||||
<Callout type="warning" title="We are in beta">
|
||||
We're making great progress towards v1, but don't recommend production use yet.
|
||||
Follow along for updates on [GitHub Releases](https://github.com/bknd-io/bknd/releases) and in our [Discord community](https://discord.gg/952SFk8Tb8)
|
||||
</Callout>
|
||||
|
||||
Welcome! bknd is the instant backend for your frontend with full REST API's, admin dashboard, auth and user management, file upload management, [type-safe SDK](/usage/sdk), [React hooks](/usage/react), and plugins to extend the bknd _(like our resend plugin for sending emails)_. bknd can be hosted with your server-side rendered (SSR) web app or hosted as a standalone app.
|
||||
|
||||
bknd is incredibly lightweight and built upon Web Standards, so that you can bring bknd anywhere JavaScript runs.
|
||||
bknd includes full REST APIs, an admin dashboard, auth, media uploads, a [type-safe SDK](/usage/sdk), [React hooks](/usage/react), and plugins to extend it. Host it with your SSR app or as a standalone service. Built on Web Standards, it runs anywhere JavaScript runs.
|
||||
|
||||
Bring your [favorite frontend](./#start-with-a-frameworkruntime) and [favorite SQL database](./#use-your-favorite-sql-database), and we'll bring the ~~backend~~ bknd.
|
||||
|
||||
## Quickstart: bknd server and admin dashboard demo
|
||||
<Cards>
|
||||
<Card
|
||||
href="/motivation"
|
||||
title="Learn about why we built bknd"
|
||||
icon={<Rocket />}
|
||||
>
|
||||
Why another backend system?
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
## Quickstart
|
||||
|
||||
<Callout type="info" title="This demo bknd instance is for playing and learning">
|
||||
Don't worry about messing anything up in this stage since you're learning the ropes of bknd. If you want to start over, please delete the generated `data.db` database file and follow this tutorial again.
|
||||
</Callout>
|
||||
|
||||
Enter the following command to spin up a bknd instance via the [bknd CLI](/usage/cli):
|
||||
Spin up a bknd instance via the [bknd CLI](/usage/cli):
|
||||
|
||||
<Tabs groupId='package-manager' persist items={[ 'npm','bun' ]}>
|
||||
|
||||
@@ -39,11 +49,9 @@ bunx bknd run
|
||||
|
||||
</Tabs>
|
||||
|
||||
This will create a local `data.db` SQLite database file in the folder you ran the command and start the bknd web server at http://localhost:1337.
|
||||
This creates a local `data.db` SQLite database and starts the bknd web server at http://localhost:1337.
|
||||
|
||||
By default, access to the admin dashboard is open and not guarded. This is intentional because bknd uses an opt-in philosophy model to allow for quick prototyping and intentional configuration. To restrict access and prevent unauthorized use, let's enable authentication and guard the dashboard.
|
||||
|
||||
We will create a user and an admin role. We will then apply the admin role to the user. Then, we can lock down the admin dashboard by enabling the guard, securing access to your bknd app. Let's begin.
|
||||
By default, the admin dashboard is open and not guarded. This is intentional — bknd uses an opt-in philosophy to allow quick prototyping. Let's enable authentication and guard the dashboard to secure it.
|
||||
|
||||
1. Visit http://localhost:1337/auth/settings. Toggle "Authentication Enabled" to enable auth. Select "Update" to save.
|
||||
2. Visit http://localhost:1337/data/entity/users. Create a user by selecting "New User" and entering an email and password.
|
||||
@@ -52,29 +60,27 @@ We will create a user and an admin role. We will then apply the admin role to th
|
||||
5. It's time to guard your admin dashboard. Visit http://localhost:1337/settings/auth. Select "Edit". Scroll to "Guard" and enable it. Then, select "Save". _(This should log you out!)_
|
||||
6. Now, log in to your secured admin dashboard at http://localhost:1337/auth/login.
|
||||
|
||||
You did it! 🥳 You've successfully started the bknd server and admin dashboard, created an admin user, and protected your bknd app by enabling the guard.
|
||||
You did it! You've started the bknd server, created an admin user, and protected your app by enabling the guard.
|
||||
|
||||
What you've just experienced is called UI-only mode, where the bknd data and configuration is managed via the admin dashboard UI. Some may prefer the UI-only mode, while others may prefer the code-only mode.
|
||||
## Modes
|
||||
|
||||
### bknd modes
|
||||
|
||||
bknd supports multiple modes to suit your needs.
|
||||
What you just experienced is **UI-only mode** — bknd's data and configuration is managed entirely via the admin dashboard. But that's not the only way to use bknd:
|
||||
|
||||
<Cards className="grid-cols-3">
|
||||
<Card title="UI-only" href="/usage/introduction#ui-only-mode" icon={<SquareMousePointer className="text-fd-primary !size-6" />}>
|
||||
<Card title="UI-only" href="/usage/setup#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" />}>
|
||||
<Card title="Code-only" href="/usage/setup#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" />}>
|
||||
<Card title="Hybrid" href="/usage/setup#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>
|
||||
|
||||
### Demo bknd in the browser
|
||||
Learn more about each mode and the underlying configuration in [Setup & Modes](/usage/setup).
|
||||
|
||||
Here is a preview of bknd in StackBlitz:
|
||||
## Try bknd in the browser
|
||||
|
||||
<Card className="p-0 pb-1">
|
||||
<StackBlitz path="github/bknd-io/bknd-demo" initialPath="/" />
|
||||
@@ -90,7 +96,7 @@ Here is a preview of bknd in StackBlitz:
|
||||
|
||||
## Start with a Framework/Runtime
|
||||
|
||||
Start by using the integration guide for these popular frameworks/runtimes. There will be more in the future, so stay tuned!
|
||||
Pick your framework or runtime to get started.
|
||||
|
||||
<Cards>
|
||||
<Card icon={<Icon icon="tabler:brand-nextjs" className="text-fd-primary !size-6" />} title="NextJS" href="/integration/nextjs" />
|
||||
@@ -144,6 +150,12 @@ Start by using the integration guide for these popular frameworks/runtimes. Ther
|
||||
href="/integration/sveltekit"
|
||||
/>
|
||||
|
||||
<Card
|
||||
icon={<Icon icon="simple-icons:tanstack" className="text-fd-primary !size-6" />}
|
||||
title="Tanstack Start"
|
||||
href="/integration/tanstack-start"
|
||||
/>
|
||||
|
||||
<Card
|
||||
icon={<Icon icon="tabler:lambda" className="text-fd-primary !size-6" />}
|
||||
title="AWS Lambda"
|
||||
|
||||
@@ -263,7 +263,7 @@ To automatically sync your secrets to a file, you may also use the [`syncSecrets
|
||||
|
||||
## 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.
|
||||
Sync your database can be useful when running in [`code`](/usage/setup/#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
|
||||
|
||||
@@ -310,7 +310,7 @@ const app = createApp({ connection });
|
||||
|
||||
## Data Structure
|
||||
|
||||
To provide a database structure, you can pass `config` to the creation of an app. In [`db` mode](/usage/introduction#ui-only-mode), the data structure is only respected if the database is empty. If you made updates, ensure to delete the database first, or perform updates through the Admin UI.
|
||||
To provide a database structure, you can pass `config` to the creation of an app. In [`db` mode](/usage/setup#ui-only-mode), the data structure is only respected if the database is empty. If you made updates, ensure to delete the database first, or perform updates through the Admin UI.
|
||||
|
||||
Here is a quick example:
|
||||
|
||||
@@ -506,9 +506,9 @@ const app = createApp({
|
||||
});
|
||||
```
|
||||
|
||||
Note that in [`db` mode](/usage/introduction#ui-only-mode), the seed function will only be executed on app's first boot. If a configuration already exists in the database, it will not be executed.
|
||||
Note that in [`db` mode](/usage/setup#ui-only-mode), the seed function will only be executed on app's first boot. If a configuration already exists in the database, it will not be executed.
|
||||
|
||||
In [`code` mode](/usage/introduction#code-only-mode), the seed function will not be automatically executed. You can manually execute it by running the following command:
|
||||
In [`code` mode](/usage/setup#code-only-mode), the seed function will not be automatically executed. You can manually execute it by running the following command:
|
||||
|
||||
```bash
|
||||
npx bknd sync --seed --force
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
---
|
||||
title: "Introduction"
|
||||
description: "Setting up bknd"
|
||||
icon: Pin
|
||||
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:
|
||||
|
||||
1. [Run it using the CLI](/usage/cli): That's the easiest and fastest way to get started.
|
||||
2. Use a runtime like [Node](/integration/node), [Bun](/integration/bun) or
|
||||
[Cloudflare](/integration/cloudflare) (workerd). This will run the API and UI in the runtime's
|
||||
native server and serves the UI assets statically from `node_modules`.
|
||||
3. Run it inside your React framework of choice like [Next.js](/integration/nextjs),
|
||||
[Astro](/integration/astro) or [Remix](/integration/remix).
|
||||
|
||||
There is also a fourth option, which is running it inside a
|
||||
[Docker container](/integration/docker). This is essentially a wrapper around the CLI.
|
||||
|
||||
## Basic setup
|
||||
|
||||
Regardless of the method you choose, at the end all adapters come down to the actual
|
||||
instantiation of the `App`, which in raw looks like this:
|
||||
|
||||
```typescript
|
||||
import { createApp, type BkndConfig } from "bknd";
|
||||
|
||||
// create the app
|
||||
const config = {
|
||||
/* ... */
|
||||
} satisfies BkndConfig;
|
||||
const app = createApp(config);
|
||||
|
||||
// build the app
|
||||
await app.build();
|
||||
|
||||
// export for Web API compliant envs
|
||||
export default app;
|
||||
```
|
||||
|
||||
In Web API compliant environments, all you have to do is to default exporting the app, as it implements the `Fetch` API. In case an explicit `fetch` export is needed, you can use the `app.fetch` property.
|
||||
|
||||
```typescript
|
||||
const app = /* ... */;
|
||||
export default {
|
||||
fetch: app.fetch,
|
||||
}
|
||||
```
|
||||
|
||||
Check the integration details for your specific runtime or framework in the [integration](/integration/introduction) section.
|
||||
|
||||
## Modes
|
||||
|
||||
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 (default)" 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;
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
Note that when using the default UI-mode, the initial configuration using the `config` property will only be applied if the database is empty.
|
||||
</Callout>
|
||||
|
||||
|
||||
### 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;
|
||||
```
|
||||
|
||||
Unlike the UI-only mode, the configuration passed to `config` is always applied. In case you make data structure changes, you may need to sync the schema to the database manually, e.g. using the [sync command](/usage/cli#syncing-the-database-sync).
|
||||
|
||||
### 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) |
|
||||
|
||||
|
||||
## Mode helpers
|
||||
|
||||
To make the setup using your preferred mode easier, there are mode helpers for [`code`](/usage/introduction#code-only-mode) and [`hybrid`](/usage/introduction#hybrid-mode) modes.
|
||||
|
||||
* built-in syncing of config, types and secrets
|
||||
* let bknd automatically sync the data schema in development
|
||||
* automatically switch modes in hybrid (from db to code) in production
|
||||
* automatically skip config validation in production to boost performance
|
||||
|
||||
To use it, you have to wrap your configuration in a mode helper, e.g. for `code` mode using the Bun adapter:
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import { code, type CodeMode } from "bknd/modes";
|
||||
import { type BunBkndConfig, writer } from "bknd/adapter/bun";
|
||||
|
||||
export default code<BunBkndConfig>({
|
||||
// some normal bun bknd config
|
||||
connection: { url: "file:data.db" },
|
||||
// ...
|
||||
// a writer is required, to sync the types
|
||||
writer,
|
||||
// (optional) mode specific config
|
||||
isProduction: Bun.env.NODE_ENV === "production",
|
||||
typesFilePath: "bknd-types.d.ts",
|
||||
// (optional) e.g. have the schema synced if !isProduction
|
||||
syncSchema: {
|
||||
force: true,
|
||||
drop: true,
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Similarily, for `hybrid` mode:
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import { hybrid, type HybridMode } from "bknd/modes";
|
||||
import { type BunBkndConfig, writer, reader } from "bknd/adapter/bun";
|
||||
|
||||
export default hybrid<BunBkndConfig>({
|
||||
// some normal bun bknd config
|
||||
connection: { url: "file:data.db" },
|
||||
// ...
|
||||
// reader/writer are required, to sync the types and config
|
||||
writer,
|
||||
reader,
|
||||
// supply secrets
|
||||
secrets: await Bun.file(".env.local").json(),
|
||||
// (optional) mode specific config
|
||||
isProduction: Bun.env.NODE_ENV === "production",
|
||||
typesFilePath: "bknd-types.d.ts",
|
||||
configFilePath: "bknd-config.json",
|
||||
// (optional) and have them automatically written if !isProduction
|
||||
syncSecrets: {
|
||||
outFile: ".env.local",
|
||||
format: "env",
|
||||
includeSecrets: true,
|
||||
},
|
||||
// (optional) also have the schema synced if !isProduction
|
||||
syncSchema: {
|
||||
force: true,
|
||||
drop: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
180
docs/content/docs/(documentation)/usage/setup.mdx
Normal file
@@ -0,0 +1,180 @@
|
||||
---
|
||||
title: "Setup & Modes"
|
||||
description: "Choose a mode and get bknd running with your app"
|
||||
icon: Pin
|
||||
tags: ["documentation"]
|
||||
---
|
||||
|
||||
import { SquareMousePointer, Code, Blend } from 'lucide-react';
|
||||
|
||||
bknd supports three modes. Each mode determines how your backend is configured and where that configuration lives.
|
||||
|
||||
## Choose your mode
|
||||
|
||||
<Cards className="grid-cols-1 sm:grid-cols-2 md:grid-cols-3">
|
||||
<Card title="UI-only (default)" href="#ui-only-mode" icon={<SquareMousePointer className="text-fd-primary !size-6" />}>
|
||||
Configure visually via the Admin UI. Config is stored in the database.
|
||||
</Card>
|
||||
<Card title="Code-only" href="#code-only-mode" icon={<Code className="text-fd-primary !size-6" />}>
|
||||
Define your schema in code with a Drizzle-like API. Config lives in your codebase.
|
||||
</Card>
|
||||
<Card title="Hybrid" href="#hybrid-mode" icon={<Blend className="text-fd-primary !size-6" />}>
|
||||
Use the Admin UI in development, export to code for production.
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
**Not sure which to pick?**
|
||||
|
||||
- **UI-only** is best for prototyping and small projects. No code needed — configure everything through the dashboard.
|
||||
- **Code-only** is best for teams, CI/CD, and version-controlled schemas. You define your data structure programmatically.
|
||||
- **Hybrid** gives you the best of both — visual configuration while developing, locked-down code config in production.
|
||||
|
||||
You can always change modes later. Start with UI-only if you're exploring.
|
||||
|
||||
---
|
||||
|
||||
## UI-only mode
|
||||
|
||||
This is the default. Run bknd and configure everything through the Admin UI. No setup code required beyond a database connection.
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import type { BkndConfig } from "bknd";
|
||||
|
||||
export default {
|
||||
connection: { url: "file:data.db" },
|
||||
} satisfies BkndConfig;
|
||||
```
|
||||
|
||||
If you want to provide an initial data structure (entities, auth settings, etc.), pass it via `config`. It will only be applied when the database is empty.
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import type { BkndConfig } from "bknd";
|
||||
|
||||
export default {
|
||||
connection: { url: "file:data.db" },
|
||||
config: {
|
||||
auth: { enabled: true },
|
||||
},
|
||||
} satisfies BkndConfig;
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
In UI-only mode, the `config` property is only applied on first boot. After that, all changes are made through the Admin UI.
|
||||
</Callout>
|
||||
|
||||
**Next step:** Pick your [framework or runtime integration](/integration/introduction) to wire bknd into your app.
|
||||
|
||||
---
|
||||
|
||||
## Code-only mode
|
||||
|
||||
Define your data structure programmatically with a Drizzle-like API. The Admin UI becomes read-only for configuration — you still use it to manage data.
|
||||
|
||||
```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 {
|
||||
connection: { url: "file:data.db" },
|
||||
config: {
|
||||
data: schema.toJSON(),
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: { secret: secureRandomString(64) },
|
||||
},
|
||||
},
|
||||
options: {
|
||||
mode: "code",
|
||||
},
|
||||
} satisfies BkndConfig;
|
||||
```
|
||||
|
||||
Unlike UI-only mode, the `config` is applied on every boot. If you change the schema, you may need to [sync the database](/usage/cli#syncing-the-database-sync).
|
||||
|
||||
**Next step:** Pick your [framework or runtime integration](/integration/introduction) to wire bknd into your app.
|
||||
|
||||
---
|
||||
|
||||
## Hybrid mode
|
||||
|
||||
Use the Admin UI to configure your backend while developing. When you're ready to deploy, export the config and run in code mode for production.
|
||||
|
||||
```typescript title="bknd.config.ts"
|
||||
import type { BkndConfig } from "bknd";
|
||||
import appConfig from "./appconfig.json" with { type: "json" };
|
||||
|
||||
export default {
|
||||
connection: { url: "file:data.db" },
|
||||
config: appConfig,
|
||||
options: {
|
||||
mode: process.env.NODE_ENV === "development" ? "db" : "code",
|
||||
manager: {
|
||||
secrets: process.env,
|
||||
},
|
||||
},
|
||||
} satisfies BkndConfig;
|
||||
```
|
||||
|
||||
To export your config, secrets, and types, use the CLI or plugins:
|
||||
|
||||
| What | CLI Command | Plugin |
|
||||
|------|-------------|--------|
|
||||
| Configuration | [`bknd config`](/usage/cli/#getting-the-configuration-config) | [`syncConfig`](/extending/plugins/#syncconfig) |
|
||||
| Secrets | [`bknd secrets`](/usage/cli/#getting-the-secrets-secrets) | [`syncSecrets`](/extending/plugins/#syncsecrets) |
|
||||
| Types | [`bknd types`](/usage/cli/#generating-types-types) | [`syncTypes`](/extending/plugins/#synctypes) |
|
||||
|
||||
**Next step:** Pick your [framework or runtime integration](/integration/introduction) to wire bknd into your app.
|
||||
|
||||
---
|
||||
|
||||
## Mode helpers
|
||||
|
||||
For code and hybrid modes, bknd provides helper functions that handle syncing, mode switching, and schema validation automatically.
|
||||
|
||||
```typescript title="bknd.config.ts (code mode with Bun)"
|
||||
import { code } from "bknd/modes";
|
||||
import { type BunBkndConfig, writer } from "bknd/adapter/bun";
|
||||
|
||||
export default code<BunBkndConfig>({
|
||||
connection: { url: "file:data.db" },
|
||||
writer,
|
||||
isProduction: Bun.env.NODE_ENV === "production",
|
||||
typesFilePath: "bknd-types.d.ts",
|
||||
});
|
||||
```
|
||||
|
||||
```typescript title="bknd.config.ts (hybrid mode with Bun)"
|
||||
import { hybrid } from "bknd/modes";
|
||||
import { type BunBkndConfig, writer, reader } from "bknd/adapter/bun";
|
||||
|
||||
export default hybrid<BunBkndConfig>({
|
||||
connection: { url: "file:data.db" },
|
||||
writer,
|
||||
reader,
|
||||
secrets: await Bun.file(".env.local").json(),
|
||||
isProduction: Bun.env.NODE_ENV === "production",
|
||||
typesFilePath: "bknd-types.d.ts",
|
||||
configFilePath: "bknd-config.json",
|
||||
});
|
||||
```
|
||||
|
||||
Mode helpers give you:
|
||||
- Built-in syncing of config, types, and secrets
|
||||
- Automatic schema syncing in development
|
||||
- Automatic mode switching (db → code) in production for hybrid
|
||||
- Skipped config validation in production for faster boot
|
||||
|
||||
---
|
||||
|
||||
## Further reading
|
||||
|
||||
- [Framework & runtime integrations](/integration/introduction) — wire bknd into Next.js, Astro, Bun, Cloudflare, etc.
|
||||
- [Database configuration](/usage/database) — choose and configure your SQL database
|
||||
- [Configuration reference](/extending/config) — full `BkndConfig` API, plugins, events, and lifecycle hooks
|
||||
@@ -9,4 +9,9 @@ export const redirectsConfig = [
|
||||
destination: "/start",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/usage/introduction",
|
||||
destination: "/usage/setup",
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
|
||||
16
examples/tanstack-start/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
count.txt
|
||||
.env
|
||||
.nitro
|
||||
.tanstack
|
||||
.wrangler
|
||||
.output
|
||||
.vinxi
|
||||
todos.json
|
||||
public/uploads
|
||||
data.db
|
||||
src/routeTree.gen.ts
|
||||
11
examples/tanstack-start/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"files.watcherExclude": {
|
||||
"**/routeTree.gen.ts": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/routeTree.gen.ts": true
|
||||
},
|
||||
"files.readonlyInclude": {
|
||||
"**/routeTree.gen.ts": true
|
||||
}
|
||||
}
|
||||
28
examples/tanstack-start/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# bknd + Tanstack Start Example
|
||||
|
||||
This is a minimal example to shows how to integrate bknd with Tanstack Start.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
bun install
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
1. **`bknd.config.ts`** - bknd configuration with database connection, schema, and seed data
|
||||
2. **`src/routes/api.$.ts`** - Handles `/api/*` requests for bknd
|
||||
3. **`src/routes/index.tsx`** - Using `getApp()` to fetch data in loader
|
||||
3. **`src/routes/ssr.tsx`** - Server Side example with `getApp()` to fetch data on server
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `GET /admin` - for Admin Dashboard
|
||||
- `GET /api/data/entity/todos` - List todos (requires auth)
|
||||
- `POST /api/auth/password/login` - Login
|
||||
|
||||
## Test Credentials
|
||||
|
||||
- Email: `test@bknd.io`
|
||||
- Password: `12345678`
|
||||
55
examples/tanstack-start/bknd.config.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { em, entity, text, boolean } from "bknd";
|
||||
import { registerLocalMediaAdapter } from "bknd/adapter/node";
|
||||
import { TanstackStartConfig } from "bknd/adapter/tanstack-start";
|
||||
|
||||
const local = registerLocalMediaAdapter();
|
||||
|
||||
const schema = em({
|
||||
todos: entity("todos", {
|
||||
title: text(),
|
||||
done: boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
// register your schema to get automatic type completion
|
||||
type Database = (typeof schema)["DB"];
|
||||
declare module "bknd" {
|
||||
interface DB extends Database {}
|
||||
}
|
||||
|
||||
export default {
|
||||
connection: {
|
||||
url: "file:data.db",
|
||||
},
|
||||
options: {
|
||||
// the seed option is only executed if the database was empty
|
||||
seed: async (ctx) => {
|
||||
// create some entries
|
||||
await ctx.em.mutator("todos").insertMany([
|
||||
{ title: "Learn bknd", done: true },
|
||||
{ title: "Build something cool", done: false },
|
||||
]);
|
||||
|
||||
// and create a user
|
||||
await ctx.app.module.auth.createUser({
|
||||
email: "test@bknd.io",
|
||||
password: "12345678",
|
||||
});
|
||||
},
|
||||
},
|
||||
config: {
|
||||
data: schema.toJSON(),
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: {
|
||||
secret: "random_gibberish_please_change_this",
|
||||
},
|
||||
},
|
||||
media: {
|
||||
enabled: true,
|
||||
adapter: local({
|
||||
path: "./public/uploads",
|
||||
}),
|
||||
},
|
||||
},
|
||||
} satisfies TanstackStartConfig;
|
||||
42
examples/tanstack-start/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "my-bknd-app",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev --port 3000",
|
||||
"build": "vite build",
|
||||
"start": "vite preview",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-devtools": "^0.7.0",
|
||||
"@tanstack/react-router": "^1.132.0",
|
||||
"@tanstack/react-router-devtools": "^1.132.0",
|
||||
"@tanstack/react-router-ssr-query": "^1.131.7",
|
||||
"@tanstack/react-start": "^1.132.0",
|
||||
"@tanstack/router-plugin": "^1.132.0",
|
||||
"bknd": "file:../../app",
|
||||
"lucide-react": "^0.561.0",
|
||||
"nitro": "^3.0.1-alpha.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vite-tsconfig-paths": "^6.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/devtools-vite": "^0.3.11",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"jsdom": "^27.0.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^7.1.7",
|
||||
"vitest": "^3.0.5",
|
||||
"web-vitals": "^5.1.0"
|
||||
}
|
||||
}
|
||||
BIN
examples/tanstack-start/public/bknd.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
14
examples/tanstack-start/public/bknd.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
width="578"
|
||||
height="188"
|
||||
viewBox="0 0 578 188"
|
||||
fill="black"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M41.5 34C37.0817 34 33.5 37.5817 33.5 42V146C33.5 150.418 37.0817 154 41.5 154H158.5C162.918 154 166.5 150.418 166.5 146V42C166.5 37.5817 162.918 34 158.5 34H41.5ZM123.434 113.942C124.126 111.752 124.5 109.42 124.5 107C124.5 94.2975 114.203 84 101.5 84C99.1907 84 96.9608 84.3403 94.8579 84.9736L87.2208 65.1172C90.9181 63.4922 93.5 59.7976 93.5 55.5C93.5 49.701 88.799 45 83 45C77.201 45 72.5 49.701 72.5 55.5C72.5 61.299 77.201 66 83 66C83.4453 66 83.8841 65.9723 84.3148 65.9185L92.0483 86.0256C87.1368 88.2423 83.1434 92.1335 80.7957 96.9714L65.4253 91.1648C65.4746 90.7835 65.5 90.3947 65.5 90C65.5 85.0294 61.4706 81 56.5 81C51.5294 81 47.5 85.0294 47.5 90C47.5 94.9706 51.5294 99 56.5 99C60.0181 99 63.0648 96.9814 64.5449 94.0392L79.6655 99.7514C78.9094 102.03 78.5 104.467 78.5 107C78.5 110.387 79.2321 113.603 80.5466 116.498L69.0273 123.731C67.1012 121.449 64.2199 120 61 120C55.201 120 50.5 124.701 50.5 130.5C50.5 136.299 55.201 141 61 141C66.799 141 71.5 136.299 71.5 130.5C71.5 128.997 71.1844 127.569 70.6158 126.276L81.9667 119.149C86.0275 125.664 93.2574 130 101.5 130C110.722 130 118.677 124.572 122.343 116.737L132.747 120.899C132.585 121.573 132.5 122.276 132.5 123C132.5 127.971 136.529 132 141.5 132C146.471 132 150.5 127.971 150.5 123C150.5 118.029 146.471 114 141.5 114C138.32 114 135.525 115.649 133.925 118.139L123.434 113.942Z"
|
||||
/>
|
||||
<path d="M243.9 151.5C240.4 151.5 237 151 233.7 150C230.4 149 227.4 147.65 224.7 145.95C222 144.15 219.75 142.15 217.95 139.95C216.15 137.65 215 135.3 214.5 132.9L219.3 131.1L218.25 149.7H198.15V39H219.45V89.25L215.4 87.6C216 85.2 217.15 82.9 218.85 80.7C220.55 78.4 222.7 76.4 225.3 74.7C227.9 72.9 230.75 71.5 233.85 70.5C236.95 69.5 240.15 69 243.45 69C250.35 69 256.5 70.8 261.9 74.4C267.3 77.9 271.55 82.75 274.65 88.95C277.85 95.15 279.45 102.25 279.45 110.25C279.45 118.25 277.9 125.35 274.8 131.55C271.7 137.75 267.45 142.65 262.05 146.25C256.75 149.75 250.7 151.5 243.9 151.5ZM238.8 133.35C242.8 133.35 246.25 132.4 249.15 130.5C252.15 128.5 254.5 125.8 256.2 122.4C257.9 118.9 258.75 114.85 258.75 110.25C258.75 105.75 257.9 101.75 256.2 98.25C254.6 94.75 252.3 92.05 249.3 90.15C246.3 88.25 242.8 87.3 238.8 87.3C234.8 87.3 231.3 88.25 228.3 90.15C225.3 92.05 222.95 94.75 221.25 98.25C219.55 101.75 218.7 105.75 218.7 110.25C218.7 114.85 219.55 118.9 221.25 122.4C222.95 125.8 225.3 128.5 228.3 130.5C231.3 132.4 234.8 133.35 238.8 133.35ZM308.312 126.15L302.012 108.6L339.512 70.65H367.562L308.312 126.15ZM288.062 150V39H309.362V150H288.062ZM341.762 150L313.262 114.15L328.262 102.15L367.412 150H341.762ZM371.675 150V70.65H392.075L392.675 86.85L388.475 88.65C389.575 85.05 391.525 81.8 394.325 78.9C397.225 75.9 400.675 73.5 404.675 71.7C408.675 69.9 412.875 69 417.275 69C423.275 69 428.275 70.2 432.275 72.6C436.375 75 439.425 78.65 441.425 83.55C443.525 88.35 444.575 94.3 444.575 101.4V150H423.275V103.05C423.275 99.45 422.775 96.45 421.775 94.05C420.775 91.65 419.225 89.9 417.125 88.8C415.125 87.6 412.625 87.1 409.625 87.3C407.225 87.3 404.975 87.7 402.875 88.5C400.875 89.2 399.125 90.25 397.625 91.65C396.225 93.05 395.075 94.65 394.175 96.45C393.375 98.25 392.975 100.2 392.975 102.3V150H382.475C380.175 150 378.125 150 376.325 150C374.525 150 372.975 150 371.675 150ZM488.536 151.5C481.636 151.5 475.436 149.75 469.936 146.25C464.436 142.65 460.086 137.8 456.886 131.7C453.786 125.5 452.236 118.35 452.236 110.25C452.236 102.35 453.786 95.3 456.886 89.1C460.086 82.9 464.386 78 469.786 74.4C475.286 70.8 481.536 69 488.536 69C492.236 69 495.786 69.6 499.186 70.8C502.686 71.9 505.786 73.45 508.486 75.45C511.286 77.45 513.536 79.7 515.236 82.2C516.936 84.6 517.886 87.15 518.086 89.85L512.686 90.75V39H533.986V150H513.886L512.986 131.7L517.186 132.15C516.986 134.65 516.086 137.05 514.486 139.35C512.886 141.65 510.736 143.75 508.036 145.65C505.436 147.45 502.436 148.9 499.036 150C495.736 151 492.236 151.5 488.536 151.5ZM493.336 133.8C497.336 133.8 500.836 132.8 503.836 130.8C506.836 128.8 509.186 126.05 510.886 122.55C512.586 119.05 513.436 114.95 513.436 110.25C513.436 105.65 512.586 101.6 510.886 98.1C509.186 94.5 506.836 91.75 503.836 89.85C500.836 87.85 497.336 86.85 493.336 86.85C489.336 86.85 485.836 87.85 482.836 89.85C479.936 91.75 477.636 94.5 475.936 98.1C474.336 101.6 473.536 105.65 473.536 110.25C473.536 114.95 474.336 119.05 475.936 122.55C477.636 126.05 479.936 128.8 482.836 130.8C485.836 132.8 489.336 133.8 493.336 133.8Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
BIN
examples/tanstack-start/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
1
examples/tanstack-start/public/file.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
1
examples/tanstack-start/public/globe.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
25
examples/tanstack-start/public/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "TanStack App",
|
||||
"name": "Create TanStack App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
examples/tanstack-start/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
BIN
examples/tanstack-start/public/tanstack-circle-logo.png
Normal file
|
After Width: | Height: | Size: 259 KiB |
|
After Width: | Height: | Size: 15 KiB |
1
examples/tanstack-start/public/window.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
20
examples/tanstack-start/src/bknd.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import config from "../bknd.config";
|
||||
import { getApp } from "bknd/adapter/tanstack-start";
|
||||
|
||||
export async function getApi({
|
||||
headers,
|
||||
verify,
|
||||
}: {
|
||||
verify?: boolean;
|
||||
headers?: Headers;
|
||||
}) {
|
||||
const app = await getApp(config, process.env);
|
||||
|
||||
if (verify) {
|
||||
const api = app.getApi({ headers });
|
||||
await api.verifyAuth();
|
||||
return api;
|
||||
}
|
||||
|
||||
return app.getApi();
|
||||
}
|
||||
52
examples/tanstack-start/src/components/Footer.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useRouterState, Link } from "@tanstack/react-router";
|
||||
|
||||
export function Footer() {
|
||||
const routerState = useRouterState();
|
||||
const pathname = routerState.location.pathname;
|
||||
|
||||
return (
|
||||
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
||||
<Link
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
to={pathname === "/" ? "/ssr" : ("/" as string)}
|
||||
>
|
||||
<img
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
{pathname === "/" ? "SSR" : "Home"}
|
||||
</Link>
|
||||
<Link
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
to={"/admin" as string}
|
||||
>
|
||||
<img
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Admin
|
||||
</Link>
|
||||
<Link
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
to={"https://bknd.io" as string}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to bknd.io →
|
||||
</Link>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
9
examples/tanstack-start/src/components/List.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
export const List = ({ items = [] }: { items: React.ReactNode[] }) => (
|
||||
<ol className="list-inside list-decimal text-sm text-center sm:text-left">
|
||||
{items.map((item, i) => (
|
||||
<li key={i} className={i < items.length - 1 ? "mb-2" : ""}>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
12
examples/tanstack-start/src/logo.svg
Normal file
|
After Width: | Height: | Size: 19 KiB |
122
examples/tanstack-start/src/routeTree.gen.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as SsrRouteImport } from './routes/ssr'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as ApiSplatRouteImport } from './routes/api.$'
|
||||
import { Route as AdminSplatRouteImport } from './routes/admin.$'
|
||||
|
||||
const SsrRoute = SsrRouteImport.update({
|
||||
id: '/ssr',
|
||||
path: '/ssr',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ApiSplatRoute = ApiSplatRouteImport.update({
|
||||
id: '/api/$',
|
||||
path: '/api/$',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AdminSplatRoute = AdminSplatRouteImport.update({
|
||||
id: '/admin/$',
|
||||
path: '/admin/$',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/ssr': typeof SsrRoute
|
||||
'/admin/$': typeof AdminSplatRoute
|
||||
'/api/$': typeof ApiSplatRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/ssr': typeof SsrRoute
|
||||
'/admin/$': typeof AdminSplatRoute
|
||||
'/api/$': typeof ApiSplatRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/ssr': typeof SsrRoute
|
||||
'/admin/$': typeof AdminSplatRoute
|
||||
'/api/$': typeof ApiSplatRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/ssr' | '/admin/$' | '/api/$'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/ssr' | '/admin/$' | '/api/$'
|
||||
id: '__root__' | '/' | '/ssr' | '/admin/$' | '/api/$'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
SsrRoute: typeof SsrRoute
|
||||
AdminSplatRoute: typeof AdminSplatRoute
|
||||
ApiSplatRoute: typeof ApiSplatRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/ssr': {
|
||||
id: '/ssr'
|
||||
path: '/ssr'
|
||||
fullPath: '/ssr'
|
||||
preLoaderRoute: typeof SsrRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/api/$': {
|
||||
id: '/api/$'
|
||||
path: '/api/$'
|
||||
fullPath: '/api/$'
|
||||
preLoaderRoute: typeof ApiSplatRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/admin/$': {
|
||||
id: '/admin/$'
|
||||
path: '/admin/$'
|
||||
fullPath: '/admin/$'
|
||||
preLoaderRoute: typeof AdminSplatRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
SsrRoute: SsrRoute,
|
||||
AdminSplatRoute: AdminSplatRoute,
|
||||
ApiSplatRoute: ApiSplatRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
import type { getRouter } from './router.tsx'
|
||||
import type { createStart } from '@tanstack/react-start'
|
||||
declare module '@tanstack/react-start' {
|
||||
interface Register {
|
||||
ssr: true
|
||||
router: Awaited<ReturnType<typeof getRouter>>
|
||||
}
|
||||
}
|
||||
17
examples/tanstack-start/src/router.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createRouter } from '@tanstack/react-router'
|
||||
|
||||
// Import the generated route tree
|
||||
import { routeTree } from './routeTree.gen'
|
||||
|
||||
// Create a new router instance
|
||||
export const getRouter = () => {
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: {},
|
||||
|
||||
scrollRestoration: true,
|
||||
defaultPreloadStaleTime: 0,
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
58
examples/tanstack-start/src/routes/__root.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
||||
import { TanStackDevtools } from "@tanstack/react-devtools";
|
||||
import { ClientProvider } from "bknd/client";
|
||||
|
||||
import appCss from "../styles.css?url";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
charSet: "utf-8",
|
||||
},
|
||||
{
|
||||
name: "viewport",
|
||||
content: "width=device-width, initial-scale=1",
|
||||
},
|
||||
{
|
||||
title: "TanStack 🤝 Bknd.io",
|
||||
},
|
||||
],
|
||||
links: [
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: appCss,
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
shellComponent: RootDocument,
|
||||
});
|
||||
|
||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
<ClientProvider verbose baseUrl={import.meta.env.APP_URL}>
|
||||
{children}
|
||||
</ClientProvider>
|
||||
<TanStackDevtools
|
||||
config={{
|
||||
position: "bottom-right",
|
||||
}}
|
||||
plugins={[
|
||||
{
|
||||
name: "Tanstack Router",
|
||||
render: <TanStackRouterDevtoolsPanel />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
23
examples/tanstack-start/src/routes/admin.$.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useAuth } from "bknd/client";
|
||||
import "bknd/dist/styles.css";
|
||||
import { Admin } from "bknd/ui";
|
||||
|
||||
export const Route = createFileRoute("/admin/$")({
|
||||
ssr: false, // "data-only" works too
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { user } = useAuth();
|
||||
return (
|
||||
<Admin
|
||||
withProvider={{ user: user }}
|
||||
config={{
|
||||
basepath: "/admin",
|
||||
logo_return_path: "/../",
|
||||
}}
|
||||
baseUrl={import.meta.env.APP_URL}
|
||||
/>
|
||||
);
|
||||
}
|
||||
13
examples/tanstack-start/src/routes/api.$.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import config from "../../bknd.config";
|
||||
import { serve } from "bknd/adapter/tanstack-start";
|
||||
|
||||
const handler = serve(config);
|
||||
|
||||
export const Route = createFileRoute("/api/$")({
|
||||
server: {
|
||||
handlers: {
|
||||
ANY: async ({ request }) => await handler(request),
|
||||
},
|
||||
},
|
||||
});
|
||||
171
examples/tanstack-start/src/routes/index.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import {
|
||||
createFileRoute,
|
||||
useRouter,
|
||||
} from "@tanstack/react-router";
|
||||
import { getApi } from "@/bknd";
|
||||
import { createServerFn, useServerFn } from "@tanstack/react-start";
|
||||
import { Footer } from "@/components/Footer";
|
||||
import { List } from "@/components/List";
|
||||
|
||||
export const completeTodo = createServerFn({ method: "POST" })
|
||||
.inputValidator(
|
||||
(data) => data as { done: boolean; id: number; title: string },
|
||||
)
|
||||
.handler(async ({ data: todo }) => {
|
||||
try {
|
||||
const api = await getApi({});
|
||||
await api.data.updateOne("todos", todo.id, {
|
||||
done: !todo.done,
|
||||
});
|
||||
console.log("state updated in db");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteTodo = createServerFn({ method: "POST" })
|
||||
.inputValidator((data) => data as { id: number })
|
||||
.handler(async ({ data }) => {
|
||||
try {
|
||||
const api = await getApi({});
|
||||
await api.data.deleteOne("todos", data.id);
|
||||
console.log("todo deleted from db");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
|
||||
export const createTodo = createServerFn({ method: "POST" })
|
||||
.inputValidator((data) => data as { title: string })
|
||||
.handler(async ({ data }) => {
|
||||
try {
|
||||
const api = await getApi({});
|
||||
await api.data.createOne("todos", { title: data.title });
|
||||
console.log("todo created in db");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
|
||||
export const getTodo = createServerFn({ method: "POST" }).handler(async () => {
|
||||
const api = await getApi({});
|
||||
const limit = 5;
|
||||
const todos = await api.data.readMany("todos", { limit, sort: "-id" });
|
||||
const total = todos.body.meta.total as number;
|
||||
return { total, todos, limit };
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
ssr:false,
|
||||
component: App,
|
||||
loader: async () => {
|
||||
return await getTodo();
|
||||
},
|
||||
});
|
||||
|
||||
function App() {
|
||||
const { todos, total, limit } = Route.useLoaderData();
|
||||
const router = useRouter();
|
||||
|
||||
const updateTodo = useServerFn(completeTodo);
|
||||
const removeTodo = useServerFn(deleteTodo);
|
||||
const addTodo = useServerFn(createTodo);
|
||||
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
||||
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
|
||||
<div className="flex flex-row items-center ">
|
||||
<img
|
||||
className="dark:invert size-18"
|
||||
src="/tanstack-circle-logo.png"
|
||||
alt="TanStack logo"
|
||||
/>
|
||||
<div className="ml-3.5 mr-2 font-mono opacity-70">&</div>
|
||||
<img
|
||||
className="dark:invert"
|
||||
src="/bknd.svg"
|
||||
alt="bknd logo"
|
||||
width={183}
|
||||
height={59}
|
||||
/>
|
||||
</div>
|
||||
<Description />
|
||||
<div className="flex flex-col border border-foreground/15 w-full py-4 px-5 gap-2">
|
||||
<h2 className="font-mono mb-1 opacity-70">
|
||||
<code>What's next?</code>
|
||||
</h2>
|
||||
<div className="flex flex-col w-full gap-2">
|
||||
{total > limit && (
|
||||
<div className="bg-foreground/10 flex justify-center p-1 text-xs rounded text-foreground/40">
|
||||
{total - limit} more todo(s) hidden
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-3">
|
||||
{todos.map((todo) => (
|
||||
<div className="flex flex-row" key={String(todo.id)}>
|
||||
<div className="flex flex-row flex-grow items-center gap-3 ml-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="flex-shrink-0 cursor-pointer"
|
||||
defaultChecked={!!todo.done}
|
||||
onChange={async () => {
|
||||
await updateTodo({ data: todo });
|
||||
router.invalidate();
|
||||
}}
|
||||
/>
|
||||
<div className="text-foreground/90 leading-none">
|
||||
{todo.title}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer grayscale transition-all hover:grayscale-0 text-xs "
|
||||
onClick={async () => {
|
||||
await removeTodo({ data: { id: todo.id } });
|
||||
router.invalidate();
|
||||
}}
|
||||
>
|
||||
❌
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<form
|
||||
className="flex flex-row w-full gap-3 mt-2"
|
||||
key={todos.map((t) => t.id).join()}
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const title = formData.get("title") as string;
|
||||
await addTodo({ data: { title } });
|
||||
router.invalidate();
|
||||
e.currentTarget.reset();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
placeholder="New todo"
|
||||
className="py-2 px-4 flex flex-grow rounded-sm bg-foreground/10 focus:bg-foreground/20 transition-colors outline-none"
|
||||
/>
|
||||
<button type="submit" className="cursor-pointer">
|
||||
Add
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Description = () => (
|
||||
<List
|
||||
items={[
|
||||
"Get started with a full backend.",
|
||||
"Focus on what matters instead of repetition.",
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
124
examples/tanstack-start/src/routes/ssr.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { getApi } from "@/bknd";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { getRequest } from "@tanstack/react-start/server";
|
||||
import { Footer } from "@/components/Footer";
|
||||
import { List } from "@/components/List";
|
||||
|
||||
export const getTodo = createServerFn({ method: "POST" }).handler(async () => {
|
||||
const api = await getApi({});
|
||||
const limit = 5;
|
||||
const todos = await api.data.readMany("todos");
|
||||
const total = todos.body.meta.total as number;
|
||||
return { total, todos, limit };
|
||||
});
|
||||
|
||||
export const getUser = createServerFn({ method: "POST" }).handler(async () => {
|
||||
const request = getRequest();
|
||||
const api = await getApi({ verify: true, headers: request.headers });
|
||||
const user = api.getUser();
|
||||
return { user };
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/ssr")({
|
||||
component: RouteComponent,
|
||||
loader: async () => {
|
||||
return { ...(await getTodo()), ...(await getUser()) };
|
||||
},
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { todos, user } = Route.useLoaderData();
|
||||
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
||||
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
|
||||
<div className="flex flex-row items-center ">
|
||||
<img
|
||||
className="dark:invert size-18"
|
||||
src="/tanstack-circle-logo.png"
|
||||
alt="TanStack logo"
|
||||
/>
|
||||
<div className="ml-3.5 mr-2 font-mono opacity-70">&</div>
|
||||
<img
|
||||
className="dark:invert"
|
||||
src="/bknd.svg"
|
||||
alt="bknd logo"
|
||||
width={183}
|
||||
height={59}
|
||||
/>
|
||||
</div>
|
||||
<List items={todos.map((todo) => todo.title)} />
|
||||
<Buttons />
|
||||
|
||||
<div>
|
||||
{user ? (
|
||||
<>
|
||||
Logged in as {user.email}.{" "}
|
||||
<Link
|
||||
className="font-medium underline"
|
||||
to={"/api/auth/logout" as string}
|
||||
>
|
||||
Logout
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
<p>
|
||||
Not logged in.{" "}
|
||||
<Link
|
||||
className="font-medium underline"
|
||||
to={"/admin/auth/login" as string}
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
</p>
|
||||
<p className="text-xs opacity-50">
|
||||
Sign in with:{" "}
|
||||
<b>
|
||||
<code>test@bknd.io</code>
|
||||
</b>{" "}
|
||||
/{" "}
|
||||
<b>
|
||||
<code>12345678</code>
|
||||
</b>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Buttons() {
|
||||
return (
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground gap-2 text-white hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
|
||||
href="https://bknd.io/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
className="grayscale"
|
||||
src="/bknd.ico"
|
||||
alt="bknd logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Go To Bknd.io
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
|
||||
href="https://docs.bknd.io/integration/tanstack-start"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
examples/tanstack-start/src/styles.css
Normal file
@@ -0,0 +1,23 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
28
examples/tanstack-start/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"jsx": "react-jsx",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"],
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": false,
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
34
examples/tanstack-start/vite.config.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { devtools } from "@tanstack/devtools-vite";
|
||||
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
||||
import viteReact from "@vitejs/plugin-react";
|
||||
import viteTsConfigPaths from "vite-tsconfig-paths";
|
||||
import { fileURLToPath, URL } from "url";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { nitro } from "nitro/vite";
|
||||
|
||||
const config = defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
nitro({ preset: "node-server" }),
|
||||
tailwindcss(),
|
||||
devtools(),
|
||||
// this is the plugin that enables path aliases
|
||||
viteTsConfigPaths({
|
||||
projects: ["./tsconfig.json"],
|
||||
}),
|
||||
|
||||
tanstackStart(),
|
||||
viteReact({
|
||||
babel: {
|
||||
plugins: ["babel-plugin-react-compiler"],
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export default config;
|
||||