diff --git a/app/package.json b/app/package.json index 970af13..c349f60 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.8.0-rc.5", + "version": "0.8.0-rc.6", "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": { diff --git a/app/src/App.ts b/app/src/App.ts index 3beec46..ab4b87a 100644 --- a/app/src/App.ts +++ b/app/src/App.ts @@ -26,6 +26,10 @@ export class AppFirstBoot extends AppEvent { } export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent, AppFirstBoot } as const; +export type AppOptions = { + plugins?: AppPlugin[]; + manager?: Omit; +}; export type CreateAppConfig = { connection?: | Connection @@ -36,8 +40,7 @@ export type CreateAppConfig = { } | LibSqlCredentials; initialConfig?: InitialModuleConfigs; - plugins?: AppPlugin[]; - options?: Omit; + options?: AppOptions; }; export type AppConfig = InitialModuleConfigs; @@ -47,32 +50,33 @@ export class App { static readonly Events = AppEvents; adminController?: AdminController; private trigger_first_boot = false; + private plugins: AppPlugin[]; constructor( private connection: Connection, _initialConfig?: InitialModuleConfigs, - private plugins: AppPlugin[] = [], - moduleManagerOptions?: ModuleManagerOptions + private options?: AppOptions ) { + this.plugins = options?.plugins ?? []; this.modules = new ModuleManager(connection, { - ...moduleManagerOptions, + ...(options?.manager ?? {}), initial: _initialConfig, onUpdated: async (key, config) => { // if the EventManager was disabled, we assume we shouldn't // respond to events, such as "onUpdated". // this is important if multiple changes are done, and then build() is called manually if (!this.emgr.enabled) { - console.warn("[APP] config updated, but event manager is disabled, skip."); + console.warn("App config updated, but event manager is disabled, skip."); return; } - console.log("[APP] config updated", key); + console.log("App config updated", key); // @todo: potentially double syncing await this.build({ sync: true }); await this.emgr.emit(new AppConfigUpdatedEvent({ app: this })); }, onFirstBoot: async () => { - console.log("[APP] first boot"); + console.log("App first boot"); this.trigger_first_boot = true; }, onServerInit: async (server) => { @@ -106,8 +110,6 @@ export class App { await this.emgr.emit(new AppBuiltEvent({ app: this })); - server.all("/api/*", async (c) => c.notFound()); - // first boot is set from ModuleManager when there wasn't a config table if (this.trigger_first_boot) { this.trigger_first_boot = false; @@ -183,7 +185,7 @@ export function createApp(config: CreateAppConfig = {}) { } else if (typeof config.connection === "object") { if ("type" in config.connection) { console.warn( - "[WARN] Using deprecated connection type 'libsql', use the 'config' object directly." + "Using deprecated connection type 'libsql', use the 'config' object directly." ); connection = new LibsqlConnection(config.connection.config); } else { @@ -191,7 +193,7 @@ export function createApp(config: CreateAppConfig = {}) { } } else { connection = new LibsqlConnection({ url: ":memory:" }); - console.warn("[!] No connection provided, using in-memory database"); + console.warn("No connection provided, using in-memory database"); } } catch (e) { console.error("Could not create connection", e); @@ -201,5 +203,5 @@ export function createApp(config: CreateAppConfig = {}) { throw new Error("Invalid connection"); } - return new App(connection, config.initialConfig, config.plugins, config.options); + return new App(connection, config.initialConfig, config.options); } diff --git a/app/src/adapter/bun/bun.adapter.ts b/app/src/adapter/bun/bun.adapter.ts index 05eec3c..851c54e 100644 --- a/app/src/adapter/bun/bun.adapter.ts +++ b/app/src/adapter/bun/bun.adapter.ts @@ -30,7 +30,6 @@ export function serve({ distPath, connection, initialConfig, - plugins, options, port = config.server.default_port, onBuilt, @@ -44,7 +43,6 @@ export function serve({ const app = await createApp({ connection, initialConfig, - plugins, options, onBuilt, buildConfig, diff --git a/app/src/cli/commands/debug.ts b/app/src/cli/commands/debug.ts index 124d7d2..77a668c 100644 --- a/app/src/cli/commands/debug.ts +++ b/app/src/cli/commands/debug.ts @@ -1,20 +1,45 @@ import path from "node:path"; import url from "node:url"; +import { createApp } from "App"; +import { getConnectionCredentialsFromEnv } from "cli/commands/run/platform"; import { getDistPath, getRelativeDistPath, getRootPath } from "cli/utils/sys"; +import { Argument } from "commander"; +import { showRoutes } from "hono/dev"; import type { CliCommand } from "../types"; export const debug: CliCommand = (program) => { program .command("debug") - .description("debug path resolution") - .action(() => { - console.log("paths", { - rootpath: getRootPath(), - distPath: getDistPath(), - relativeDistPath: getRelativeDistPath(), - cwd: process.cwd(), - dir: path.dirname(url.fileURLToPath(import.meta.url)), - resolvedPkg: path.resolve(getRootPath(), "package.json") - }); - }); + .description("debug bknd") + .addArgument(new Argument("", "subject to debug").choices(Object.keys(subjects))) + .action(action); }; + +const subjects = { + paths: async () => { + console.log("[PATHS]", { + rootpath: getRootPath(), + distPath: getDistPath(), + relativeDistPath: getRelativeDistPath(), + cwd: process.cwd(), + dir: path.dirname(url.fileURLToPath(import.meta.url)), + resolvedPkg: path.resolve(getRootPath(), "package.json") + }); + }, + routes: async () => { + console.log("[APP ROUTES]"); + const credentials = getConnectionCredentialsFromEnv(); + const app = createApp({ connection: credentials }); + await app.build(); + showRoutes(app.server); + } +}; + +async function action(subject: string) { + console.log("debug", { subject }); + if (subject in subjects) { + await subjects[subject](); + } else { + console.error("Invalid subject: ", subject); + } +} diff --git a/app/src/cli/commands/run/platform.ts b/app/src/cli/commands/run/platform.ts index 86758dc..eda86e4 100644 --- a/app/src/cli/commands/run/platform.ts +++ b/app/src/cli/commands/run/platform.ts @@ -32,7 +32,7 @@ export async function attachServeStatic(app: any, platform: Platform) { export async function startServer(server: Platform, app: any, options: { port: number }) { const port = options.port; - console.log(`(using ${server} serve)`); + console.log(`Using ${server} serve`); switch (server) { case "node": { @@ -54,7 +54,7 @@ export async function startServer(server: Platform, app: any, options: { port: n } const url = `http://localhost:${port}`; - console.log(`Server listening on ${url}`); + console.info("Server listening on", url); await open(url); } diff --git a/app/src/cli/commands/run/run.ts b/app/src/cli/commands/run/run.ts index a34a65c..2f078ad 100644 --- a/app/src/cli/commands/run/run.ts +++ b/app/src/cli/commands/run/run.ts @@ -2,10 +2,12 @@ import type { Config } from "@libsql/client/node"; import { App, type CreateAppConfig } from "App"; import { StorageLocalAdapter } from "adapter/node"; import type { CliBkndConfig, CliCommand } from "cli/types"; +import { replaceConsole } from "cli/utils/cli"; import { Option } from "commander"; import { config } from "core"; import dotenv from "dotenv"; import { registries } from "modules/registries"; +import c from "picocolors"; import { PLATFORMS, type Platform, @@ -27,6 +29,13 @@ export const run: CliCommand = (program) => { .default(config.server.default_port) .argParser((v) => Number.parseInt(v)) ) + .addOption( + new Option("-m, --memory", "use in-memory database").conflicts([ + "config", + "db-url", + "db-token" + ]) + ) .addOption(new Option("-c, --config ", "config file")) .addOption( new Option("--db-url ", "database url, can be any valid libsql url").conflicts( @@ -97,33 +106,44 @@ export async function makeConfigApp(config: CliBkndConfig, platform?: Platform) async function action(options: { port: number; + memory?: boolean; config?: string; dbUrl?: string; dbToken?: string; server: Platform; }) { + replaceConsole(); const configFilePath = await getConfigPath(options.config); let app: App | undefined = undefined; if (options.dbUrl) { + console.info("Using connection from", c.cyan("--db-url")); const connection = options.dbUrl ? { url: options.dbUrl, authToken: options.dbToken } : undefined; app = await makeApp({ connection, server: { platform: options.server } }); } else if (configFilePath) { - console.log("[INFO] Using config from:", configFilePath); + console.info("Using config from", c.cyan(configFilePath)); const config = (await import(configFilePath).then((m) => m.default)) as CliBkndConfig; app = await makeConfigApp(config, options.server); + } else if (options.memory) { + console.info("Using", c.cyan("in-memory"), "connection"); + app = await makeApp({ server: { platform: options.server } }); } else { const credentials = getConnectionCredentialsFromEnv(); if (credentials) { - console.log("[INFO] Using connection from environment"); + console.info("Using connection from env", c.cyan(credentials.url)); app = await makeConfigApp({ app: { connection: credentials } }, options.server); } } if (!app) { - app = await makeApp({ server: { platform: options.server } }); + const connection = { url: "file:data.db" } as Config; + console.info("Using connection", c.cyan(connection.url)); + app = await makeApp({ + connection, + server: { platform: options.server } + }); } await startServer(options.server, app, { port: options.port }); diff --git a/app/src/cli/utils/cli.ts b/app/src/cli/utils/cli.ts index 5b0d8e7..6e3a284 100644 --- a/app/src/cli/utils/cli.ts +++ b/app/src/cli/utils/cli.ts @@ -1,3 +1,6 @@ +import { isDebug } from "core"; +import c from "picocolors"; +import type { Formatter } from "picocolors/types"; const _SPEEDUP = process.env.LOCAL; const DEFAULT_WAIT = _SPEEDUP ? 0 : 250; @@ -54,3 +57,31 @@ export async function* typewriter( } } } + +function ifString(args: any[], c: Formatter) { + return args.map((a) => (typeof a === "string" ? c(a) : a)); +} + +const originalConsole = { + log: console.log, + info: console.info, + debug: console.debug, + warn: console.warn, + error: console.error +}; + +export const $console = { + log: (...args: any[]) => originalConsole.info(c.gray("[LOG] "), ...ifString(args, c.dim)), + info: (...args: any[]) => originalConsole.info(c.cyan("[INFO] "), ...args), + debug: (...args: any[]) => isDebug() && originalConsole.info(c.yellow("[DEBUG]"), ...args), + warn: (...args: any[]) => originalConsole.info(c.yellow("[WARN] "), ...ifString(args, c.yellow)), + error: (...args: any[]) => originalConsole.info(c.red("[ERROR]"), ...ifString(args, c.red)) +}; + +export function replaceConsole() { + console.log = $console.log; + console.info = $console.info; + console.debug = $console.debug; + console.warn = $console.warn; + console.error = $console.error; +} diff --git a/app/src/data/api/DataController.ts b/app/src/data/api/DataController.ts index c7eea9b..bd78f68 100644 --- a/app/src/data/api/DataController.ts +++ b/app/src/data/api/DataController.ts @@ -144,7 +144,7 @@ export class DataController extends Controller { //console.log("request", c.req.raw); const { entity, context } = c.req.param(); if (!this.entityExists(entity)) { - console.log("not found", entity, definedEntities); + console.warn("not found:", entity, definedEntities); return c.notFound(); } const _entity = this.em.entity(entity); @@ -228,7 +228,7 @@ export class DataController extends Controller { //console.log("request", c.req.raw); const { entity } = c.req.param(); if (!this.entityExists(entity)) { - console.log("not found", entity, definedEntities); + console.warn("not found:", entity, definedEntities); return c.notFound(); } const options = c.req.valid("query") as RepoQuery; diff --git a/app/src/flows/AppFlows.ts b/app/src/flows/AppFlows.ts index 4f75cbf..c31e051 100644 --- a/app/src/flows/AppFlows.ts +++ b/app/src/flows/AppFlows.ts @@ -63,6 +63,8 @@ export class AppFlows extends Module { }); }); + hono.all("*", (c) => c.notFound()); + this.ctx.server.route(this.config.basepath, hono); // register flows diff --git a/app/src/modules/server/AdminController.tsx b/app/src/modules/server/AdminController.tsx index 32c0c4e..c31bad0 100644 --- a/app/src/modules/server/AdminController.tsx +++ b/app/src/modules/server/AdminController.tsx @@ -7,6 +7,7 @@ import { html } from "hono/html"; import { Fragment } from "hono/jsx"; import { Controller } from "modules/Controller"; import * as SystemPermissions from "modules/permissions"; +import type { AppTheme } from "modules/server/AppServer"; const htmlBkndContextReplace = ""; @@ -246,7 +247,7 @@ export class AdminController extends Controller { } } -const style = (theme: "light" | "dark" = "light") => { +const style = (theme: AppTheme) => { const base = { margin: 0, padding: 0, @@ -271,6 +272,6 @@ const style = (theme: "light" | "dark" = "light") => { return { ...base, - ...styles[theme] + ...styles[theme === "light" ? "light" : "dark"] }; }; diff --git a/app/src/modules/server/AppServer.ts b/app/src/modules/server/AppServer.ts index c189262..9b4e6bf 100644 --- a/app/src/modules/server/AppServer.ts +++ b/app/src/modules/server/AppServer.ts @@ -4,12 +4,15 @@ import { cors } from "hono/cors"; import { Module } from "modules/Module"; const serverMethods = ["GET", "POST", "PATCH", "PUT", "DELETE"]; +const appThemes = ["dark", "light", "system"] as const; +export type AppTheme = (typeof appThemes)[number]; + export const serverConfigSchema = Type.Object( { admin: Type.Object( { basepath: Type.Optional(Type.String({ default: "", pattern: "^(/.+)?$" })), - color_scheme: Type.Optional(StringEnum(["dark", "light"], { default: "light" })), + color_scheme: Type.Optional(StringEnum(["dark", "light", "system"])), logo_return_path: Type.Optional( Type.String({ default: "/", diff --git a/app/src/ui/Admin.tsx b/app/src/ui/Admin.tsx index d966266..38d6cec 100644 --- a/app/src/ui/Admin.tsx +++ b/app/src/ui/Admin.tsx @@ -44,7 +44,7 @@ function AdminInternal() { const { theme } = useTheme(); return ( - + diff --git a/app/src/ui/client/ClientProvider.tsx b/app/src/ui/client/ClientProvider.tsx index c81e5cb..b24d1f9 100644 --- a/app/src/ui/client/ClientProvider.tsx +++ b/app/src/ui/client/ClientProvider.tsx @@ -1,5 +1,6 @@ import { Api, type ApiOptions, type TApiUser } from "Api"; import { isDebug } from "core"; +import type { AppTheme } from "modules/server/AppServer"; import { createContext, useContext } from "react"; const ClientContext = createContext<{ baseUrl: string; api: Api }>({ @@ -61,7 +62,7 @@ export const useBaseUrl = () => { type BkndWindowContext = { user?: TApiUser; logout_route: string; - color_scheme?: "light" | "dark"; + color_scheme?: AppTheme; }; export function useBkndWindowContext(): BkndWindowContext { if (typeof window !== "undefined" && window.__BKND__) { diff --git a/app/src/ui/client/use-theme.ts b/app/src/ui/client/use-theme.ts index 5fce405..935c1c1 100644 --- a/app/src/ui/client/use-theme.ts +++ b/app/src/ui/client/use-theme.ts @@ -1,18 +1,26 @@ +import type { AppTheme } from "modules/server/AppServer"; import { useBkndWindowContext } from "ui/client/ClientProvider"; import { useBknd } from "ui/client/bknd"; -export type Theme = "light" | "dark"; - -export function useTheme(fallback: Theme = "light"): { theme: Theme } { +export function useTheme(fallback: AppTheme = "system"): { theme: AppTheme } { const b = useBknd(); const winCtx = useBkndWindowContext(); - if (b) { - if (b?.adminOverride?.color_scheme) { - return { theme: b.adminOverride.color_scheme }; - } else if (!b.fallback) { - return { theme: b.config.server.admin.color_scheme ?? fallback }; - } + + // 1. override + // 2. config + // 3. winCtx + // 4. fallback + // 5. default + 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 theme = override ?? config ?? win ?? fallback; + + if (theme === "system") { + return { theme: prefersDark ? "dark" : "light" }; } - return { theme: winCtx.color_scheme ?? fallback }; + return { theme }; } diff --git a/app/src/ui/routes/index.tsx b/app/src/ui/routes/index.tsx index c1e5919..db1fb0f 100644 --- a/app/src/ui/routes/index.tsx +++ b/app/src/ui/routes/index.tsx @@ -10,12 +10,6 @@ import MediaRoutes from "./media"; import { Root, RootEmpty } from "./root"; import SettingsRoutes from "./settings"; -/*const DataRoutes = lazy(() => import("./data")); -const AuthRoutes = lazy(() => import("./auth")); -const MediaRoutes = lazy(() => import("./media")); -const FlowRoutes = lazy(() => import("./flows")); -const SettingsRoutes = lazy(() => import("./settings"));*/ - // @ts-ignore const TestRoutes = lazy(() => import("./test")); diff --git a/app/tailwind.config.js b/app/tailwind.config.js index fa735bd..cf7879c 100644 --- a/app/tailwind.config.js +++ b/app/tailwind.config.js @@ -1,3 +1,5 @@ +import tailwindCssAnimate from "tailwindcss-animate"; + /** @type {import("tailwindcss").Config} */ export default { content: ["./index.html", "./src/ui/**/*.tsx", "./src/ui/lib/mantine/theme.ts"], @@ -13,5 +15,5 @@ export default { } } }, - plugins: [require("tailwindcss-animate")] + plugins: [tailwindCssAnimate] }; diff --git a/app/vite.dev.ts b/app/vite.dev.ts index 9dfb3a1..cf95359 100644 --- a/app/vite.dev.ts +++ b/app/vite.dev.ts @@ -1,5 +1,6 @@ import { readFile } from "node:fs/promises"; import { serveStatic } from "@hono/node-server/serve-static"; +import { showRoutes } from "hono/dev"; import { App, registries } from "./src"; import { StorageLocalAdapter } from "./src/media/storage/adapters/StorageLocalAdapter"; @@ -28,6 +29,7 @@ if (example) { let app: App; const recreate = import.meta.env.VITE_APP_DISABLE_FRESH !== "1"; +let routesShown = false; export default { async fetch(request: Request) { if (!app || recreate) { @@ -44,6 +46,14 @@ export default { "sync" ); await app.build(); + + // log routes + if (!routesShown) { + routesShown = true; + console.log("\n\n[APP ROUTES]"); + showRoutes(app.server); + console.log("-------\n\n"); + } } return app.fetch(request); diff --git a/bun.lockb b/bun.lockb index 63c76bc..2fa9b69 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/usage/database.mdx b/docs/usage/database.mdx index a6464e1..04f3a62 100644 --- a/docs/usage/database.mdx +++ b/docs/usage/database.mdx @@ -17,10 +17,7 @@ The easiest to get started is using SQLite in-memory. When serving the API in th the function accepts an object with connection details. To use an in-memory database, you can either omit the object completely or explicitly use it as follows: ```json { - "type": "libsql", - "config": { - "url": ":memory:" - } + "url": ":memory:" } ``` @@ -29,10 +26,7 @@ Just like the in-memory option, using a file is just as easy: ```json { - "type": "libsql", - "config": { - "url": "file:" - } + "url": "file:" } ``` Please note that using SQLite as a file is only supported in server environments. @@ -47,10 +41,7 @@ turso dev The command will yield a URL. Use it in the connection object: ```json { - "type": "libsql", - "config": { - "url": "http://localhost:8080" - } + "url": "http://localhost:8080" } ``` @@ -59,11 +50,8 @@ If you want to use LibSQL on Turso, [sign up for a free account](https://turso.t connection object to your new database: ```json { - "type": "libsql", - "config": { - "url": "libsql://your-database-url.turso.io", - "authToken": "your-auth-token" - } + "url": "libsql://your-database-url.turso.io", + "authToken": "your-auth-token" } ``` diff --git a/docs/usage/introduction.mdx b/docs/usage/introduction.mdx index 7758ce2..985be99 100644 --- a/docs/usage/introduction.mdx +++ b/docs/usage/introduction.mdx @@ -44,18 +44,21 @@ import type { Connection } from "bknd/data"; import type { Config } from "@libsql/client"; type AppPlugin = (app: App) => Promise | void; +type ManagerOptions = { + basePath?: string; + trustFetched?: boolean; + onFirstBoot?: () => Promise; + seed?: (ctx: ModuleBuildContext) => Promise; +}; type CreateAppConfig = { connection?: | Connection | Config; initialConfig?: InitialModuleConfigs; - plugins?: AppPlugin[]; options?: { - basePath?: string; - trustFetched?: boolean; - onFirstBoot?: () => Promise; - seed?: (ctx: ModuleBuildContext) => Promise; + plugins?: AppPlugin[]; + manager?: ManagerOptions }; }; ```