UNPKG

@h3ravel/core

Version:

Core application container, lifecycle management and service providers for H3ravel.

911 lines (897 loc) 26.8 kB
//#region rolldown:runtime var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion require("reflect-metadata"); let __h3ravel_shared = require("@h3ravel/shared"); let __h3ravel_support = require("@h3ravel/support"); let fast_glob = require("fast-glob"); fast_glob = __toESM(fast_glob); let node_path = require("node:path"); node_path = __toESM(node_path); let detect_port = require("detect-port"); let dotenv = require("dotenv"); dotenv = __toESM(dotenv); let dotenv_expand = require("dotenv-expand"); dotenv_expand = __toESM(dotenv_expand); let node_fs_promises = require("node:fs/promises"); let semver = require("semver"); semver = __toESM(semver); let __h3ravel_http = require("@h3ravel/http"); //#region src/Container.ts var Container = class { bindings = /* @__PURE__ */ new Map(); singletons = /* @__PURE__ */ new Map(); static hasAnyDecorator(target) { if (Reflect.getMetadataKeys(target).length > 0) return true; const paramLength = target.length; for (let i = 0; i < paramLength; i++) if (Reflect.getMetadataKeys(target, `__param_${i}`).length > 0) return true; return false; } bind(key, factory) { this.bindings.set(key, factory); } /** * Remove one or more transient services from the container */ unbind(key) { if (Array.isArray(key)) for (let i = 0; i < key.length; i++) { this.bindings.delete(key[i]); this.singletons.delete(key[i]); } else { this.bindings.delete(key); this.singletons.delete(key); } } /** * Bind a singleton service to the container */ singleton(key, factory) { this.bindings.set(key, () => { if (!this.singletons.has(key)) this.singletons.set(key, factory(this)); return this.singletons.get(key); }); } make(key) { /** * Direct factory binding */ if (this.bindings.has(key)) return this.bindings.get(key)(); /** * If this is a class constructor, auto-resolve via reflection */ if (typeof key === "function") return this.build(key); throw new Error(`No binding found for key: ${typeof key === "string" ? key : key?.name}`); } /** * Automatically build a class with constructor dependency injection */ build(ClassType) { let dependencies = []; if (Array.isArray(ClassType.__inject__)) dependencies = ClassType.__inject__.map((alias) => { return this.make(alias); }); else dependencies = (Reflect.getMetadata("design:paramtypes", ClassType) || []).map((dep) => this.make(dep)); return new ClassType(...dependencies); } /** * Check if a service is registered */ has(key) { return this.bindings.has(key); } }; //#endregion //#region src/Di/ContainerResolver.ts var ContainerResolver = class ContainerResolver { constructor(app) { this.app = app; } async resolveMethodParams(instance, method, ..._default) { /** * Get param types for instance method */ let params = Reflect.getMetadata("design:paramtypes", instance, String(method)) || []; /** * Ensure that the Application class is always available */ if (params.length < 1 && _default.length > 0) params = _default; /** * Resolve the bound dependencies */ const args = params.filter((e) => ContainerResolver.isClass(e) || e instanceof Application).map((type) => { if (type instanceof Application) return type; return this.app.make(type); }); return new Promise((resolve) => { resolve(instance[method](...args)); }); } static isClass(C) { return typeof C === "function" && C.prototype !== void 0 && Object.toString.call(C).substring(0, 5) === "class"; } }; //#endregion //#region src/ProviderRegistry.ts var ProviderRegistry = class { static providers = /* @__PURE__ */ new Map(); static priorityMap = /* @__PURE__ */ new Map(); static filteredProviders = []; static sortable = true; /** * Set wether providers should be sorted or not. * * @returns */ static setSortable(sort = true) { this.sortable = sort; } /** * Get a unique identifier for the Provider. * * @param provider * @returns */ static getKey(provider) { const anyProvider = provider; if (typeof anyProvider.uid === "string") return anyProvider.uid; if (typeof anyProvider.id === "string") return anyProvider.id; return provider.name || "AnonymousProvider"; } /** * Register one or more providers. * Duplicate constructors will be ignored. * * @param providers * @returns */ static register(...providers) { const list = this.sortable ? this.sort(providers.concat(...this.providers.values())) : providers.concat(...this.providers.values()); for (const provider of list) { const key = this.getKey(provider); this.providers.set(key, provider); } } /** * Bulk register providers from an array. * * @param providers * @returns */ static registerMany(providers) { const list = this.sortable ? this.sort(providers.concat(...this.providers.values())) : providers.concat(...this.providers.values()); for (const provider of list) { const key = this.getKey(provider); this.providers.set(key, provider); } } /** * Set the filtered providers. * * @returns */ static setFiltered(filtered) { this.filteredProviders = filtered; } /** * Resolve (instantiate) all providers with the given application or Service Container. * * @param app * @returns */ static async resolve(app, useServiceContainer = false) { const providers = Array.from(this.providers.values()).filter((e) => { return !!e && (this.filteredProviders.length < 1 || !this.filteredProviders.includes(e.name)); }); return await Promise.all(providers.map(async (ProviderClass) => { const provider = new ProviderClass(app); if (!useServiceContainer) return Promise.resolve(provider); await new ContainerResolver(app).resolveMethodParams(provider, "register", app); return provider; })); } /** * Sort the service providers * * @param providers * @returns */ static sort(providers) { /** * Base priority (default 0) */ providers.forEach((Provider) => { const key = this.getKey(Provider); this.priorityMap.set(`${Provider.name}::${key}`, Provider.priority ?? 0); }); /** * Handle before/after adjustments */ providers.forEach((Provider) => { const order = Provider.order; if (!order) return; const [direction, target] = order.split(":"); const targetPriority = this.priorityMap.get(target) ?? 0; const key = this.getKey(Provider); if (direction === "before") this.priorityMap.set(`${Provider.name}::${key}`, targetPriority - 1); else if (direction === "after") this.priorityMap.set(`${Provider.name}::${key}`, targetPriority + 1); }); /** * Return service providers sorted based on thier name and priority */ return providers.sort((A, B) => { const keyA = this.getKey(A); const keyB = this.getKey(B); return (this.priorityMap.get(`${B.name}::${keyB}`) ?? 0) - (this.priorityMap.get(`${A.name}::${keyA}`) ?? 0); }); } /** * Sort service providers */ static doSort() { const raw = this.sort(Array.from(this.providers.values())); const providers = /* @__PURE__ */ new Map(); for (const provider of raw) { const key = this.getKey(provider); providers.set(key, provider); } this.providers = providers; } /** * Log the service providers in a table * * @param priorityMap */ static log(providers) { const sorted = Array.from((providers ?? this.providers).values()); console.table(sorted.map((P) => ({ Name: P.constructor.name, Order: P.constructor.order ?? "N/A", Priority: P.constructor.priority }))); console.info(""); } /** * Get all registered providers as an array. * * @returns */ static all() { return Array.from(this.providers.values()); } /** * Check if a provider is already registered. * * @param provider * @returns */ static has(provider) { return this.providers.has(this.getKey(provider)); } /** * Automatically search for and discover service providers in packages. * * @param autoRegister * @returns */ static async discoverProviders(autoRegister = true) { const manifests = await (0, fast_glob.default)([ "node_modules/@h3ravel/*/package.json", "node_modules/@h3ravel-community/*/package.json", "node_modules/h3ravel-*/package.json" ]); const providers = []; if (autoRegister) { for (const manifestPath of manifests) { const pkg = await this.getManifest(node_path.default.resolve(manifestPath)); if (pkg.h3ravel?.providers) providers.push(...await Promise.all(pkg.h3ravel.providers.map(async (name) => (await import(node_path.default.resolve(node_path.default.dirname(manifestPath), "dist/index.js")))[name]))); } for (const provider of providers) { const key = this.getKey(provider); this.providers.set(key, provider); } } return providers; } /** * Get the content of the package.json file * * @param manifestPath * @returns */ static async getManifest(manifestPath) { let pkg; try { pkg = (await import(manifestPath)).default; } catch { const { createRequire } = await import("module"); pkg = createRequire(require("url").pathToFileURL(__filename).href)(manifestPath); } return pkg; } }; //#endregion //#region src/Registerer.ts var Registerer = class Registerer { constructor(app) { this.app = app; } static register(app) { new Registerer(app).bootRegister(); } bootRegister() { globalThis.dd = __h3ravel_support.dd; globalThis.dump = __h3ravel_support.dump; globalThis.app_path = (path$2) => this.appPath(path$2); globalThis.base_path = (path$2) => this.basePath(path$2); globalThis.public_path = (path$2) => this.publicPath(path$2); globalThis.storage_path = (path$2) => this.storagePath(path$2); globalThis.database_path = (path$2) => this.databasePath(path$2); } appPath(path$2) { return this.app.getPath("base", node_path.default.join(`/${process.env.DIST_DIR ?? "src"}/`.replace(/([^:]\/)\/+/g, "$1"), "app", path$2 ?? "")); } basePath(path$2) { return this.app.getPath("base", path$2); } publicPath(path$2) { return this.app.getPath("public", path$2); } storagePath(path$2) { return this.app.getPath("base", node_path.default.join("storage", path$2 ?? "")); } databasePath(path$2) { return this.app.getPath("database", path$2); } }; //#endregion //#region src/Application.ts var Application = class Application extends Container { paths = new __h3ravel_shared.PathLoader(); context; tries = 0; booted = false; basePath; versions = { app: "0.0.0", ts: "0.0.0" }; static versions = { app: "0.0.0", ts: "0.0.0" }; providers = []; externalProviders = []; filteredProviders = []; /** * List of registered console commands */ registeredCommands = []; constructor(basePath) { super(); dotenv_expand.default.expand(dotenv.default.config({ quiet: true })); this.basePath = basePath; this.setPath("base", basePath); this.loadOptions(); this.registerBaseBindings(); Registerer.register(this); } /** * Register core bindings into the container */ registerBaseBindings() { this.bind(Application, () => this); this.bind("path.base", () => this.basePath); this.bind("load.paths", () => this.paths); } async loadOptions() { try { const corePath = __h3ravel_shared.FileSystem.findModulePkg("@h3ravel/core", process.cwd()) ?? ""; const app = JSON.parse(await (0, node_fs_promises.readFile)(node_path.default.join(process.cwd(), "/package.json"), { encoding: "utf8" })); const core = JSON.parse(await (0, node_fs_promises.readFile)(node_path.default.join(corePath, "package.json"), { encoding: "utf8" })); if (core) { this.versions.app = semver.default.minVersion(core.version)?.version ?? this.versions.app; Application.versions.app = this.versions.app; } if (app && app.devDependencies) { this.versions.ts = semver.default.minVersion(app.devDependencies.typescript)?.version ?? this.versions.ts; Application.versions.ts = this.versions.ts; } if (app && app.dependencies) { const versions = Object.fromEntries(Object.entries(app.dependencies).filter(([e]) => e.includes("@h3ravel")).map(([name, ver]) => [__h3ravel_support.Str.afterLast(name, "/"), semver.default.minVersion(ver.includes("work") ? this.versions.app : ver)?.version])); Object.assign(this.versions, versions); Object.assign(Application.versions, versions); } } catch {} } /** * Get all registered providers */ getRegisteredProviders() { return this.providers; } /** * Load default and optional providers dynamically * * Auto-Registration Behavior * * Minimal App: Loads only core, config, http, router by default. * Full-Stack App: Installs database, mail, queue, cache → they self-register via their providers. */ async getConfiguredProviders() { return [(await import("@h3ravel/core")).CoreServiceProvider]; } async getAllProviders() { return [...await this.getConfiguredProviders(), ...this.externalProviders]; } /** * Configure and Dynamically register all configured service providers, then boot the app. * * @param providers All regitererable service providers * @param filtered A list of service provider name strings we do not want to register at all cost * @param autoRegisterProviders If set to false, service providers will not be auto discovered and registered. * * @returns */ async quickStartup(providers, filtered = [], autoRegisterProviders = true) { this.registerProviders(providers, filtered); await this.registerConfiguredProviders(autoRegisterProviders); return this.boot(); } /** * Dynamically register all configured providers * * @param autoRegister If set to false, service providers will not be auto discovered and registered. */ async registerConfiguredProviders(autoRegister = true) { const providers = await this.getAllProviders(); ProviderRegistry.setSortable(false); ProviderRegistry.setFiltered(this.filteredProviders); ProviderRegistry.registerMany(providers); if (autoRegister) await ProviderRegistry.discoverProviders(autoRegister); ProviderRegistry.doSort(); for (const ProviderClass of ProviderRegistry.all()) { if (!ProviderClass) continue; const provider = new ProviderClass(this); await this.register(provider); } } /** * Register service providers * * @param providers * @param filtered */ registerProviders(providers, filtered = []) { this.externalProviders.push(...providers); this.filteredProviders = filtered; } /** * Register a provider */ async register(provider) { await new ContainerResolver(this).resolveMethodParams(provider, "register", this); if (provider.registeredCommands && provider.registeredCommands.length > 0) this.registeredCommands.push(...provider.registeredCommands); this.providers.push(provider); } /** * Register the listed service providers. * * @param commands An array of console commands to register. */ withCommands(commands) { this.registeredCommands = commands; return this; } /** * checks if the application is running in CLI */ runningInConsole() { return typeof process !== "undefined" && !!process.stdout && !!process.stdin; } getRuntimeEnv() { if (typeof window !== "undefined" && typeof document !== "undefined") return "browser"; if (typeof process !== "undefined" && process.versions?.node) return "node"; return "unknown"; } /** * Boot all service providers after registration */ async boot() { if (this.booted) return this; /** * If debug is enabled, let's show the loaded service provider info */ if ((process.env.APP_DEBUG === "true" && process.env.EXTENDED_DEBUG !== "false" || Number(process.env.VERBOSE) > 1) && !this.providers.some((e) => e.runsInConsole)) ProviderRegistry.log(this.providers); for (const provider of this.providers) if (provider.boot) if (Container.hasAnyDecorator(provider.boot)) /** * If the service provider is decorated use the IoC container */ await this.make(provider.boot); else /** * Otherwise instantiate manually so that we can at least * pass the app instance */ await provider.boot(this); this.booted = true; return this; } async fire(h3App, preferredPort) { if (!h3App) throw new __h3ravel_support.InvalidArgumentException("No valid H3 app instance was provided."); const serve = this.make("http.serve"); const port = preferredPort ?? env("PORT", 3e3); const tries = env("RETRIES", 1); const hostname = env("HOSTNAME", "localhost"); try { const realPort = await (0, detect_port.detect)(port); if (port == realPort) { const server = serve(h3App, { port, hostname, silent: true }); __h3ravel_shared.Logger.parse([["🚀 H3ravel running at:", "green"], [`${server.options.protocol ?? "http"}://${server.options.hostname}:${server.options.port}`, "cyan"]]); } else if (this.tries <= tries) { await this.fire(h3App, realPort); this.tries++; } else __h3ravel_shared.Logger.parse([["ERROR:", "bgRed"], ["No free port available", "red"]]); } catch (e) { __h3ravel_shared.Logger.parse([ ["An error occured", "bgRed"], [e.message, "red"], [e.stack, "red"] ], "\n"); } return this; } /** * Get the base path of the app * * @returns */ getBasePath() { return this.basePath; } /** * Dynamically retrieves a path property from the class. * Any property ending with "Path" is accessible automatically. * * @param name - The base name of the path property * @returns */ getPath(name, suffix) { return node_path.default.join(this.paths.getPath(name, this.basePath), suffix ?? ""); } /** * Programatically set the paths. * * @param name - The base name of the path property * @param path - The new path * @returns */ setPath(name, path$2) { return this.paths.setPath(name, path$2, this.basePath); } /** * Returns the installed version of the system core and typescript. * * @returns */ getVersion(key) { return this.versions[key]?.replaceAll(/\^|~/g, ""); } /** * Returns the installed version of the system core and typescript. * * @returns */ static getVersion(key) { return this.versions[key]?.replaceAll(/\^|~/g, ""); } }; //#endregion //#region src/Controller.ts /** * Base controller class */ var Controller = class { app; constructor(app) { this.app = app; } }; //#endregion //#region src/Di/Inject.ts function Inject(...dependencies) { return function(target) { target.__inject__ = dependencies; }; } /** * Allows binding dependencies to both class and class methods * * @returns */ function Injectable() { return (...args) => { if (args.length === 1) args[0]; if (args.length === 3) { args[0]; args[1]; args[2]; } }; } //#endregion //#region src/Exceptions/ConfigException.ts var ConfigException = class extends Error { key; constructor(key, type = "config", cause) { const info = { any: `${key} not configured`, env: `${key} environment variable not configured`, config: `${key} config not set` }; const message = __h3ravel_shared.Logger.log([["ERROR:", "bgRed"], [info[type], "white"]], " ", false); super(message, { cause }); this.key = key; } }; //#endregion //#region src/H3ravel.ts /** * Simple global entry point for H3ravel applications * * @param providers * @param basePath * @param callback */ const h3ravel = async (providers = [], basePath = process.cwd(), config = { initialize: false, autoload: false, filteredProviders: [] }, middleware = async () => void 0) => { let h3App; const app = new Application(basePath); await app.quickStartup(providers, config.filteredProviders, config.autoload); try { h3App = app.make("http.app"); app.context = async (event) => { if (event._h3ravelContext) return event._h3ravelContext; __h3ravel_http.Request.enableHttpMethodParameterOverride(); const ctx = __h3ravel_http.HttpContext.init({ app, request: await __h3ravel_http.Request.create(event, app), response: new __h3ravel_http.Response(event, app) }); event._h3ravelContext = ctx; return ctx; }; const kernel = new Kernel(async (event) => app.context(event), [new __h3ravel_http.LogRequests()]); h3App.use((event) => kernel.handle(event, middleware)); } catch { if (!h3App && config.h3) h3App = config.h3; } const originalFire = app.fire; const proxyThis = (function makeProxy(appRef, orig) { return new Proxy(appRef, { get(target, prop, receiver) { if (prop === "fire") return orig; return Reflect.get(target, prop, receiver); }, has(target, prop) { if (prop === "fire") return true; return Reflect.has(target, prop); }, getOwnPropertyDescriptor(target, prop) { if (prop === "fire") return { configurable: true, enumerable: false, writable: true, value: orig }; return Reflect.getOwnPropertyDescriptor(target, prop); } }); })(app, originalFire); if (config.initialize && h3App) return await Reflect.apply(originalFire, app, [h3App]); app.fire = function() { if (!h3App) throw new ConfigException("Provide a H3 app instance in the config or install @h3ravel/http"); return Reflect.apply(originalFire, proxyThis, [h3App]); }; return app; }; //#endregion //#region src/Http/Kernel.ts /** * Kernel class handles middleware execution and response transformations. * It acts as the core middleware pipeline for HTTP requests. */ var Kernel = class { /** * @param context - A factory function that converts an H3Event into an HttpContext. * @param middleware - An array of middleware classes that will be executed in sequence. */ constructor(context, middleware = []) { this.context = context; this.middleware = middleware; } /** * Handles an incoming request and passes it through middleware before invoking the next handler. * * @param event - The raw H3 event object. * @param next - A callback function that represents the next layer (usually the controller or final handler). * @returns A promise resolving to the result of the request pipeline. */ async handle(event, next) { /** * Convert the raw event into a standardized HttpContext */ const ctx = await this.context(event); const { app } = ctx.request; /** * Bind HTTP Response instance to the service container */ app.bind("http.response", () => { return ctx.response; }); /** * Bind HTTP Request instance to the service container */ app.bind("http.request", () => { return ctx.request; }); /** * Run middleware stack and obtain result */ const result = await this.runMiddleware(ctx, () => next(ctx)); /** * If a plain object is returned from a controller or middleware, * automatically set the JSON Content-Type header for the response. */ if (result !== void 0 && this.isPlainObject(result)) event.res.headers.set("Content-Type", "application/json; charset=UTF-8"); return result; } /** * Sequentially runs middleware in the order they were registered. * * @param context - The standardized HttpContext. * @param next - Callback to execute when middleware completes. * @returns A promise resolving to the final handler's result. */ async runMiddleware(context, next) { let index = -1; const runner = async (i) => { if (i <= index) throw new Error("next() called multiple times"); index = i; const middleware = this.middleware[i]; if (middleware) /** * Execute the current middleware and proceed to the next one */ return middleware.handle(context, () => runner(i + 1)); else /** * If no more middleware, call the final handler */ return next(context); }; return runner(0); } /** * Utility function to determine if a value is a plain object or array. * * @param value - The value to check. * @returns True if the value is a plain object or array, otherwise false. */ isPlainObject(value) { return typeof value === "object" && value !== null && (value.constructor === Object || value.constructor === Array); } }; //#endregion //#region src/ServiceProvider.ts const Inference = class {}; var ServiceProvider = class extends Inference { /** * The current app instance */ app; /** * Unique Identifier for the service providers */ static uid; /** * Sort order */ static order; /** * Sort priority */ static priority = 0; /** * Indicate that this service provider only runs in console */ static console = false; /** * List of registered console commands */ registeredCommands; constructor(app) { super(); this.app = app; } /** * Register the listed service providers. * * @param commands An array of console commands to register. * * @deprecated since version 1.16.0. Will be removed in future versions, use `registerCommands` instead */ commands(commands) { this.registerCommands(commands); } /** * Register the listed service providers. * * @param commands An array of console commands to register. */ registerCommands(commands) { this.registeredCommands = commands; } }; //#endregion //#region src/Providers/CoreServiceProvider.ts /** * Bootstraps core services and bindings. * * Bind essential services to the container (logger, config repository). * Register app-level singletons. * Set up exception handling. * * Auto-Registered */ var CoreServiceProvider = class extends ServiceProvider { static priority = 999; register() { Object.assign(globalThis, { str: __h3ravel_support.str }); } boot() { try { Object.assign(globalThis, { asset: this.app.make("asset") }); } catch {} } }; //#endregion exports.Application = Application; exports.ConfigException = ConfigException; exports.Container = Container; exports.ContainerResolver = ContainerResolver; exports.Controller = Controller; exports.CoreServiceProvider = CoreServiceProvider; exports.Inject = Inject; exports.Injectable = Injectable; exports.Kernel = Kernel; exports.ProviderRegistry = ProviderRegistry; exports.Registerer = Registerer; exports.ServiceProvider = ServiceProvider; exports.h3ravel = h3ravel;