diff --git a/app/build.ts b/app/build.ts index c20526a..1ab90ea 100644 --- a/app/build.ts +++ b/app/build.ts @@ -14,15 +14,15 @@ 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) { @@ -43,146 +43,149 @@ if (clean) { 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")); + }, + }); } /** @@ -191,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 { - 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 { + 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 @@ -363,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 | null = null; - let isBuilding = false; + let debounceTimer: ReturnType | 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(() => {}); } diff --git a/app/e2e/inc/adapters.ts b/app/e2e/inc/adapters.ts index 347d23b..30b647b 100644 --- a/app/e2e/inc/adapters.ts +++ b/app/e2e/inc/adapters.ts @@ -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; } diff --git a/app/package.json b/app/package.json index 3027760..ceb3771 100644 --- a/app/package.json +++ b/app/package.json @@ -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"] } }, diff --git a/app/src/adapter/tanstack-start/index.ts b/app/src/adapter/tanstack-start/index.ts new file mode 100644 index 0000000..b6a4846 --- /dev/null +++ b/app/src/adapter/tanstack-start/index.ts @@ -0,0 +1 @@ +export * from "./tanstack-start.adapter"; diff --git a/app/src/adapter/tanstack-start/tanstack-start.adapter.spec.ts b/app/src/adapter/tanstack-start/tanstack-start.adapter.spec.ts new file mode 100644 index 0000000..1101f61 --- /dev/null +++ b/app/src/adapter/tanstack-start/tanstack-start.adapter.spec.ts @@ -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(bunTestRunner, { + makeApp: tanstackStart.getApp, + makeHandler: tanstackStart.serve, + }); +}); diff --git a/app/src/adapter/tanstack-start/tanstack-start.adapter.ts b/app/src/adapter/tanstack-start/tanstack-start.adapter.ts new file mode 100644 index 0000000..55151fe --- /dev/null +++ b/app/src/adapter/tanstack-start/tanstack-start.adapter.ts @@ -0,0 +1,33 @@ +import { createFrameworkApp, type FrameworkBkndConfig } from "bknd/adapter"; + +export type TanstackStartEnv = NodeJS.ProcessEnv; + +export type TanstackStartConfig = + FrameworkBkndConfig; + +/** + * Get bknd app instance + * @param config - bknd configuration + * @param args - environment variables + */ +export async function getApp( + config: TanstackStartConfig = {}, + 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( + config: TanstackStartConfig = {}, + args: Env = process.env as Env, +) { + return async (request: Request) => { + const app = await getApp(config, args); + return app.fetch(request); + }; +} diff --git a/bun.lock b/bun.lock index af2a52d..25eaa15 100644 --- a/bun.lock +++ b/bun.lock @@ -3846,7 +3846,7 @@ "@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "@bknd/plasmic/@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + "@bknd/plasmic/@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], "@bundled-es-modules/tough-cookie/tough-cookie": ["tough-cookie@4.1.4", "", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag=="], @@ -4750,7 +4750,7 @@ "@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg=="], - "@bknd/plasmic/@types/bun/bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + "@bknd/plasmic/@types/bun/bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], "@bundled-es-modules/tough-cookie/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="], diff --git a/docs/content/docs/(documentation)/integration/(frameworks)/meta.json b/docs/content/docs/(documentation)/integration/(frameworks)/meta.json index 6fb7bfc..5cc7900 100644 --- a/docs/content/docs/(documentation)/integration/(frameworks)/meta.json +++ b/docs/content/docs/(documentation)/integration/(frameworks)/meta.json @@ -1,3 +1,10 @@ { - "pages": ["nextjs", "react-router", "astro", "sveltekit", "vite"] + "pages": [ + "nextjs", + "react-router", + "astro", + "sveltekit", + "tanstack-start", + "vite" + ] } diff --git a/docs/content/docs/(documentation)/integration/(frameworks)/tanstack-start.mdx b/docs/content/docs/(documentation)/integration/(frameworks)/tanstack-start.mdx new file mode 100644 index 0000000..f34b599 --- /dev/null +++ b/docs/content/docs/(documentation)/integration/(frameworks)/tanstack-start.mdx @@ -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: + + + +```bash tab="npm" +npm install bknd +``` + +```bash tab="pnpm" +pnpm install bknd +``` + +```bash tab="yarn" +yarn add bknd +``` + +```bash tab="bun" +bun add bknd +``` + + + +## Configuration + + + 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. + + +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 = FrameworkBkndConfig; +``` + +## 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(); +}; +``` + + + 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. + + +## 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 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 + + + +## 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 ( +
+

Todos

+
    + {todos.map((todo) => ( +
  • {todo.title}
  • + ))} +
