mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
Merge pull request #89 from bknd-io/feat/app-api-exp-for-nextjs
optimized local api instantiation to prepare for nextjs app router
This commit is contained in:
@@ -48,7 +48,7 @@ describe("App", () => {
|
||||
|
||||
const todos = await app.getApi().data.readMany("todos");
|
||||
expect(todos.length).toBe(2);
|
||||
expect(todos[0].title).toBe("ctx");
|
||||
expect(todos[1].title).toBe("api");
|
||||
expect(todos[0]?.title).toBe("ctx");
|
||||
expect(todos[1]?.title).toBe("api");
|
||||
});
|
||||
});
|
||||
|
||||
34
app/build.ts
34
app/build.ts
@@ -56,7 +56,7 @@ async function buildApi() {
|
||||
watch,
|
||||
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
|
||||
outDir: "dist",
|
||||
external: ["bun:test", "@libsql/client", "bknd/client"],
|
||||
external: ["bun:test", "@libsql/client"],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
format: ["esm"],
|
||||
@@ -71,16 +71,19 @@ async function buildApi() {
|
||||
});
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Building UI for direct imports
|
||||
*/
|
||||
async function buildUi() {
|
||||
await tsup.build({
|
||||
const base = {
|
||||
minify,
|
||||
sourcemap,
|
||||
watch,
|
||||
entry: ["src/ui/index.ts", "src/ui/client/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
||||
outDir: "dist/ui",
|
||||
external: [
|
||||
"bun:test",
|
||||
"react",
|
||||
@@ -104,7 +107,24 @@ async function buildUi() {
|
||||
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();
|
||||
},
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
...base,
|
||||
entry: ["src/ui/client/index.ts"],
|
||||
outDir: "dist/ui/client",
|
||||
onSuccess: async () => {
|
||||
await rewriteClient("./dist/ui/client/index.js");
|
||||
delayTypes();
|
||||
},
|
||||
});
|
||||
@@ -146,11 +166,7 @@ async function buildUiElements() {
|
||||
};
|
||||
},
|
||||
onSuccess: async () => {
|
||||
// manually replace ui/client with bknd/client
|
||||
const path = "./dist/ui/elements/index.js";
|
||||
const bundle = await Bun.file(path).text();
|
||||
await Bun.write(path, bundle.replaceAll("ui/client", "bknd/client"));
|
||||
|
||||
await rewriteClient("./dist/ui/elements/index.js");
|
||||
delayTypes();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"bin": "./dist/cli/index.js",
|
||||
"version": "0.9.0-rc.1",
|
||||
"version": "0.9.0-rc.1-7",
|
||||
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
||||
"homepage": "https://bknd.io",
|
||||
"repository": {
|
||||
|
||||
@@ -50,6 +50,7 @@ export type CreateAppConfig = {
|
||||
};
|
||||
|
||||
export type AppConfig = InitialModuleConfigs;
|
||||
export type LocalApiOptions = Request | ApiOptions;
|
||||
|
||||
export class App {
|
||||
modules: ModuleManager;
|
||||
@@ -186,13 +187,13 @@ export class App {
|
||||
return this.module.auth.createUser(p);
|
||||
}
|
||||
|
||||
getApi(options: Request | ApiOptions = {}) {
|
||||
getApi(options?: LocalApiOptions) {
|
||||
const fetcher = this.server.request as typeof fetch;
|
||||
if (options instanceof Request) {
|
||||
if (options && options instanceof Request) {
|
||||
return new Api({ request: options, headers: options.headers, fetcher });
|
||||
}
|
||||
|
||||
return new Api({ host: "http://localhost", ...options, fetcher });
|
||||
return new Api({ host: "http://localhost", ...(options ?? {}), fetcher });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,49 +1,35 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { nodeRequestToRequest } from "adapter/utils";
|
||||
import type { App } from "bknd";
|
||||
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
||||
import { Api } from "bknd/client";
|
||||
|
||||
export type NextjsBkndConfig = FrameworkBkndConfig & {
|
||||
cleanSearch?: string[];
|
||||
cleanRequest?: { searchParams?: string[] };
|
||||
};
|
||||
|
||||
type GetServerSidePropsContext = {
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
params?: Params;
|
||||
query: any;
|
||||
preview?: boolean;
|
||||
previewData?: any;
|
||||
draftMode?: boolean;
|
||||
resolvedUrl: string;
|
||||
locale?: string;
|
||||
locales?: string[];
|
||||
defaultLocale?: string;
|
||||
};
|
||||
let app: App;
|
||||
let building: boolean = false;
|
||||
|
||||
export function createApi({ req }: GetServerSidePropsContext) {
|
||||
const request = nodeRequestToRequest(req);
|
||||
return new Api({
|
||||
host: new URL(request.url).origin,
|
||||
headers: request.headers,
|
||||
});
|
||||
export async function getApp(config: NextjsBkndConfig) {
|
||||
if (building) {
|
||||
while (building) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
}
|
||||
if (app) return app;
|
||||
}
|
||||
|
||||
export function withApi<T>(handler: (ctx: GetServerSidePropsContext & { api: Api }) => T) {
|
||||
return async (ctx: GetServerSidePropsContext & { api: Api }) => {
|
||||
const api = createApi(ctx);
|
||||
await api.verifyAuth();
|
||||
return handler({ ...ctx, api });
|
||||
};
|
||||
building = true;
|
||||
if (!app) {
|
||||
app = await createFrameworkApp(config);
|
||||
await app.build();
|
||||
}
|
||||
building = false;
|
||||
return app;
|
||||
}
|
||||
|
||||
function getCleanRequest(
|
||||
req: Request,
|
||||
{ cleanSearch = ["route"] }: Pick<NextjsBkndConfig, "cleanSearch">,
|
||||
) {
|
||||
function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequest"]) {
|
||||
if (!cleanRequest) return req;
|
||||
|
||||
const url = new URL(req.url);
|
||||
cleanSearch?.forEach((k) => url.searchParams.delete(k));
|
||||
cleanRequest?.searchParams?.forEach((k) => url.searchParams.delete(k));
|
||||
|
||||
return new Request(url.toString(), {
|
||||
method: req.method,
|
||||
@@ -52,13 +38,12 @@ function getCleanRequest(
|
||||
});
|
||||
}
|
||||
|
||||
let app: App;
|
||||
export function serve({ cleanSearch, ...config }: NextjsBkndConfig = {}) {
|
||||
export function serve({ cleanRequest, ...config }: NextjsBkndConfig = {}) {
|
||||
return async (req: Request) => {
|
||||
if (!app) {
|
||||
app = await createFrameworkApp(config);
|
||||
app = await getApp(config);
|
||||
}
|
||||
const request = getCleanRequest(req, { cleanSearch });
|
||||
return app.fetch(request, process.env);
|
||||
const request = getCleanRequest(req, cleanRequest);
|
||||
return app.fetch(request);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@ type RemixContext = {
|
||||
let app: App;
|
||||
let building: boolean = false;
|
||||
|
||||
export async function getApp(config: RemixBkndConfig, args?: RemixContext) {
|
||||
export async function getApp<Args extends RemixContext = RemixContext>(
|
||||
config: RemixBkndConfig<Args>,
|
||||
args?: Args
|
||||
) {
|
||||
if (building) {
|
||||
while (building) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
@@ -31,7 +34,7 @@ export function serve<Args extends RemixContext = RemixContext>(
|
||||
config: RemixBkndConfig<Args> = {},
|
||||
) {
|
||||
return async (args: Args) => {
|
||||
app = await createFrameworkApp(config, args);
|
||||
app = await getApp(config, args);
|
||||
return app.fetch(args.request);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export async function replacePackageJsonVersions(
|
||||
|
||||
export async function updateBkndPackages(dir?: string, map?: Record<string, string>) {
|
||||
const versions = {
|
||||
bknd: "^" + (await sysGetVersion()),
|
||||
bknd: await sysGetVersion(),
|
||||
...(map ?? {}),
|
||||
};
|
||||
await replacePackageJsonVersions(
|
||||
|
||||
@@ -5,6 +5,7 @@ export {
|
||||
type AppConfig,
|
||||
type CreateAppConfig,
|
||||
type AppPlugin,
|
||||
type LocalApiOptions,
|
||||
} from "./App";
|
||||
|
||||
export {
|
||||
|
||||
@@ -264,7 +264,6 @@ export class FetchPromise<T = ApiResponse<any>> implements Promise<T> {
|
||||
} else {
|
||||
resBody = res.body;
|
||||
}
|
||||
console.groupEnd();
|
||||
|
||||
return createResponseProxy<T>(res, resBody, resData);
|
||||
}
|
||||
|
||||
@@ -286,7 +286,7 @@ export class ModuleManager {
|
||||
|
||||
return result as unknown as ConfigTable;
|
||||
},
|
||||
this.verbosity > Verbosity.silent ? [] : ["log", "error", "warn"],
|
||||
this.verbosity > Verbosity.silent ? [] : ["error"],
|
||||
);
|
||||
|
||||
this.logger
|
||||
|
||||
@@ -54,9 +54,9 @@ function AdminInternal() {
|
||||
);
|
||||
}
|
||||
|
||||
const Skeleton = ({ theme }: { theme?: string }) => {
|
||||
const actualTheme =
|
||||
(theme ?? document.querySelector("html")?.classList.contains("light")) ? "light" : "dark";
|
||||
const Skeleton = ({ theme }: { theme?: any }) => {
|
||||
const t = useTheme();
|
||||
const actualTheme = theme ?? t.theme;
|
||||
|
||||
return (
|
||||
<div id="bknd-admin" className={actualTheme + " antialiased"}>
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { AppTheme } from "modules/server/AppServer";
|
||||
import { useBkndWindowContext } from "ui/client/ClientProvider";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
|
||||
export function useTheme(fallback: AppTheme = "system"): { theme: AppTheme } {
|
||||
export function useTheme(fallback: AppTheme = "system") {
|
||||
const b = useBknd();
|
||||
const winCtx = useBkndWindowContext();
|
||||
|
||||
@@ -14,13 +14,16 @@ export function useTheme(fallback: AppTheme = "system"): { theme: AppTheme } {
|
||||
const override = b?.adminOverride?.color_scheme;
|
||||
const config = b?.config.server.admin.color_scheme;
|
||||
const win = winCtx.color_scheme;
|
||||
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
const prefersDark =
|
||||
typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
|
||||
const theme = override ?? config ?? win ?? fallback;
|
||||
|
||||
if (theme === "system") {
|
||||
return { theme: prefersDark ? "dark" : "light" };
|
||||
}
|
||||
|
||||
return { theme };
|
||||
return {
|
||||
theme: (theme === "system" ? (prefersDark ? "dark" : "light") : theme) as AppTheme,
|
||||
prefersDark,
|
||||
override,
|
||||
config,
|
||||
win,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
// there is no lifecycle or Hook in React that we can use to switch
|
||||
// .current at the right timing."
|
||||
// So we will have to make do with this "close enough" approach for now.
|
||||
import { useInsertionEffect, useRef } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export const useEvent = <Fn>(fn: Fn | ((...args: any[]) => any) | undefined): Fn => {
|
||||
const ref = useRef([fn, (...args) => ref[0](...args)]).current;
|
||||
// Per Dan Abramov: useInsertionEffect executes marginally closer to the
|
||||
// correct timing for ref synchronization than useLayoutEffect on React 18.
|
||||
// See: https://github.com/facebook/react/pull/25881#issuecomment-1356244360
|
||||
useInsertionEffect(() => {
|
||||
useEffect(() => {
|
||||
ref[0] = fn;
|
||||
});
|
||||
}, []);
|
||||
return ref[1];
|
||||
};
|
||||
|
||||
@@ -45,7 +45,6 @@ export function StepEntityFields() {
|
||||
const values = watch();
|
||||
|
||||
const updateListener = useEvent((data: TAppDataEntityFields) => {
|
||||
console.log("updateListener", data);
|
||||
setValue("fields", data as any);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Type } from "core/utils";
|
||||
import { type Entity, querySchema } from "data";
|
||||
import { Fragment } from "react";
|
||||
import { TbDots } from "react-icons/tb";
|
||||
import { useApi, useApiQuery } from "ui/client";
|
||||
import { useApiQuery } from "ui/client";
|
||||
import { useBknd } from "ui/client/bknd";
|
||||
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
@@ -83,7 +83,7 @@ export function DataEntityList({ params }) {
|
||||
search.set("perPage", perPage);
|
||||
}
|
||||
|
||||
const isUpdating = $q.isLoading && $q.isValidating;
|
||||
const isUpdating = $q.isLoading || $q.isValidating;
|
||||
|
||||
return (
|
||||
<Fragment key={entity.name}>
|
||||
|
||||
@@ -108,9 +108,7 @@ export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsForm
|
||||
|
||||
useEffect(() => {
|
||||
if (props?.onChange) {
|
||||
console.log("----set");
|
||||
watch((data: any) => {
|
||||
console.log("---calling");
|
||||
props?.onChange?.(toCleanValues(data));
|
||||
});
|
||||
}
|
||||
|
||||
4
examples/nextjs/.gitignore
vendored
4
examples/nextjs/.gitignore
vendored
@@ -28,8 +28,9 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for commiting if needed)
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
@@ -38,4 +39,3 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
!test.db
|
||||
|
||||
@@ -1,38 +1,36 @@
|
||||
# bknd starter: Next.js
|
||||
A minimal Next.js project with bknd integration.
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Project Structure
|
||||
## Getting Started
|
||||
|
||||
Inside of your Next.js project, you'll see the following folders and files:
|
||||
First, run the development server:
|
||||
|
||||
```text
|
||||
/
|
||||
├── public/
|
||||
├── src/
|
||||
│ └── pages/
|
||||
│ └── admin/
|
||||
│ │ └── [[...admin]].tsx
|
||||
│ └── api/
|
||||
│ │ └── [...route].ts
|
||||
│ ├── _app.tsx
|
||||
│ ├── _document.tsx
|
||||
│ └── index.tsx
|
||||
└── package.json
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
To update `bknd` config, check `src/pages/api/[...route].ts` and `src/pages/admin/[[...admin]].tsx`.
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
## Commands
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
| Command | Action |
|
||||
|:--------------------------|:-------------------------------------------------|
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:3000` |
|
||||
| `npm run build` | Build your production site |
|
||||
| `npm run db` | Starts a local LibSQL database |
|
||||
## Learn More
|
||||
|
||||
## Want to learn more?
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
Feel free to check [our documentation](https://docs.bknd.io/integration/nextjs) or jump into our [Discord server](https://discord.gg/952SFk8Tb8).
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
|
||||
@@ -2,17 +2,6 @@ import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
reactStrictMode: true
|
||||
|
||||
/*transpilePackages: [
|
||||
"@rjsf/core",
|
||||
"@libsql/isomorphic-fetch",
|
||||
"@libsql/isomorphic-ws",
|
||||
"@libsql/kysely-libsql"
|
||||
],
|
||||
experimental: {
|
||||
esmExternals: "loose"
|
||||
}*/
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -3,24 +3,24 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run db & next dev",
|
||||
"db": "turso dev --db-file test.db",
|
||||
"dev": "next dev",
|
||||
"dev:turbo": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "npm run db & next start",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"bknd": "file:../../app",
|
||||
"next": "15.0.2",
|
||||
"react": "file:../../node_modules/react",
|
||||
"react-dom": "file:../../node_modules/react-dom"
|
||||
"react-dom": "file:../../node_modules/react-dom",
|
||||
"next": "15.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1"
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
14
examples/nextjs/public/bknd.svg
Normal file
14
examples/nextjs/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 |
18
examples/nextjs/src/app/admin/[[...admin]]/page.tsx
Normal file
18
examples/nextjs/src/app/admin/[[...admin]]/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Admin } from "bknd/ui";
|
||||
import "bknd/dist/styles.css";
|
||||
import { getApi } from "@/bknd";
|
||||
|
||||
export default async function AdminPage() {
|
||||
const api = await getApi({ verify: true });
|
||||
|
||||
return (
|
||||
<Admin
|
||||
withProvider={{ user: api.getUser() }}
|
||||
config={{
|
||||
basepath: "/admin",
|
||||
logo_return_path: "/../",
|
||||
color_scheme: "system",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
16
examples/nextjs/src/app/api/[[...bknd]]/route.ts
Normal file
16
examples/nextjs/src/app/api/[[...bknd]]/route.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { getApp } from "@/bknd";
|
||||
|
||||
// if you're not using a local media adapter, or file database,
|
||||
// you can uncomment this line to enable running bknd on edge
|
||||
// export const runtime = "edge";
|
||||
|
||||
const handler = async (request: Request) => {
|
||||
const app = await getApp();
|
||||
return app.fetch(request);
|
||||
};
|
||||
|
||||
export const GET = handler;
|
||||
export const POST = handler;
|
||||
export const PUT = handler;
|
||||
export const PATCH = handler;
|
||||
export const DELETE = handler;
|
||||
3
examples/nextjs/src/app/env/route.ts
vendored
Normal file
3
examples/nextjs/src/app/env/route.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export const GET = async (req: Request) => {
|
||||
return Response.json(process.env);
|
||||
};
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
25
examples/nextjs/src/app/globals.css
Normal file
25
examples/nextjs/src/app/globals.css
Normal file
@@ -0,0 +1,25 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
@theme {
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
32
examples/nextjs/src/app/layout.tsx
Normal file
32
examples/nextjs/src/app/layout.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next & bknd App",
|
||||
description: "Presented by create next app & bknd",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
97
examples/nextjs/src/app/page.tsx
Normal file
97
examples/nextjs/src/app/page.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import Image from "next/image";
|
||||
import { getApi } from "@/bknd";
|
||||
|
||||
export default async function Home() {
|
||||
const api = await getApi();
|
||||
|
||||
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 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
|
||||
<div className="flex flex-row items-center ">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<div className="ml-3.5 mr-2 font-mono opacity-70">&</div>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/bknd.svg"
|
||||
alt="bknd logo"
|
||||
width={183}
|
||||
height={59}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
||||
<li className="mb-2">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
|
||||
src/app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li>Save and see your changes instantly.</li>
|
||||
</ol>
|
||||
|
||||
<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 text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</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/nextjs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<pre className="text-sm">
|
||||
{JSON.stringify(await api.data.readMany("posts"), null, 2)}
|
||||
</pre>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="/ssr"
|
||||
>
|
||||
<Image aria-hidden src="/file.svg" alt="File icon" width={16} height={16} />
|
||||
SSR
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="/admin"
|
||||
>
|
||||
<Image aria-hidden src="/window.svg" alt="Window icon" width={16} height={16} />
|
||||
Admin
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://bknd.io"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image aria-hidden src="/globe.svg" alt="Globe icon" width={16} height={16} />
|
||||
Go to bknd.io →
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
examples/nextjs/src/app/ssr/page.tsx
Normal file
14
examples/nextjs/src/app/ssr/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { getApi } from "@/bknd";
|
||||
|
||||
export default async function SSRPage() {
|
||||
const api = await getApi({ verify: true });
|
||||
const { data } = await api.data.readMany("posts");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Server-Side Rendered Page</h1>
|
||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
<pre>{JSON.stringify(api.getUser(), null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
examples/nextjs/src/bknd.ts
Normal file
35
examples/nextjs/src/bknd.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { type NextjsBkndConfig, getApp as getBkndApp } from "bknd/adapter/nextjs";
|
||||
import { registerLocalMediaAdapter } from "bknd/adapter/node";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
// The local media adapter works well in development, and server based
|
||||
// deployments. However, on vercel or any other serverless deployments,
|
||||
// you shouldn't use a filesystem based media adapter.
|
||||
//
|
||||
// Additionally, if you run the bknd api on the "edge" runtime,
|
||||
// this would not work as well.
|
||||
//
|
||||
// For production, it is recommended to uncomment the line below.
|
||||
registerLocalMediaAdapter();
|
||||
|
||||
export const config = {
|
||||
connection: {
|
||||
url: process.env.DB_URL as string,
|
||||
authToken: process.env.DB_TOKEN as string,
|
||||
},
|
||||
} as const satisfies NextjsBkndConfig;
|
||||
|
||||
export async function getApp() {
|
||||
return await getBkndApp(config);
|
||||
}
|
||||
|
||||
export async function getApi(opts?: { verify?: boolean }) {
|
||||
const app = await getApp();
|
||||
if (opts?.verify) {
|
||||
const api = app.getApi({ headers: await headers() });
|
||||
await api.verifyAuth();
|
||||
return api;
|
||||
}
|
||||
|
||||
return app.getApi();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import "@/styles/globals.css";
|
||||
import type { AppProps } from "next/app";
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
return <Component {...pageProps} />;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Html, Head, Main, NextScript } from "next/document";
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head />
|
||||
<body className="antialiased">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { InferGetServerSidePropsType as InferProps } from "next";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { withApi } from "bknd/adapter/nextjs";
|
||||
import "bknd/dist/styles.css";
|
||||
|
||||
const Admin = dynamic(() => import("bknd/ui").then((mod) => mod.Admin), {
|
||||
ssr: false
|
||||
});
|
||||
|
||||
export const getServerSideProps = withApi(async (context) => {
|
||||
return {
|
||||
props: {
|
||||
user: context.api.getUser()
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export default function AdminPage({ user }: InferProps<typeof getServerSideProps>) {
|
||||
if (typeof document === "undefined") return null;
|
||||
return (
|
||||
<Admin
|
||||
withProvider={{ user }}
|
||||
config={{ basepath: "/admin", logo_return_path: "/../", color_scheme: "system" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { App } from "bknd";
|
||||
import { serve } from "bknd/adapter/nextjs";
|
||||
import { boolean, em, entity, text } from "bknd/data";
|
||||
import { secureRandomString } from "bknd/utils";
|
||||
|
||||
export const config = {
|
||||
runtime: "edge",
|
||||
// add a matcher for bknd dist to allow dynamic otherwise build may fail.
|
||||
// inside this repo it's '../../app/dist/index.js', outside probably inside node_modules
|
||||
// see https://github.com/vercel/next.js/issues/51401
|
||||
// and https://github.com/vercel/next.js/pull/69402
|
||||
unstable_allowDynamic: ["**/*.js"]
|
||||
};
|
||||
|
||||
// the em() function makes it easy to create an initial schema
|
||||
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/core" {
|
||||
interface DB extends Database {}
|
||||
}
|
||||
|
||||
export default serve({
|
||||
// we can use any libsql config, and if omitted, uses in-memory
|
||||
connection: {
|
||||
url: "http://localhost:8080"
|
||||
},
|
||||
// an initial config is only applied if the database is empty
|
||||
initialConfig: {
|
||||
data: schema.toJSON(),
|
||||
// we're enabling auth ...
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: {
|
||||
secret: secureRandomString(64)
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {
|
||||
// the seed option is only executed if the database was empty
|
||||
seed: async (ctx) => {
|
||||
await ctx.em.mutator("todos").insertMany([
|
||||
{ title: "Learn bknd", done: true },
|
||||
{ title: "Build something cool", done: false }
|
||||
]);
|
||||
}
|
||||
},
|
||||
// here we can hook into the app lifecycle events ...
|
||||
beforeBuild: async (app) => {
|
||||
app.emgr.onEvent(
|
||||
App.Events.AppFirstBoot,
|
||||
async () => {
|
||||
// ... to create an initial user
|
||||
await app.module.auth.createUser({
|
||||
email: "ds@bknd.io",
|
||||
password: "12345678"
|
||||
});
|
||||
},
|
||||
"sync"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
type Data = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export default function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<Data>,
|
||||
) {
|
||||
res.status(200).json({ name: "John Doe" });
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,24 +0,0 @@
|
||||
import { withApi } from "bknd/adapter/nextjs";
|
||||
import type { InferGetServerSidePropsType } from "next";
|
||||
|
||||
export const getServerSideProps = withApi(async (context) => {
|
||||
const { data = [] } = await context.api.data.readMany("todos");
|
||||
const user = context.api.getUser();
|
||||
|
||||
return { props: { data, user } };
|
||||
});
|
||||
|
||||
export default function Home({
|
||||
data,
|
||||
user
|
||||
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Data</h1>
|
||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
|
||||
<h1>User</h1>
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
export default {
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
@@ -15,5 +15,4 @@ const config: Config = {
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
export default config;
|
||||
} satisfies Config;
|
||||
|
||||
Binary file not shown.
@@ -13,10 +13,15 @@
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { App } from "bknd";
|
||||
import { App, type LocalApiOptions } from "bknd";
|
||||
import { registerLocalMediaAdapter } from "bknd/adapter/node";
|
||||
import { type RemixBkndConfig, getApp as getBkndApp } from "bknd/adapter/remix";
|
||||
import { boolean, em, entity, text } from "bknd/data";
|
||||
@@ -76,17 +76,7 @@ export async function getApp(args?: { request: Request }) {
|
||||
return await getBkndApp(config, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* If args are given, it will use authentication details from the request
|
||||
* @param args
|
||||
*/
|
||||
export async function getApi(args?: { request: Request }) {
|
||||
const app = await getApp(args);
|
||||
if (args) {
|
||||
const api = app.getApi(args.request);
|
||||
await api.verifyAuth();
|
||||
return api;
|
||||
}
|
||||
|
||||
return app.getApi();
|
||||
export async function getApi(options?: LocalApiOptions) {
|
||||
const app = await getApp();
|
||||
return await app.getApi(options);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user