diff --git a/app/__test__/app/repro.spec.ts b/app/__test__/app/repro.spec.ts index be2da387..e1619f5a 100644 --- a/app/__test__/app/repro.spec.ts +++ b/app/__test__/app/repro.spec.ts @@ -1,5 +1,4 @@ import { describe, expect, test } from "bun:test"; -import { registries } from "../../src"; import { createApp } from "core/test/utils"; import * as proto from "../../src/data/prototype"; import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter"; @@ -14,8 +13,8 @@ describe("repros", async () => { * There was an issue that AppData had old configs because of system entity "media" */ test("registers media entity correctly to relate to it", async () => { - registries.media.register("local", StorageLocalAdapter); const app = createApp(); + app.module.media.adapters.set("local", StorageLocalAdapter); await app.build(); { diff --git a/app/__test__/media/MediaController.spec.ts b/app/__test__/media/MediaController.spec.ts index 9ec23557..b2d73d8a 100644 --- a/app/__test__/media/MediaController.spec.ts +++ b/app/__test__/media/MediaController.spec.ts @@ -1,17 +1,12 @@ /// import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { registries } from "../../src"; import { createApp } from "core/test/utils"; import { mergeObject, randomString } from "../../src/core/utils"; import type { TAppMediaConfig } from "../../src/media/media-schema"; import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter"; import { assetsPath, assetsTmpPath, disableConsoleLog, enableConsoleLog } from "../helper"; -beforeAll(() => { - registries.media.register("local", StorageLocalAdapter); -}); - const path = `${assetsPath}/image.png`; async function makeApp(mediaOverride: Partial = {}) { @@ -32,6 +27,8 @@ async function makeApp(mediaOverride: Partial = {}) { }, }); + app.module.media.adapters.set("local", StorageLocalAdapter); + await app.build(); return app; } diff --git a/app/src/App.ts b/app/src/App.ts index f1a495b5..54a6e95d 100644 --- a/app/src/App.ts +++ b/app/src/App.ts @@ -30,6 +30,7 @@ export type AppPluginConfig = { onServerInit?: (server: Hono) => MaybePromise; onFirstBoot?: () => MaybePromise; onBoot?: () => MaybePromise; + onModulesCreated?: (modules: Modules) => void; }; export type AppPlugin = (app: App) => AppPluginConfig; @@ -113,6 +114,7 @@ export class App( opts?: RuntimeOptions, ) { const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static"); - registerLocalMediaAdapter(); - return await createRuntimeApp( + const app = await createRuntimeApp( { ...config, serveStatic: serveStatic({ root }), @@ -27,6 +26,8 @@ export async function createApp( args ?? (process.env as Env), opts, ); + registerLocalMediaAdapter(app); + return app; } export function createHandler( diff --git a/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts b/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts index 427f8e4d..5638b76a 100644 --- a/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts +++ b/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts @@ -8,6 +8,7 @@ import { getCached } from "./modes/cached"; import { getDurable } from "./modes/durable"; import type { App } from "bknd"; import { $console } from "core/utils"; +import { registerMedia } from "./storage/StorageR2Adapter"; declare global { namespace Cloudflare { @@ -33,7 +34,7 @@ export type CloudflareBkndConfig = RuntimeBkndConfig & keepAliveSeconds?: number; forceHttps?: boolean; manifest?: string; - registerMedia?: boolean | ((env: Env) => void); + registerMedia?: boolean; }; export type Context = { @@ -99,7 +100,16 @@ export function serve( throw new Error(`Unknown mode ${mode}`); } + registerMediaInternal(app, config, context); return app.fetch(request, env, ctx); }, }; } + +let media_registered: boolean = false; +function registerMediaInternal(app: App, config: CloudflareBkndConfig, ctx?: Context) { + if (!media_registered && config.registerMedia !== false) { + registerMedia(app, ctx?.env as any); + media_registered = true; + } +} diff --git a/app/src/adapter/cloudflare/config.ts b/app/src/adapter/cloudflare/config.ts index 8dbfff6c..e2958a51 100644 --- a/app/src/adapter/cloudflare/config.ts +++ b/app/src/adapter/cloudflare/config.ts @@ -1,6 +1,5 @@ /// -import { registerMedia } from "./storage/StorageR2Adapter"; import { getBinding } from "./bindings"; import { d1Sqlite } from "./connection/D1Connection"; import { Connection } from "bknd/data"; @@ -88,20 +87,10 @@ export function d1SessionHelper(config: CloudflareBkndConfig) { }; } -let media_registered: boolean = false; export function makeConfig( config: CloudflareBkndConfig, args?: CfMakeConfigArgs, ) { - if (!media_registered && config.registerMedia !== false) { - if (typeof config.registerMedia === "function") { - config.registerMedia(args?.env as any); - } else { - registerMedia(args?.env as any); - } - media_registered = true; - } - const appConfig = makeAdapterConfig(config, args?.env); // if connection instance is given, don't do anything diff --git a/app/src/adapter/cloudflare/index.ts b/app/src/adapter/cloudflare/index.ts index b8b3c4e8..d58045b2 100644 --- a/app/src/adapter/cloudflare/index.ts +++ b/app/src/adapter/cloudflare/index.ts @@ -14,7 +14,6 @@ export { } from "./bindings"; export { constants } from "./config"; export { StorageR2Adapter } from "./storage/StorageR2Adapter"; -export { registries } from "bknd"; // for compatibility with old code export function d1( diff --git a/app/src/adapter/cloudflare/storage/StorageR2Adapter.ts b/app/src/adapter/cloudflare/storage/StorageR2Adapter.ts index 9fc41d41..36308804 100644 --- a/app/src/adapter/cloudflare/storage/StorageR2Adapter.ts +++ b/app/src/adapter/cloudflare/storage/StorageR2Adapter.ts @@ -1,4 +1,4 @@ -import { registries } from "bknd"; +import type { App } from "bknd"; import { isDebug } from "bknd/core"; import { guessMimeType as guess, StorageAdapter, type FileBody } from "bknd/media"; import { getBindings } from "../bindings"; @@ -13,10 +13,10 @@ export function makeSchema(bindings: string[] = []) { ); } -export function registerMedia(env: Record) { +export function registerMedia(app: App, env: Record) { const r2_bindings = getBindings(env, "R2Bucket"); - registries.media.register( + app.module.media.adapters.set( "r2", class extends StorageR2Adapter { constructor(private config: any) { diff --git a/app/src/adapter/index.ts b/app/src/adapter/index.ts index 9e74dd01..780868ef 100644 --- a/app/src/adapter/index.ts +++ b/app/src/adapter/index.ts @@ -4,13 +4,14 @@ import { $console } from "bknd/utils"; import type { MiddlewareHandler } from "hono"; import type { AdminControllerOptions } from "modules/server/AdminController"; import { Connection } from "bknd/data"; +import type { MaybePromise } from "core/types"; export { Connection } from "bknd/data"; export type BkndConfig = CreateAppConfig & { app?: CreateAppConfig | ((args: Args) => CreateAppConfig); - onBuilt?: (app: App) => Promise; - beforeBuild?: (app: App) => Promise; + onBuilt?: (app: App) => MaybePromise; + beforeBuild?: (app: App) => MaybePromise; buildConfig?: Parameters[0]; }; diff --git a/app/src/adapter/node/node.adapter.ts b/app/src/adapter/node/node.adapter.ts index 88b7d62e..275ddc8f 100644 --- a/app/src/adapter/node/node.adapter.ts +++ b/app/src/adapter/node/node.adapter.ts @@ -29,8 +29,7 @@ export async function createApp( console.warn("relativeDistPath is deprecated, please use distPath instead"); } - registerLocalMediaAdapter(); - return await createRuntimeApp( + const app = await createRuntimeApp( { ...config, serveStatic: serveStatic({ root }), @@ -39,6 +38,8 @@ export async function createApp( args ?? { env: process.env }, opts, ); + registerLocalMediaAdapter(app); + return app; } export function createHandler( diff --git a/app/src/adapter/node/storage/index.ts b/app/src/adapter/node/storage/index.ts index 549bf702..ca86c18c 100644 --- a/app/src/adapter/node/storage/index.ts +++ b/app/src/adapter/node/storage/index.ts @@ -1,14 +1,10 @@ -import { registries } from "bknd"; +import type { App } from "bknd"; import { type LocalAdapterConfig, StorageLocalAdapter } from "./StorageLocalAdapter"; export * from "./StorageLocalAdapter"; -let registered = false; -export function registerLocalMediaAdapter() { - if (!registered) { - registries.media.register("local", StorageLocalAdapter); - registered = true; - } +export function registerLocalMediaAdapter(app: App) { + app.module.media.adapters.set("local", StorageLocalAdapter); return (config: Partial = {}) => { const adapter = new StorageLocalAdapter(config); diff --git a/app/src/adapter/vite/vite.adapter.ts b/app/src/adapter/vite/vite.adapter.ts index c69bc1e0..63c6026e 100644 --- a/app/src/adapter/vite/vite.adapter.ts +++ b/app/src/adapter/vite/vite.adapter.ts @@ -32,8 +32,7 @@ async function createApp( env: ViteEnv = {} as ViteEnv, opts: FrameworkOptions = {}, ): Promise { - registerLocalMediaAdapter(); - return await createRuntimeApp( + const app = await createRuntimeApp( { ...config, adminOptions: config.adminOptions ?? { @@ -49,6 +48,8 @@ async function createApp( env, opts, ); + registerLocalMediaAdapter(app); + return app; } export function serve( diff --git a/app/src/cli/commands/run/run.ts b/app/src/cli/commands/run/run.ts index a379cf72..da77e7b8 100644 --- a/app/src/cli/commands/run/run.ts +++ b/app/src/cli/commands/run/run.ts @@ -1,6 +1,6 @@ import type { Config } from "@libsql/client/node"; import type { App, CreateAppConfig } from "App"; -import { StorageLocalAdapter } from "adapter/node/storage"; +import { registerLocalMediaAdapter } from "adapter/node/storage"; import type { CliBkndConfig, CliCommand } from "cli/types"; import { Option } from "commander"; import { config } from "core"; @@ -56,13 +56,6 @@ export const run: CliCommand = (program) => { .action(action); }; -// automatically register local adapter -// @todo: add back -/* const local = StorageLocalAdapter.prototype.getName(); -if (!registries.media.has(local)) { - registries.media.register(local, StorageLocalAdapter); -} */ - type MakeAppConfig = { connection?: CreateAppConfig["connection"]; server?: { platform?: Platform }; @@ -71,10 +64,12 @@ type MakeAppConfig = { }; async function makeApp(config: MakeAppConfig) { - return await createRuntimeApp({ + const app = await createRuntimeApp({ serveStatic: await serveStatic(config.server?.platform ?? "node"), ...config, }); + registerLocalMediaAdapter(app); + return app; } export async function makeConfigApp(_config: CliBkndConfig, platform?: Platform) { diff --git a/app/src/media/AppMedia.ts b/app/src/media/AppMedia.ts index 39734d75..58314c57 100644 --- a/app/src/media/AppMedia.ts +++ b/app/src/media/AppMedia.ts @@ -2,7 +2,7 @@ import type { AppEntity, Constructor } from "core"; import { $console, objectTransform } from "core/utils"; import type { Entity, EntityManager } from "data"; import { type FileUploadedEventData, Storage, type StorageAdapter, MediaPermissions } from "media"; -import { Module } from "modules/Module"; +import { Module, type ModuleBuildContext } from "modules/Module"; import { type FieldSchema, em, entity } from "../data/prototype"; import { MediaController } from "./api/MediaController"; import { mediaConfigSchema, type TAppMediaConfig } from "./media-schema"; @@ -25,21 +25,18 @@ export class AppMedia extends Module> { private _storage?: Storage; adapters: Map> = new Map(); + constructor(initial?: Partial, _ctx?: ModuleBuildContext) { + super(initial, _ctx); + this.adapters.set("s3", StorageS3Adapter); + this.adapters.set("cloudinary", StorageCloudinaryAdapter); + } + override async build() { if (!this.config.enabled) { this.setBuilt(); return; } - // register default adapters - if (!this.adapters.has("s3")) { - this.adapters.set("s3", StorageS3Adapter); - } - - if (!this.adapters.has("cloudinary")) { - this.adapters.set("cloudinary", StorageCloudinaryAdapter); - } - if (!this.config.adapter) { console.info("No storage adapter provided, skip building media."); return; diff --git a/app/src/modules/Module.ts b/app/src/modules/Module.ts index 97a22124..d08a7ba9 100644 --- a/app/src/modules/Module.ts +++ b/app/src/modules/Module.ts @@ -38,8 +38,11 @@ export abstract class Module { overwritePaths: this.getOverwritePaths(), onBeforeUpdate: this.onBeforeUpdate.bind(this), }); + this.init(); } + init() {} + static ctx_flags = { sync_required: false, ctx_reload_required: false, diff --git a/app/src/modules/ModuleManager.ts b/app/src/modules/ModuleManager.ts index cee1b47a..0c8f325e 100644 --- a/app/src/modules/ModuleManager.ts +++ b/app/src/modules/ModuleManager.ts @@ -80,6 +80,8 @@ export type ModuleManagerOptions = { seed?: (ctx: ModuleBuildContext) => Promise; // called right after modules are built, before finish onModulesBuilt?: (ctx: ModuleBuildContext) => Promise; + // called right after modules are created, before build + onModulesCreated?: (modules: Modules, ctx: ModuleBuildContext) => void; /** @deprecated */ verbosity?: Verbosity; }; @@ -127,8 +129,16 @@ export class ModuleManagerConfigUpdateEvent< }> { static override slug = "mm-config-update"; } +export class ModuleManagerModulesCreatedEvent extends ModuleManagerEvent<{ + modules: Modules; + ctx: ModuleBuildContext; +}> { + static override slug = "mm-modules-created"; +} + export const ModuleManagerEvents = { ModuleManagerConfigUpdateEvent, + ModuleManagerModulesCreatedEvent, }; // @todo: cleanup old diffs on upgrade @@ -195,6 +205,11 @@ export class ModuleManager { this.modules[key] = module; } + + if (this.options?.onModulesCreated) { + this.options.onModulesCreated(this.modules, context); + } + this.logger.log("modules created"); } catch (e) { this.logger.log("failed to create modules", e); diff --git a/app/src/ui/components/form/json-schema-form/Field.tsx b/app/src/ui/components/form/json-schema-form/Field.tsx index e516b8f4..cb8bea48 100644 --- a/app/src/ui/components/form/json-schema-form/Field.tsx +++ b/app/src/ui/components/form/json-schema-form/Field.tsx @@ -64,12 +64,13 @@ const FieldImpl = ({ const id = `${name}-${useId()}`; const required = typeof _required === "boolean" ? _required : ctx.required; - if (!isTypeSchema(schema)) + if (!schema) { return (
             [Field] {path} has no schema ({JSON.stringify(schema)})
          
); + } if (isType(schema.type, "object")) { return ; diff --git a/app/tsconfig.json b/app/tsconfig.json index a40d88a7..8b82a267 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -49,5 +49,5 @@ "__test__", "e2e/**/*.ts" ], - "exclude": ["node_modules", "dist", "dist/types", "**/*.d.ts"] + "exclude": ["node_modules", "dist", "dist/types", "**/*.d.ts", "**/dist/**"] } diff --git a/app/vite.dev.ts b/app/vite.dev.ts index 1274d21f..bc4b9c50 100644 --- a/app/vite.dev.ts +++ b/app/vite.dev.ts @@ -1,8 +1,8 @@ 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/adapter/node"; +import { App } from "./src/App"; +import { StorageLocalAdapter } from "./src/adapter/node/storage/StorageLocalAdapter"; import type { Connection } from "./src/data/connection/Connection"; import { __bknd } from "modules/ModuleManager"; import { nodeSqlite } from "./src/adapter/node/connection/NodeSqliteConnection"; @@ -10,8 +10,6 @@ import { libsql } from "./src/data/connection/sqlite/libsql/LibsqlConnection"; import { $console } from "core/utils"; import { createClient } from "@libsql/client"; -registries.media.register("local", StorageLocalAdapter); - const example = import.meta.env.VITE_EXAMPLE; let connection: Connection; @@ -70,7 +68,7 @@ if (import.meta.env.VITE_DB_LIBSQL_URL) { } */ let app: App; -const recreate = import.meta.env.VITE_APP_FRESH === "1"; +const recreate = true; //import.meta.env.VITE_APP_FRESH === "1"; const debugRerenders = import.meta.env.VITE_DEBUG_RERENDERS === "1"; let firstStart = true; export default { @@ -78,6 +76,13 @@ export default { if (!app || recreate) { app = App.create({ connection, + options: { + manager: { + onModulesCreated: (modules, ctx) => { + modules.media.adapters.set("local", StorageLocalAdapter); + }, + }, + }, }); app.emgr.onEvent( App.Events.AppBuiltEvent, diff --git a/examples/astro/bknd.config.ts b/examples/astro/bknd.config.ts index 8ddea7e8..fbd55b27 100644 --- a/examples/astro/bknd.config.ts +++ b/examples/astro/bknd.config.ts @@ -3,9 +3,6 @@ import { registerLocalMediaAdapter } from "bknd/adapter/node"; import { boolean, em, entity, text } from "bknd/data"; import { secureRandomString } from "bknd/utils"; -// since we're running in node, we can register the local media adapter -const local = registerLocalMediaAdapter(); - // the em() function makes it easy to create an initial schema const schema = em({ todos: entity("todos", { @@ -41,11 +38,17 @@ export default { // ... and media media: { enabled: true, - adapter: local({ - path: "./public/uploads", - }), + adapter: { + type: "local", + config: { + path: "./public/uploads", + }, + }, }, }, + beforeBuild: (app) => { + registerLocalMediaAdapter(app); + }, options: { // the seed option is only executed if the database was empty seed: async (ctx) => { diff --git a/examples/nextjs/bknd.config.ts b/examples/nextjs/bknd.config.ts index 39a12a7f..e8c43825 100644 --- a/examples/nextjs/bknd.config.ts +++ b/examples/nextjs/bknd.config.ts @@ -3,16 +3,6 @@ import { boolean, em, entity, text } from "bknd/data"; import { registerLocalMediaAdapter } from "bknd/adapter/node"; import { secureRandomString } from "bknd/utils"; -// 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. -const local = registerLocalMediaAdapter(); - const schema = em({ todos: entity("todos", { title: text(), @@ -50,11 +40,23 @@ export default { // ... and media media: { enabled: true, - adapter: local({ - path: "./public/uploads", - }), + adapter: { + type: "local", + config: { + path: "./public/uploads", + }, + }, }, }, + beforeBuild: (app) => { + // 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. + registerLocalMediaAdapter(app); + }, options: { // the seed option is only executed if the database was empty seed: async (ctx) => { diff --git a/examples/react-router/bknd.config.ts b/examples/react-router/bknd.config.ts index 06239b34..1850f447 100644 --- a/examples/react-router/bknd.config.ts +++ b/examples/react-router/bknd.config.ts @@ -3,9 +3,6 @@ import type { ReactRouterBkndConfig } from "bknd/adapter/react-router"; import { boolean, em, entity, text } from "bknd/data"; import { secureRandomString } from "bknd/utils"; -// since we're running in node, we can register the local media adapter -const local = registerLocalMediaAdapter(); - const schema = em({ todos: entity("todos", { title: text(), @@ -40,11 +37,18 @@ export default { // ... and media media: { enabled: true, - adapter: local({ - path: "./public/uploads", - }), + adapter: { + type: "local", + config: { + path: "./public/uploads", + }, + }, }, }, + beforeBuild: (app) => { + // since we're running in node, we can register the local media adapter + registerLocalMediaAdapter(app); + }, options: { // the seed option is only executed if the database was empty seed: async (ctx) => {