+
+ ); +} +``` + + + +### 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 ( +
+ {user ? ( + <> + Logged in as {user.email}.{" "} + + Logout + + + ) : ( +
+

+ Not logged in. + + Login + +

+

+ Sign in with: + + test@bknd.io + + / + + 12345678 + +

+
+ )} +
+ ) +} +``` + +Check the [Tanstack Start repository example](https://github.com/bknd-io/bknd/tree/main/examples/tanstack-start) for more implementation details. diff --git a/docs/content/docs/(documentation)/integration/introduction.mdx b/docs/content/docs/(documentation)/integration/introduction.mdx index 7119efd..6b94374 100644 --- a/docs/content/docs/(documentation)/integration/introduction.mdx +++ b/docs/content/docs/(documentation)/integration/introduction.mdx @@ -33,6 +33,12 @@ bknd seamlessly integrates with popular frameworks, allowing you to use what you href="/integration/sveltekit" /> +} + title="Tanstack Start" + href="/integration/tanstack-start" +/> + Create a new issue to request a guide for your framework. diff --git a/docs/content/docs/(documentation)/start.mdx b/docs/content/docs/(documentation)/start.mdx index 5a7a937..744588d 100644 --- a/docs/content/docs/(documentation)/start.mdx +++ b/docs/content/docs/(documentation)/start.mdx @@ -150,6 +150,12 @@ Pick your framework or runtime to get started. href="/integration/sveltekit" /> +} + title="Tanstack Start" + href="/integration/tanstack-start" +/> + } title="AWS Lambda" diff --git a/examples/tanstack-start/.gitignore b/examples/tanstack-start/.gitignore new file mode 100644 index 0000000..2396459 --- /dev/null +++ b/examples/tanstack-start/.gitignore @@ -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 \ No newline at end of file diff --git a/examples/tanstack-start/.vscode/settings.json b/examples/tanstack-start/.vscode/settings.json new file mode 100644 index 0000000..00b5278 --- /dev/null +++ b/examples/tanstack-start/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "files.watcherExclude": { + "**/routeTree.gen.ts": true + }, + "search.exclude": { + "**/routeTree.gen.ts": true + }, + "files.readonlyInclude": { + "**/routeTree.gen.ts": true + } +} diff --git a/examples/tanstack-start/README.md b/examples/tanstack-start/README.md new file mode 100644 index 0000000..af94249 --- /dev/null +++ b/examples/tanstack-start/README.md @@ -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` diff --git a/examples/tanstack-start/bknd.config.ts b/examples/tanstack-start/bknd.config.ts new file mode 100644 index 0000000..febdcff --- /dev/null +++ b/examples/tanstack-start/bknd.config.ts @@ -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; diff --git a/examples/tanstack-start/package.json b/examples/tanstack-start/package.json new file mode 100644 index 0000000..d26625f --- /dev/null +++ b/examples/tanstack-start/package.json @@ -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" + } +} diff --git a/examples/tanstack-start/public/bknd.ico b/examples/tanstack-start/public/bknd.ico new file mode 100644 index 0000000..c1a946d Binary files /dev/null and b/examples/tanstack-start/public/bknd.ico differ diff --git a/examples/tanstack-start/public/bknd.svg b/examples/tanstack-start/public/bknd.svg new file mode 100644 index 0000000..182ef92 --- /dev/null +++ b/examples/tanstack-start/public/bknd.svg @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/examples/tanstack-start/public/favicon.ico b/examples/tanstack-start/public/favicon.ico new file mode 100644 index 0000000..a11777c Binary files /dev/null and b/examples/tanstack-start/public/favicon.ico differ diff --git a/examples/tanstack-start/public/file.svg b/examples/tanstack-start/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/examples/tanstack-start/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/tanstack-start/public/globe.svg b/examples/tanstack-start/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/examples/tanstack-start/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/tanstack-start/public/manifest.json b/examples/tanstack-start/public/manifest.json new file mode 100644 index 0000000..078ef50 --- /dev/null +++ b/examples/tanstack-start/public/manifest.json @@ -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" +} diff --git a/examples/tanstack-start/public/robots.txt b/examples/tanstack-start/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/examples/tanstack-start/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/tanstack-start/public/tanstack-circle-logo.png b/examples/tanstack-start/public/tanstack-circle-logo.png new file mode 100644 index 0000000..9db3e67 Binary files /dev/null and b/examples/tanstack-start/public/tanstack-circle-logo.png differ diff --git a/examples/tanstack-start/public/tanstack-word-logo-white.svg b/examples/tanstack-start/public/tanstack-word-logo-white.svg new file mode 100644 index 0000000..b6ec508 --- /dev/null +++ b/examples/tanstack-start/public/tanstack-word-logo-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/tanstack-start/public/window.svg b/examples/tanstack-start/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/examples/tanstack-start/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/tanstack-start/src/bknd.ts b/examples/tanstack-start/src/bknd.ts new file mode 100644 index 0000000..3e0b9fb --- /dev/null +++ b/examples/tanstack-start/src/bknd.ts @@ -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(); +} diff --git a/examples/tanstack-start/src/components/Footer.tsx b/examples/tanstack-start/src/components/Footer.tsx new file mode 100644 index 0000000..e0d4da9 --- /dev/null +++ b/examples/tanstack-start/src/components/Footer.tsx @@ -0,0 +1,52 @@ +import { useRouterState, Link } from "@tanstack/react-router"; + +export function Footer() { + const routerState = useRouterState(); + const pathname = routerState.location.pathname; + + return ( +
+ + File icon + {pathname === "/" ? "SSR" : "Home"} + + + Window icon + Admin + + + Globe icon + Go to bknd.io → + +
+ ); +} diff --git a/examples/tanstack-start/src/components/List.tsx b/examples/tanstack-start/src/components/List.tsx new file mode 100644 index 0000000..5470ac0 --- /dev/null +++ b/examples/tanstack-start/src/components/List.tsx @@ -0,0 +1,9 @@ +export const List = ({ items = [] }: { items: React.ReactNode[] }) => ( +
    + {items.map((item, i) => ( +
  1. + {item} +
  2. + ))} +
+); diff --git a/examples/tanstack-start/src/logo.svg b/examples/tanstack-start/src/logo.svg new file mode 100644 index 0000000..fe53fe8 --- /dev/null +++ b/examples/tanstack-start/src/logo.svg @@ -0,0 +1,12 @@ + + + logo + + \ No newline at end of file diff --git a/examples/tanstack-start/src/routeTree.gen.ts b/examples/tanstack-start/src/routeTree.gen.ts new file mode 100644 index 0000000..13011d3 --- /dev/null +++ b/examples/tanstack-start/src/routeTree.gen.ts @@ -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() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/examples/tanstack-start/src/router.tsx b/examples/tanstack-start/src/router.tsx new file mode 100644 index 0000000..5c70836 --- /dev/null +++ b/examples/tanstack-start/src/router.tsx @@ -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 +} diff --git a/examples/tanstack-start/src/routes/__root.tsx b/examples/tanstack-start/src/routes/__root.tsx new file mode 100644 index 0000000..9593904 --- /dev/null +++ b/examples/tanstack-start/src/routes/__root.tsx @@ -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 ( + + + + + + + {children} + + , + }, + ]} + /> + + + + ); +} diff --git a/examples/tanstack-start/src/routes/admin.$.tsx b/examples/tanstack-start/src/routes/admin.$.tsx new file mode 100644 index 0000000..6e2971f --- /dev/null +++ b/examples/tanstack-start/src/routes/admin.$.tsx @@ -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 ( + + ); +} diff --git a/examples/tanstack-start/src/routes/api.$.ts b/examples/tanstack-start/src/routes/api.$.ts new file mode 100644 index 0000000..f65526c --- /dev/null +++ b/examples/tanstack-start/src/routes/api.$.ts @@ -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), + }, + }, +}); diff --git a/examples/tanstack-start/src/routes/index.tsx b/examples/tanstack-start/src/routes/index.tsx new file mode 100644 index 0000000..e2123d8 --- /dev/null +++ b/examples/tanstack-start/src/routes/index.tsx @@ -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 ( +
+
+
+ TanStack logo +
&
+ bknd logo +
+ +
+

+ What's next? +

+
+ {total > limit && ( +
+ {total - limit} more todo(s) hidden +
+ )} +
+ {todos.map((todo) => ( +
+
+ { + await updateTodo({ data: todo }); + router.invalidate(); + }} + /> +
+ {todo.title} +
+
+ +
+ ))} +
+
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(); + }} + > + + +
+
+
+
+
+
+ ); +} + +const Description = () => ( + +); + diff --git a/examples/tanstack-start/src/routes/ssr.tsx b/examples/tanstack-start/src/routes/ssr.tsx new file mode 100644 index 0000000..2782654 --- /dev/null +++ b/examples/tanstack-start/src/routes/ssr.tsx @@ -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 ( +
+
+
+ TanStack logo +
&
+ bknd logo +
+ todo.title)} /> + + +
+ {user ? ( + <> + Logged in as {user.email}.{" "} + + Logout + + + ) : ( +
+

+ Not logged in.{" "} + + Login + +

+

+ Sign in with:{" "} + + test@bknd.io + {" "} + /{" "} + + 12345678 + +

+
+ )} +
+
+
+
+ ); +} + +function Buttons() { + return ( + + ); +} diff --git a/examples/tanstack-start/src/styles.css b/examples/tanstack-start/src/styles.css new file mode 100644 index 0000000..1d1ea1e --- /dev/null +++ b/examples/tanstack-start/src/styles.css @@ -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; +} diff --git a/examples/tanstack-start/tsconfig.json b/examples/tanstack-start/tsconfig.json new file mode 100644 index 0000000..477479f --- /dev/null +++ b/examples/tanstack-start/tsconfig.json @@ -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/*"] + } + } +} diff --git a/examples/tanstack-start/vite.config.ts b/examples/tanstack-start/vite.config.ts new file mode 100644 index 0000000..a0b568e --- /dev/null +++ b/examples/tanstack-start/vite.config.ts @@ -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;