@h3ravel/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,164 @@
1
+ import { Container } from './Container'
2
+ import { PathLoader } from './Utils/PathLoader'
3
+ import { ServiceProvider } from './ServiceProvider'
4
+ import path from 'node:path'
5
+
6
+ export class Application extends Container {
7
+ paths = new PathLoader()
8
+ private booted = false
9
+ private versions = { app: '0', ts: '0' }
10
+ private basePath: string
11
+
12
+ private providers: ServiceProvider[] = []
13
+ protected externalProviders: Array<new (_app: Application) => ServiceProvider> = []
14
+
15
+ constructor(basePath: string) {
16
+ super()
17
+ this.basePath = basePath
18
+ this.setPath('base', basePath)
19
+ this.loadOptions()
20
+ this.registerBaseBindings()
21
+ }
22
+
23
+ /**
24
+ * Register core bindings into the container
25
+ */
26
+ protected registerBaseBindings () {
27
+ this.bind(Application, () => this)
28
+ this.bind('path.base', () => this.basePath)
29
+ this.bind('app.paths', () => this.paths)
30
+ }
31
+
32
+ /**
33
+ * Dynamically register all configured providers
34
+ */
35
+ public async registerConfiguredProviders () {
36
+ const providers = await this.getAllProviders()
37
+
38
+ for (const ProviderClass of providers) {
39
+ if (!ProviderClass) continue
40
+ const provider = new ProviderClass(this)
41
+ await this.register(provider)
42
+ }
43
+ }
44
+
45
+ protected async loadOptions () {
46
+ const app = await this.safeImport(this.getPath('base', 'package.json'))
47
+ const core = await this.safeImport('../package.json')
48
+
49
+ if (app && app.dependencies) {
50
+ this.versions.app = app.dependencies['@h3ravel/core']
51
+ }
52
+ if (core && core.devDependencies) {
53
+ this.versions.ts = app.devDependencies.typescript
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Load default and optional providers dynamically
59
+ *
60
+ * Auto-Registration Behavior
61
+ *
62
+ * Minimal App: Loads only core, config, http, router by default.
63
+ * Full-Stack App: Installs database, mail, queue, cache → they self-register via their providers.
64
+ */
65
+ protected async getConfiguredProviders (): Promise<Array<new (_app: Application) => ServiceProvider>> {
66
+ return [
67
+ (await import('@h3ravel/core')).AppServiceProvider,
68
+ (await import('@h3ravel/http')).HttpServiceProvider,
69
+ (await import('@h3ravel/config')).ConfigServiceProvider,
70
+ (await import('@h3ravel/router')).RouteServiceProvider,
71
+ (await import('@h3ravel/router')).AssetsServiceProvider,
72
+ (await import('@h3ravel/core')).ViewServiceProvider,
73
+ (await this.safeImport('@h3ravel/database'))?.DatabaseServiceProvider,
74
+ (await this.safeImport('@h3ravel/cache'))?.CacheServiceProvider,
75
+ (await this.safeImport('@h3ravel/console'))?.ConsoleServiceProvider,
76
+ (await this.safeImport('@h3ravel/queue'))?.QueueServiceProvider,
77
+ (await this.safeImport('@h3ravel/mail'))?.MailServiceProvider,
78
+ ]
79
+ }
80
+
81
+ protected async getAllProviders (): Promise<Array<new (_app: Application) => ServiceProvider>> {
82
+ const coreProviders = await this.getConfiguredProviders()
83
+ return [...coreProviders, ...this.externalProviders]
84
+ }
85
+
86
+ registerProviders (providers: Array<new (_app: Application) => ServiceProvider>): void {
87
+ this.externalProviders.push(...providers)
88
+ }
89
+
90
+ /**
91
+ * Register a provider
92
+ */
93
+ public async register (provider: ServiceProvider) {
94
+ await provider.register()
95
+ this.providers.push(provider)
96
+ }
97
+
98
+ /**
99
+ * Boot all providers after registration
100
+ */
101
+ public async boot () {
102
+ if (this.booted) return
103
+
104
+ for (const provider of this.providers) {
105
+ if (provider.boot) {
106
+ await provider.boot()
107
+ }
108
+ }
109
+
110
+ this.booted = true
111
+ }
112
+
113
+ /**
114
+ * Attempt to dynamically import an optional module
115
+ */
116
+ private async safeImport (moduleName: string) {
117
+ try {
118
+ const mod = await import(moduleName)
119
+ return mod.default ?? mod
120
+ } catch {
121
+ return null
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Get the base path of the app
127
+ *
128
+ * @returns
129
+ */
130
+ getBasePath (): string {
131
+ return this.basePath
132
+ }
133
+
134
+ /**
135
+ * Dynamically retrieves a path property from the class.
136
+ * Any property ending with "Path" is accessible automatically.
137
+ *
138
+ * @param name - The base name of the path property
139
+ * @returns
140
+ */
141
+ getPath (name: Parameters<PathLoader['setPath']>[0], pth?: string) {
142
+ return path.join(this.paths.getPath(name, this.basePath), pth ?? '')
143
+ }
144
+
145
+ /**
146
+ * Programatically set the paths.
147
+ *
148
+ * @param name - The base name of the path property
149
+ * @param path - The new path
150
+ * @returns
151
+ */
152
+ setPath (name: Parameters<PathLoader['setPath']>[0], path: string) {
153
+ return this.paths.setPath(name, path, this.basePath)
154
+ }
155
+
156
+ /**
157
+ * Returns the installed version of the system core and typescript.
158
+ *
159
+ * @returns
160
+ */
161
+ getVersion (key: 'app' | 'ts') {
162
+ return this.versions[key]?.replaceAll(/\^|\~/g, '')
163
+ }
164
+ }
@@ -0,0 +1,71 @@
1
+ import type { Bindings, UseKey } from "./Contracts/BindingsContract"
2
+
3
+ type IBinding = UseKey | (new (..._args: any[]) => unknown)
4
+
5
+ export class Container {
6
+ private bindings = new Map<IBinding, () => unknown>()
7
+ private singletons = new Map<IBinding, unknown>()
8
+
9
+ /**
10
+ * Bind a transient service to the container
11
+ */
12
+ bind<T> (key: new (...args: any[]) => T, factory: () => T): void
13
+ bind<T extends UseKey> (key: T, factory: () => Bindings[T]): void
14
+ bind<T extends UseKey> (
15
+ key: T,
16
+ factory: () => Bindings[T] | T
17
+ ) {
18
+ this.bindings.set(key, factory)
19
+ }
20
+
21
+ /**
22
+ * Bind a singleton service to the container
23
+ */
24
+ singleton<T extends UseKey> (
25
+ key: T | (new (..._args: any[]) => Bindings[T]),
26
+ factory: () => Bindings[T]
27
+ ) {
28
+ this.bindings.set(key, () => {
29
+ if (!this.singletons.has(key)) {
30
+ this.singletons.set(key, factory())
31
+ }
32
+ return this.singletons.get(key)!
33
+ })
34
+ }
35
+
36
+ /**
37
+ * Resolve a service from the container
38
+ */
39
+ make<T extends UseKey> (key: T | (new (..._args: any[]) => Bindings[T])): Bindings[T] {
40
+ // 1️⃣ Direct factory binding
41
+ if (this.bindings.has(key)) {
42
+ return this.bindings.get(key)!() as Bindings[T]
43
+ }
44
+
45
+ // 2️⃣ If class constructor → auto-resolve via reflection
46
+ if (typeof key === 'function') {
47
+ return this.build(key)
48
+ }
49
+
50
+ throw new Error(
51
+ `No binding found for key: ${typeof key === 'string' ? key : (key as any)?.name}`
52
+ )
53
+ }
54
+
55
+ /**
56
+ * Automatically build a class with constructor dependency injection
57
+ */
58
+ private build<T extends UseKey> (ClassType: new (..._args: any[]) => Bindings[T]): Bindings[T] {
59
+ const paramTypes: any[] = Reflect.getMetadata('design:paramtypes', ClassType) || []
60
+ const dependencies = paramTypes.map((dep) => this.make(dep))
61
+ return new ClassType(...dependencies)
62
+ }
63
+
64
+
65
+ /**
66
+ * Check if a service is registered
67
+ */
68
+ has (key: UseKey): boolean {
69
+ return this.bindings.has(key)
70
+ }
71
+ }
File without changes
@@ -0,0 +1,37 @@
1
+ import { DotNestedKeys, DotNestedValue } from "@h3ravel/support";
2
+ import type { H3, serve } from "h3";
3
+
4
+ import type { Edge } from "edge.js";
5
+ import { PathLoader } from "../Utils/PathLoader";
6
+ import type { Router } from "@h3ravel/router";
7
+
8
+ type RemoveIndexSignature<T> = {
9
+ [K in keyof T as string extends K
10
+ ? never
11
+ : number extends K
12
+ ? never
13
+ : K]: T[K]
14
+ }
15
+
16
+ export type Bindings = {
17
+ [key: string]: any;
18
+ env<T extends string> (): NodeJS.ProcessEnv
19
+ env<T extends string> (key: T, def?: any): any
20
+ view: Edge,
21
+ asset (key: string, def?: string): string,
22
+ router: Router
23
+ config: {
24
+ // get<X extends Record<string, any>> (): X
25
+ // get<X extends Record<string, any>, K extends DotNestedKeys<X>> (key: K, def?: any): DotNestedValue<X, K>
26
+ get<X extends Record<string, any>> (): X
27
+ get<X extends Record<string, any>, T extends Extract<keyof X, string>> (key: T, def?: any): X[T]
28
+ set<T extends string> (key: T, value: any): void
29
+ load?(): any
30
+ }
31
+ 'http.app': H3
32
+ 'path.base': string
33
+ 'app.paths': PathLoader
34
+ 'http.serve': typeof serve
35
+ }
36
+
37
+ export type UseKey = keyof RemoveIndexSignature<Bindings>
@@ -0,0 +1,20 @@
1
+ import { HttpContext, IController } from '@h3ravel/http'
2
+
3
+ import { Application } from '.'
4
+
5
+ /**
6
+ * Base controller class
7
+ */
8
+ export abstract class Controller implements IController {
9
+ protected app: Application
10
+
11
+ constructor(app: Application) {
12
+ this.app = app
13
+ }
14
+
15
+ public show (_ctx: HttpContext): any { return }
16
+ public index (_ctx: HttpContext): any { return }
17
+ public store (_ctx: HttpContext): any { return }
18
+ public update (_ctx: HttpContext): any { return }
19
+ public destroy (_ctx: HttpContext): any { return }
20
+ }
@@ -0,0 +1 @@
1
+ export default class { }
@@ -0,0 +1,47 @@
1
+ import { HttpContext, Middleware, Request, Response } from '@h3ravel/http'
2
+
3
+ import type { H3Event } from 'h3'
4
+
5
+ export class Kernel {
6
+ constructor(private middleware: Middleware[] = []) { }
7
+
8
+ async handle (event: H3Event, next: (ctx: HttpContext) => Promise<unknown>): Promise<unknown> {
9
+ const context: HttpContext = {
10
+ request: new Request(event),
11
+ response: new Response(event)
12
+ }
13
+
14
+ const result = await this.runMiddleware(context, () => next(context))
15
+
16
+ // Auto-set JSON header if plain object returned
17
+ if (result !== undefined && this.isPlainObject(result)) {
18
+ event.res.headers.set('Content-Type', 'application/json; charset=UTF-8')
19
+ }
20
+
21
+ return result
22
+ }
23
+
24
+ private async runMiddleware (context: HttpContext, next: (ctx: HttpContext) => Promise<unknown>) {
25
+ let index = -1
26
+
27
+ const runner = async (i: number): Promise<unknown> => {
28
+ if (i <= index) throw new Error('next() called multiple times')
29
+ index = i
30
+ const middleware = this.middleware[i]
31
+
32
+ if (middleware) {
33
+ return middleware.handle(context, () => runner(i + 1))
34
+ } else {
35
+ return next(context)
36
+ }
37
+ }
38
+
39
+ return runner(0)
40
+ }
41
+
42
+ private isPlainObject (value: unknown): value is Record<string, unknown> {
43
+ return typeof value === 'object' &&
44
+ value !== null &&
45
+ (value.constructor === Object || value.constructor === Array)
46
+ }
47
+ }
@@ -0,0 +1,18 @@
1
+ import 'reflect-metadata'
2
+
3
+ import { ServiceProvider } from '../ServiceProvider'
4
+
5
+ /**
6
+ * Bootstraps core services and bindings.
7
+ *
8
+ * Bind essential services to the container (logger, config repository).
9
+ * Register app-level singletons.
10
+ * Set up exception handling.
11
+ *
12
+ * Auto-Registered
13
+ */
14
+ export class AppServiceProvider extends ServiceProvider {
15
+ register () {
16
+ // Core bindings
17
+ }
18
+ }
@@ -0,0 +1,19 @@
1
+ import { Edge } from 'edge.js'
2
+ import { ServiceProvider } from '@h3ravel/core'
3
+
4
+ export class ViewServiceProvider extends ServiceProvider {
5
+ register (): void {
6
+ const config = this.app.make('config')
7
+ const edge = Edge.create({
8
+ cache: process.env.NODE_ENV === 'production'
9
+ })
10
+
11
+ edge.mount(this.app.getPath('views'))
12
+
13
+ edge.global('asset', this.app.make('asset'))
14
+ edge.global('config', config.get)
15
+ edge.global('app', this.app)
16
+
17
+ this.app.bind('view', () => edge)
18
+ }
19
+ }
@@ -0,0 +1,21 @@
1
+ import { Application } from './Application'
2
+
3
+ export abstract class ServiceProvider {
4
+ protected app: Application
5
+
6
+ constructor(app: Application) {
7
+ this.app = app
8
+ }
9
+
10
+ /**
11
+ * Register bindings to the container.
12
+ * Runs before boot().
13
+ */
14
+ abstract register (): void | Promise<void>
15
+
16
+ /**
17
+ * Perform post-registration booting of services.
18
+ * Runs after all providers have been registered.
19
+ */
20
+ boot?(): void | Promise<void>
21
+ }
@@ -0,0 +1,46 @@
1
+ import nodepath from "path"
2
+
3
+ type PathName = 'views' | 'routes' | 'assets' | 'base' | 'public' | 'storage' | 'config'
4
+
5
+ export class PathLoader {
6
+ private paths = {
7
+ base: '',
8
+ views: '/src/resources/views',
9
+ assets: '/public/assets',
10
+ routes: '/src/routes',
11
+ config: '/src/config',
12
+ public: '/public',
13
+ storage: '/src/storage',
14
+ }
15
+
16
+ /**
17
+ * Dynamically retrieves a path property from the class.
18
+ * Any property ending with "Path" is accessible automatically.
19
+ *
20
+ * @param name - The base name of the path property
21
+ * @param base - The base path to include to the path
22
+ * @returns
23
+ */
24
+ getPath (name: PathName, base?: string): string {
25
+ if (base && name !== 'base') {
26
+ return nodepath.join(base, this.paths[name])
27
+ }
28
+
29
+ return this.paths[name]
30
+ }
31
+
32
+ /**
33
+ * Programatically set the paths.
34
+ *
35
+ * @param name - The base name of the path property
36
+ * @param path - The new path
37
+ * @param base - The base path to include to the path
38
+ */
39
+ setPath (name: PathName, path: string, base?: string) {
40
+ if (base && name !== 'base') {
41
+ this.paths[name] = nodepath.join(base, path)
42
+ }
43
+
44
+ this.paths[name] = path
45
+ }
46
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @file Automatically generated by barrelsby.
3
+ */
4
+
5
+ export * from './Application';
6
+ export * from './Container';
7
+ export * from './Controller';
8
+ export * from './ServiceProvider';
9
+ export * from './Contracts/BindingsContract';
10
+ export * from './Exceptions/Handler';
11
+ export * from './Http/Kernel';
12
+ export * from './Providers/AppServiceProvider';
13
+ export * from './Providers/ViewServiceProvider';
14
+ export * from './Utils/PathLoader';
package/tests/.gitkeep ADDED
File without changes
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist"
5
+ },
6
+ "include": ["src", "../http/src/Middleware", "../http/src/Middleware.ts"],
7
+ "exclude": ["dist", "node_modules"]
8
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['esm', 'cjs'],
6
+ dts: true,
7
+ sourcemap: true,
8
+ clean: true
9
+ })