added fallback route to server, created extensive setup instructions in docs

This commit is contained in:
dswbx
2024-12-24 16:01:42 +01:00
parent 8ef11aa382
commit 06125f1afe
20 changed files with 564 additions and 174 deletions

View File

@@ -3,7 +3,7 @@
"type": "module",
"sideEffects": false,
"bin": "./dist/cli/index.js",
"version": "0.4.0-rc2",
"version": "0.4.0-rc3",
"scripts": {
"build:all": "NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
"dev": "vite",

View File

@@ -10,7 +10,7 @@ import * as SystemPermissions from "modules/permissions";
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
import { SystemController } from "modules/server/SystemController";
export type AppPlugin = (app: App) => void;
export type AppPlugin = (app: App) => Promise<void> | void;
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
export class AppConfigUpdatedEvent extends AppEvent {
@@ -93,7 +93,7 @@ export class App {
// load plugins
if (this.plugins.length > 0) {
this.plugins.forEach((plugin) => plugin(this));
await Promise.all(this.plugins.map((plugin) => plugin(this)));
}
//console.log("emitting built", options);

View File

@@ -7,7 +7,6 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx }: Cont
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
const key = config.key ?? "app";
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
const cachedConfig = await kv.get(key);
const initialConfig = cachedConfig ? JSON.parse(cachedConfig) : undefined;
@@ -15,28 +14,31 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx }: Cont
ctx.waitUntil(kv!.put(key, JSON.stringify(__config)));
}
const app = await createRuntimeApp({
...create_config,
initialConfig,
onBuilt: async (app) => {
app.module.server.client.get("/__bknd/cache", async (c) => {
await kv.delete(key);
return c.json({ message: "Cache cleared" });
});
await config.onBuilt?.(app);
const app = await createRuntimeApp(
{
...config,
initialConfig,
onBuilt: async (app) => {
app.module.server.client.get("/__bknd/cache", async (c) => {
await kv.delete(key);
return c.json({ message: "Cache cleared" });
});
await config.onBuilt?.(app);
},
beforeBuild: async (app) => {
app.emgr.onEvent(
App.Events.AppConfigUpdatedEvent,
async ({ params: { app } }) => {
saveConfig(app.toJSON(true));
},
"sync"
);
await config.beforeBuild?.(app);
},
adminOptions: { html: config.html }
},
beforeBuild: async (app) => {
app.emgr.onEvent(
App.Events.AppConfigUpdatedEvent,
async ({ params: { app } }) => {
saveConfig(app.toJSON(true));
},
"sync"
);
await config.beforeBuild?.(app);
},
adminOptions: { html: config.html }
});
env
);
if (!cachedConfig) {
saveConfig(app.toJSON(true));

View File

@@ -3,12 +3,13 @@ import type { App } from "bknd";
import type { CloudflareBkndConfig, Context } from "../index";
export async function makeApp(config: CloudflareBkndConfig, { env }: Context) {
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
return await createRuntimeApp({
...config,
...create_config,
adminOptions: config.html ? { html: config.html } : undefined
});
return await createRuntimeApp(
{
...config,
adminOptions: config.html ? { html: config.html } : undefined
},
env
);
}
export async function getFresh(config: CloudflareBkndConfig, ctx: Context) {

View File

@@ -4,15 +4,16 @@ import type { MiddlewareHandler } from "hono";
import { StorageLocalAdapter } from "media/storage/adapters/StorageLocalAdapter";
import type { AdminControllerOptions } from "modules/server/AdminController";
type BaseExternalBkndConfig = CreateAppConfig & {
export type BkndConfig<Env = any> = CreateAppConfig & {
app?: CreateAppConfig | ((env: Env) => CreateAppConfig);
onBuilt?: (app: App) => Promise<void>;
beforeBuild?: (app: App) => Promise<void>;
buildConfig?: Parameters<App["build"]>[0];
};
export type FrameworkBkndConfig = BaseExternalBkndConfig;
export type FrameworkBkndConfig<Env = any> = BkndConfig<Env>;
export type RuntimeBkndConfig = BaseExternalBkndConfig & {
export type RuntimeBkndConfig<Env = any> = BkndConfig<Env> & {
distPath?: string;
};
@@ -44,8 +45,27 @@ export function registerLocalMediaAdapter() {
registries.media.register("local", StorageLocalAdapter);
}
export async function createFrameworkApp(config: FrameworkBkndConfig): Promise<App> {
const app = App.create(config);
export function makeConfig<Env = any>(config: BkndConfig<Env>, env?: Env): CreateAppConfig {
let additionalConfig: CreateAppConfig = {};
if ("app" in config && config.app) {
if (typeof config.app === "function") {
if (!env) {
throw new Error("env is required when config.app is a function");
}
additionalConfig = config.app(env);
} else {
additionalConfig = config.app;
}
}
return { ...config, ...additionalConfig };
}
export async function createFrameworkApp<Env = any>(
config: FrameworkBkndConfig,
env?: Env
): Promise<App> {
const app = App.create(makeConfig(config, env));
if (config.onBuilt) {
app.emgr.onEvent(
@@ -63,21 +83,24 @@ export async function createFrameworkApp(config: FrameworkBkndConfig): Promise<A
return app;
}
export async function createRuntimeApp({
serveStatic,
registerLocalMedia,
adminOptions,
...config
}: RuntimeBkndConfig & {
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
registerLocalMedia?: boolean;
adminOptions?: AdminControllerOptions | false;
}): Promise<App> {
export async function createRuntimeApp<Env = any>(
{
serveStatic,
registerLocalMedia,
adminOptions,
...config
}: RuntimeBkndConfig & {
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
registerLocalMedia?: boolean;
adminOptions?: AdminControllerOptions | false;
},
env?: Env
): Promise<App> {
if (registerLocalMedia) {
registerLocalMediaAdapter();
}
const app = App.create(config);
const app = App.create(makeConfig(config, env));
app.emgr.onEvent(
App.Events.AppBuiltEvent,

View File

@@ -1,24 +1,40 @@
import { serveStatic } from "@hono/node-server/serve-static";
import { type RuntimeBkndConfig, createRuntimeApp } from "adapter";
import type { CreateAppConfig } from "bknd";
import type { App } from "bknd";
export type ViteBkndConfig<Env = any> = RuntimeBkndConfig & {
app: CreateAppConfig | ((env: Env) => CreateAppConfig);
export type ViteBkndConfig<Env = any> = RuntimeBkndConfig<Env> & {
setAdminHtml?: boolean;
forceDev?: boolean;
html?: string;
};
async function createApp(config: ViteBkndConfig, env: any) {
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
return await createRuntimeApp({
...create_config,
adminOptions: config.setAdminHtml
? { html: config.html, forceDev: config.forceDev }
: undefined,
serveStatic: ["/assets/*", serveStatic({ root: config.distPath ?? "./" })]
});
export function addViteScript(html: string, addBkndContext: boolean = true) {
return html.replace(
"</head>",
`<script type="module">
import RefreshRuntime from "/@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
<script type="module" src="/@vite/client"></script>
${addBkndContext ? "<!-- BKND_CONTEXT -->" : ""}
</head>`
);
}
async function createApp(config: ViteBkndConfig, env?: any) {
return await createRuntimeApp(
{
...config,
adminOptions: config.setAdminHtml
? { html: config.html, forceDev: config.forceDev }
: undefined,
serveStatic: ["/assets/*", serveStatic({ root: config.distPath ?? "./" })]
},
env
);
}
export async function serveFresh(config: ViteBkndConfig) {

View File

@@ -74,6 +74,21 @@ export class AppServer extends Module<typeof serverConfigSchema> {
})
);
// add an initial fallback route
this.client.use("/", async (c, next) => {
await next();
// if not finalized or giving a 404
if (!c.finalized || c.res.status === 404) {
// double check it's root
if (new URL(c.req.url).pathname === "/") {
c.res = undefined;
c.res = Response.json({
bknd: "hello world!"
});
}
}
});
this.client.onError((err, c) => {
//throw err;
console.error(err);
@@ -82,21 +97,6 @@ export class AppServer extends Module<typeof serverConfigSchema> {
return err;
}
/*if (isDebug()) {
console.log("accept", c.req.header("Accept"));
if (c.req.header("Accept") === "application/json") {
const stack = err.stack;
if ("toJSON" in err && typeof err.toJSON === "function") {
return c.json({ ...err.toJSON(), stack }, 500);
}
return c.json({ message: String(err), stack }, 500);
} else {
throw err;
}
}*/
if (err instanceof Exception) {
console.log("---is exception", err.code);
return c.json(err.toJSON(), err.code as any);
@@ -107,32 +107,6 @@ export class AppServer extends Module<typeof serverConfigSchema> {
this.setBuilt();
}
/*setAdminHtml(html: string) {
this.admin_html = html;
const basepath = (String(this.config.admin.basepath) + "/").replace(/\/+$/, "/");
const allowed_prefix = basepath + "auth";
const login_path = basepath + "auth/login";
this.client.get(basepath + "*", async (c, next) => {
const path = new URL(c.req.url).pathname;
if (!path.startsWith(allowed_prefix)) {
console.log("guard check permissions");
try {
this.ctx.guard.throwUnlessGranted(SystemPermissions.admin);
} catch (e) {
return c.redirect(login_path);
}
}
return c.html(this.admin_html!);
});
}
getAdminHtml() {
return this.admin_html;
}*/
override toJSON(secrets?: boolean) {
return this.config;
}