From 675a39ad5c48d4d39743c076725918641ae33c13 Mon Sep 17 00:00:00 2001 From: dswbx Date: Mon, 3 Mar 2025 15:42:42 +0100 Subject: [PATCH 1/2] added aws lambda adapter + improvements to handle concurrency --- app/build.ts | 1 + app/package.json | 7 +- app/src/App.ts | 22 +++- app/src/adapter/aws/aws-lambda.adapter.ts | 68 +++++++++++ app/src/adapter/aws/index.ts | 1 + app/src/adapter/bun/bun.adapter.ts | 2 + app/src/adapter/index.ts | 11 +- app/src/cli/commands/copy-assets.ts | 36 ++++++ app/src/cli/commands/index.ts | 1 + app/src/modules/ModuleManager.ts | 11 +- app/src/modules/SystemApi.ts | 3 +- app/src/modules/server/AdminController.tsx | 34 +++--- app/src/modules/server/SystemController.ts | 20 +++- app/src/ui/client/BkndProvider.tsx | 16 ++- app/src/ui/routes/settings/index.tsx | 18 ++- examples/aws-lambda/clean.sh | 53 +++++++++ examples/aws-lambda/deploy.sh | 131 +++++++++++++++++++++ examples/aws-lambda/index.mjs | 14 +++ examples/aws-lambda/package.json | 21 ++++ examples/aws-lambda/test.js | 31 +++++ examples/aws-lambda/trust-policy.json | 12 ++ examples/bun/static.ts | 17 +++ 22 files changed, 488 insertions(+), 42 deletions(-) create mode 100644 app/src/adapter/aws/aws-lambda.adapter.ts create mode 100644 app/src/adapter/aws/index.ts create mode 100644 app/src/cli/commands/copy-assets.ts create mode 100755 examples/aws-lambda/clean.sh create mode 100755 examples/aws-lambda/deploy.sh create mode 100644 examples/aws-lambda/index.mjs create mode 100644 examples/aws-lambda/package.json create mode 100644 examples/aws-lambda/test.js create mode 100644 examples/aws-lambda/trust-policy.json create mode 100644 examples/bun/static.ts diff --git a/app/build.ts b/app/build.ts index 6ad0dc2..6f7872a 100644 --- a/app/build.ts +++ b/app/build.ts @@ -216,6 +216,7 @@ async function buildAdapters() { await tsup.build(baseConfig("remix")); await tsup.build(baseConfig("bun")); await tsup.build(baseConfig("astro")); + await tsup.build(baseConfig("aws")); await tsup.build( baseConfig("cloudflare", { external: [/^kysely/], diff --git a/app/package.json b/app/package.json index 0d71394..8b636fe 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.9.0-rc.1-7", + "version": "0.9.0-rc.1-11", "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": { @@ -191,6 +191,11 @@ "import": "./dist/adapter/astro/index.js", "require": "./dist/adapter/astro/index.cjs" }, + "./adapter/aws": { + "types": "./dist/types/adapter/aws/index.d.ts", + "import": "./dist/adapter/aws/index.js", + "require": "./dist/adapter/aws/index.cjs" + }, "./dist/main.css": "./dist/ui/main.css", "./dist/styles.css": "./dist/ui/styles.css", "./dist/manifest.json": "./dist/static/.vite/manifest.json" diff --git a/app/src/App.ts b/app/src/App.ts index e484685..ac0ea1d 100644 --- a/app/src/App.ts +++ b/app/src/App.ts @@ -58,6 +58,8 @@ export class App { adminController?: AdminController; private trigger_first_boot = false; private plugins: AppPlugin[]; + private _id: string = crypto.randomUUID(); + private _building: boolean = false; constructor( private connection: Connection, @@ -90,6 +92,11 @@ export class App { server.use(async (c, next) => { c.set("app", this); await next(); + + try { + // gracefully add the app id + c.res.headers.set("X-bknd-id", this._id); + } catch (e) {} }); }, }); @@ -100,9 +107,18 @@ export class App { return this.modules.ctx().emgr; } - async build(options?: { sync?: boolean }) { + async build(options?: { sync?: boolean; fetch?: boolean; forceBuild?: boolean }) { + // prevent multiple concurrent builds + if (this._building) { + while (this._building) { + await new Promise((resolve) => setTimeout(resolve, 10)); + } + if (!options?.forceBuild) return; + } + this._building = true; + if (options?.sync) this.modules.ctx().flags.sync_required = true; - await this.modules.build(); + await this.modules.build({ fetch: options?.fetch }); const { guard, server } = this.modules.ctx(); @@ -127,6 +143,8 @@ export class App { app: this, }); } + + this._building = false; } mutateConfig(module: Module) { diff --git a/app/src/adapter/aws/aws-lambda.adapter.ts b/app/src/adapter/aws/aws-lambda.adapter.ts new file mode 100644 index 0000000..9488065 --- /dev/null +++ b/app/src/adapter/aws/aws-lambda.adapter.ts @@ -0,0 +1,68 @@ +import type { App } from "bknd"; +import { handle } from "hono/aws-lambda"; +import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter"; + +export type AwsLambdaBkndConfig = RuntimeBkndConfig & { + assets?: + | { + mode: "local"; + root: string; + } + | { + mode: "url"; + url: string; + }; +}; + +let app: App; +export async function createApp({ + adminOptions = false, + assets, + ...config +}: AwsLambdaBkndConfig = {}) { + if (!app) { + let additional: Partial = { + adminOptions, + }; + + if (assets?.mode) { + switch (assets.mode) { + case "local": + // @todo: serve static outside app context + additional = { + adminOptions: adminOptions === false ? undefined : adminOptions, + serveStatic: (await import("@hono/node-server/serve-static")).serveStatic({ + root: assets.root, + onFound: (path, c) => { + c.res.headers.set("Cache-Control", "public, max-age=31536000"); + }, + }), + }; + break; + case "url": + additional.adminOptions = { + ...(typeof adminOptions === "object" ? adminOptions : {}), + assets_path: assets.url, + }; + break; + default: + throw new Error("Invalid assets mode"); + } + } + + app = await createRuntimeApp({ + ...config, + ...additional, + }); + } + + return app; +} + +export function serveLambda(config: AwsLambdaBkndConfig = {}) { + console.log("serving lambda"); + return async (event) => { + const app = await createApp(config); + return await handle(app.server)(event); + }; +} diff --git a/app/src/adapter/aws/index.ts b/app/src/adapter/aws/index.ts new file mode 100644 index 0000000..9c07f2b --- /dev/null +++ b/app/src/adapter/aws/index.ts @@ -0,0 +1 @@ +export * from "./aws-lambda.adapter"; diff --git a/app/src/adapter/bun/bun.adapter.ts b/app/src/adapter/bun/bun.adapter.ts index 65ab616..2087c5e 100644 --- a/app/src/adapter/bun/bun.adapter.ts +++ b/app/src/adapter/bun/bun.adapter.ts @@ -34,6 +34,7 @@ export function serve({ port = config.server.default_port, onBuilt, buildConfig, + adminOptions, ...serveOptions }: BunBkndConfig = {}) { Bun.serve({ @@ -46,6 +47,7 @@ export function serve({ options, onBuilt, buildConfig, + adminOptions, distPath, }); return app.fetch(request); diff --git a/app/src/adapter/index.ts b/app/src/adapter/index.ts index 99f9100..6d416e9 100644 --- a/app/src/adapter/index.ts +++ b/app/src/adapter/index.ts @@ -14,6 +14,8 @@ export type FrameworkBkndConfig = BkndConfig; export type RuntimeBkndConfig = BkndConfig & { distPath?: string; + serveStatic?: MiddlewareHandler | [string, MiddlewareHandler]; + adminOptions?: AdminControllerOptions | false; }; export function makeConfig(config: BkndConfig, args?: Args): CreateAppConfig { @@ -55,14 +57,7 @@ export async function createFrameworkApp( } export async function createRuntimeApp( - { - serveStatic, - adminOptions, - ...config - }: RuntimeBkndConfig & { - serveStatic?: MiddlewareHandler | [string, MiddlewareHandler]; - adminOptions?: AdminControllerOptions | false; - }, + { serveStatic, adminOptions, ...config }: RuntimeBkndConfig, env?: Env, ): Promise { const app = App.create(makeConfig(config, env)); diff --git a/app/src/cli/commands/copy-assets.ts b/app/src/cli/commands/copy-assets.ts new file mode 100644 index 0000000..813288c --- /dev/null +++ b/app/src/cli/commands/copy-assets.ts @@ -0,0 +1,36 @@ +import { getRelativeDistPath } from "cli/utils/sys"; +import type { CliCommand } from "../types"; +import { Option } from "commander"; +import fs from "node:fs/promises"; +import path from "node:path"; +import c from "picocolors"; + +export const copyAssets: CliCommand = (program) => { + program + .command("copy-assets") + .description("copy static assets") + .addOption(new Option("-o --out ", "directory to copy to")) + .addOption(new Option("-c --clean", "clean the output directory")) + .action(action); +}; + +async function action(options: { out?: string; clean?: boolean }) { + const out = options.out ?? "static"; + + // clean "out" directory + if (options.clean) { + await fs.rm(out, { recursive: true, force: true }); + } + + // recursively copy from src/assets to out using node fs + const from = path.resolve(getRelativeDistPath(), "static"); + await fs.cp(from, out, { recursive: true }); + + // in out, move ".vite/manifest.json" to "manifest.json" + await fs.rename(path.resolve(out, ".vite/manifest.json"), path.resolve(out, "manifest.json")); + + // delete ".vite" directory in out + await fs.rm(path.resolve(out, ".vite"), { recursive: true }); + + console.log(c.green(`Assets copied to: ${c.bold(out)}`)); +} diff --git a/app/src/cli/commands/index.ts b/app/src/cli/commands/index.ts index e27235f..cc1b2ae 100644 --- a/app/src/cli/commands/index.ts +++ b/app/src/cli/commands/index.ts @@ -4,3 +4,4 @@ export { run } from "./run"; export { debug } from "./debug"; export { user } from "./user"; export { create } from "./create"; +export { copyAssets } from "./copy-assets"; diff --git a/app/src/modules/ModuleManager.ts b/app/src/modules/ModuleManager.ts index 0170f3b..1cc51d6 100644 --- a/app/src/modules/ModuleManager.ts +++ b/app/src/modules/ModuleManager.ts @@ -468,13 +468,18 @@ export class ModuleManager { }); } - async build() { + async build(opts?: { fetch?: boolean }) { this.logger.context("build").log("version", this.version()); this.logger.log("booted with", this._booted_with); // if no config provided, try fetch from db - if (this.version() === 0) { - this.logger.context("no version").log("version is 0"); + if (this.version() === 0 || opts?.fetch === true) { + if (this.version() === 0) { + this.logger.context("no version").log("version is 0"); + } else { + this.logger.context("force fetch").log("force fetch"); + } + try { const result = await this.fetch(); diff --git a/app/src/modules/SystemApi.ts b/app/src/modules/SystemApi.ts index 219a979..dc2e5c6 100644 --- a/app/src/modules/SystemApi.ts +++ b/app/src/modules/SystemApi.ts @@ -20,10 +20,11 @@ export class SystemApi extends ModuleApi { return this.get<{ version: number } & ModuleConfigs>("config"); } - readSchema(options?: { config?: boolean; secrets?: boolean }) { + readSchema(options?: { config?: boolean; secrets?: boolean; fresh?: boolean }) { return this.get("schema", { config: options?.config ? 1 : 0, secrets: options?.secrets ? 1 : 0, + fresh: options?.fresh ? 1 : 0, }); } diff --git a/app/src/modules/server/AdminController.tsx b/app/src/modules/server/AdminController.tsx index d83119a..5957467 100644 --- a/app/src/modules/server/AdminController.tsx +++ b/app/src/modules/server/AdminController.tsx @@ -164,13 +164,23 @@ export class AdminController extends Controller { }; if (isProd) { - // @ts-ignore - const manifest = await import("bknd/dist/manifest.json", { - assert: { type: "json" }, - }); + let manifest: any; + if (this.options.assets_path.startsWith("http")) { + manifest = await fetch(this.options.assets_path + "manifest.json", { + headers: { + Accept: "application/json", + }, + }).then((res) => res.json()); + } else { + // @ts-ignore + manifest = await import("bknd/dist/manifest.json", { + assert: { type: "json" }, + }).then((res) => res.default); + } + // @todo: load all marked as entry (incl. css) - assets.js = manifest.default["src/ui/main.tsx"].file; - assets.css = manifest.default["src/ui/main.tsx"].css[0] as any; + assets.js = manifest["src/ui/main.tsx"].file; + assets.css = manifest["src/ui/main.tsx"].css[0] as any; } const theme = configs.server.admin.color_scheme ?? "light"; @@ -197,16 +207,8 @@ export class AdminController extends Controller { )} {isProd ? ( -