@strav/kernel 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.
Files changed (64) hide show
  1. package/package.json +59 -0
  2. package/src/cache/cache_manager.ts +60 -0
  3. package/src/cache/cache_store.ts +31 -0
  4. package/src/cache/helpers.ts +74 -0
  5. package/src/cache/index.ts +4 -0
  6. package/src/cache/memory_store.ts +63 -0
  7. package/src/config/configuration.ts +105 -0
  8. package/src/config/index.ts +2 -0
  9. package/src/config/loaders/base_loader.ts +69 -0
  10. package/src/config/loaders/env_loader.ts +112 -0
  11. package/src/config/loaders/typescript_loader.ts +56 -0
  12. package/src/config/types.ts +8 -0
  13. package/src/core/application.ts +241 -0
  14. package/src/core/container.ts +113 -0
  15. package/src/core/index.ts +4 -0
  16. package/src/core/inject.ts +39 -0
  17. package/src/core/service_provider.ts +44 -0
  18. package/src/encryption/encryption_manager.ts +215 -0
  19. package/src/encryption/helpers.ts +158 -0
  20. package/src/encryption/index.ts +3 -0
  21. package/src/encryption/types.ts +6 -0
  22. package/src/events/emitter.ts +101 -0
  23. package/src/events/index.ts +2 -0
  24. package/src/exceptions/errors.ts +71 -0
  25. package/src/exceptions/exception_handler.ts +140 -0
  26. package/src/exceptions/helpers.ts +25 -0
  27. package/src/exceptions/http_exception.ts +132 -0
  28. package/src/exceptions/index.ts +23 -0
  29. package/src/exceptions/strav_error.ts +11 -0
  30. package/src/helpers/compose.ts +104 -0
  31. package/src/helpers/crypto.ts +4 -0
  32. package/src/helpers/env.ts +50 -0
  33. package/src/helpers/index.ts +6 -0
  34. package/src/helpers/strings.ts +67 -0
  35. package/src/helpers/ulid.ts +28 -0
  36. package/src/i18n/defaults/en/validation.json +20 -0
  37. package/src/i18n/helpers.ts +76 -0
  38. package/src/i18n/i18n_manager.ts +157 -0
  39. package/src/i18n/index.ts +3 -0
  40. package/src/i18n/translator.ts +96 -0
  41. package/src/i18n/types.ts +17 -0
  42. package/src/index.ts +11 -0
  43. package/src/logger/index.ts +5 -0
  44. package/src/logger/logger.ts +113 -0
  45. package/src/logger/sinks/console_sink.ts +24 -0
  46. package/src/logger/sinks/file_sink.ts +24 -0
  47. package/src/logger/sinks/sink.ts +36 -0
  48. package/src/providers/cache_provider.ts +16 -0
  49. package/src/providers/config_provider.ts +26 -0
  50. package/src/providers/encryption_provider.ts +16 -0
  51. package/src/providers/i18n_provider.ts +17 -0
  52. package/src/providers/index.ts +8 -0
  53. package/src/providers/logger_provider.ts +16 -0
  54. package/src/providers/storage_provider.ts +16 -0
  55. package/src/storage/index.ts +32 -0
  56. package/src/storage/local_driver.ts +46 -0
  57. package/src/storage/ostra_client.ts +432 -0
  58. package/src/storage/ostra_driver.ts +58 -0
  59. package/src/storage/s3_driver.ts +51 -0
  60. package/src/storage/storage.ts +43 -0
  61. package/src/storage/storage_manager.ts +70 -0
  62. package/src/storage/types.ts +49 -0
  63. package/src/storage/upload.ts +91 -0
  64. package/tsconfig.json +5 -0
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@strav/kernel",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Foundation of the Strav framework — application lifecycle, IoC container, configuration, events, and core utilities",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "bun",
9
+ "framework",
10
+ "typescript",
11
+ "strav"
12
+ ],
13
+ "files": [
14
+ "src/",
15
+ "package.json",
16
+ "tsconfig.json",
17
+ "CHANGELOG.md"
18
+ ],
19
+ "exports": {
20
+ ".": "./src/index.ts",
21
+ "./core": "./src/core/index.ts",
22
+ "./core/*": "./src/core/*.ts",
23
+ "./config": "./src/config/index.ts",
24
+ "./config/*": "./src/config/*.ts",
25
+ "./events": "./src/events/index.ts",
26
+ "./events/*": "./src/events/*.ts",
27
+ "./exceptions": "./src/exceptions/index.ts",
28
+ "./exceptions/*": "./src/exceptions/*.ts",
29
+ "./helpers": "./src/helpers/index.ts",
30
+ "./helpers/*": "./src/helpers/*.ts",
31
+ "./encryption": "./src/encryption/index.ts",
32
+ "./encryption/*": "./src/encryption/*.ts",
33
+ "./storage": "./src/storage/index.ts",
34
+ "./storage/*": "./src/storage/*.ts",
35
+ "./cache": "./src/cache/index.ts",
36
+ "./cache/*": "./src/cache/*.ts",
37
+ "./i18n": "./src/i18n/index.ts",
38
+ "./i18n/*": "./src/i18n/*.ts",
39
+ "./logger": "./src/logger/index.ts",
40
+ "./logger/*": "./src/logger/*.ts",
41
+ "./providers": "./src/providers/index.ts",
42
+ "./providers/*": "./src/providers/*.ts"
43
+ },
44
+ "dependencies": {
45
+ "@types/luxon": "^3.7.1",
46
+ "luxon": "^3.7.2",
47
+ "pino": "^10.3.1",
48
+ "pino-pretty": "^13.1.3",
49
+ "reflect-metadata": "^0.2.2",
50
+ "ulid": "^3.0.2"
51
+ },
52
+ "scripts": {
53
+ "test": "bun test tests/",
54
+ "typecheck": "tsc --noEmit"
55
+ },
56
+ "devDependencies": {
57
+ "bun-types": "^1.3.9"
58
+ }
59
+ }
@@ -0,0 +1,60 @@
1
+ import { inject } from '../core/inject.ts'
2
+ import { ConfigurationError } from '../exceptions/errors.ts'
3
+ import Configuration from '../config/configuration.ts'
4
+ import { MemoryCacheStore } from './memory_store.ts'
5
+ import type { CacheStore, CacheConfig } from './cache_store.ts'
6
+
7
+ /**
8
+ * Central cache configuration hub.
9
+ *
10
+ * Resolved once via the DI container — reads the cache config
11
+ * and initializes the appropriate store driver.
12
+ *
13
+ * @example
14
+ * app.singleton(CacheManager)
15
+ * app.resolve(CacheManager)
16
+ *
17
+ * // Plug in a custom store (e.g., Redis)
18
+ * CacheManager.useStore(new MyRedisStore())
19
+ */
20
+ @inject
21
+ export default class CacheManager {
22
+ private static _store: CacheStore
23
+ private static _config: CacheConfig
24
+
25
+ constructor(config: Configuration) {
26
+ CacheManager._config = {
27
+ default: 'memory',
28
+ prefix: '',
29
+ ttl: 3600,
30
+ ...(config.get('cache', {}) as object),
31
+ }
32
+
33
+ const driver = CacheManager._config.default
34
+ if (driver === 'memory') {
35
+ CacheManager._store = new MemoryCacheStore()
36
+ } else {
37
+ throw new ConfigurationError(
38
+ `Unknown cache driver: ${driver}. Use CacheManager.useStore() for custom drivers.`
39
+ )
40
+ }
41
+ }
42
+
43
+ static get store(): CacheStore {
44
+ if (!CacheManager._store) {
45
+ throw new ConfigurationError(
46
+ 'CacheManager not configured. Resolve it through the container first.'
47
+ )
48
+ }
49
+ return CacheManager._store
50
+ }
51
+
52
+ static get config(): CacheConfig {
53
+ return CacheManager._config
54
+ }
55
+
56
+ /** Swap the cache store at runtime (e.g., for testing or a custom Redis store). */
57
+ static useStore(store: CacheStore): void {
58
+ CacheManager._store = store
59
+ }
60
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Pluggable cache storage backend.
3
+ *
4
+ * Implement this interface to use Redis, database, or other stores.
5
+ * The default in-memory implementation uses a Map with lazy TTL eviction.
6
+ */
7
+ export interface CacheStore {
8
+ /** Retrieve a cached value. Returns `null` if the key doesn't exist or has expired. */
9
+ get<T = unknown>(key: string): Promise<T | null>
10
+
11
+ /** Store a value with optional TTL in seconds. Omit TTL for no expiry. */
12
+ set(key: string, value: unknown, ttl?: number): Promise<void>
13
+
14
+ /** Check if a key exists and is not expired. */
15
+ has(key: string): Promise<boolean>
16
+
17
+ /** Remove a single key from the cache. */
18
+ forget(key: string): Promise<void>
19
+
20
+ /** Remove all entries from the cache. */
21
+ flush(): Promise<void>
22
+ }
23
+
24
+ export interface CacheConfig {
25
+ /** Cache driver name. @default 'memory' */
26
+ default: string
27
+ /** Key prefix applied to all cache operations. @default '' */
28
+ prefix: string
29
+ /** Default TTL in seconds when none is specified. @default 3600 */
30
+ ttl: number
31
+ }
@@ -0,0 +1,74 @@
1
+ import CacheManager from './cache_manager.ts'
2
+
3
+ function prefixed(key: string): string {
4
+ return CacheManager.config.prefix + key
5
+ }
6
+
7
+ /**
8
+ * Cache helper object — the primary API for cache-aside operations.
9
+ *
10
+ * All methods delegate to `CacheManager.store` with the configured prefix.
11
+ *
12
+ * @example
13
+ * import { cache } from '@stravigor/kernel/cache'
14
+ *
15
+ * const user = await cache.remember(`user:${id}`, 300, () => User.find(id))
16
+ * await cache.forget(`user:${id}`)
17
+ */
18
+ export const cache = {
19
+ /** Retrieve a cached value. */
20
+ async get<T = unknown>(key: string): Promise<T | null> {
21
+ return CacheManager.store.get<T>(prefixed(key))
22
+ },
23
+
24
+ /** Store a value with optional TTL in seconds. Falls back to config default. */
25
+ async set(key: string, value: unknown, ttl?: number): Promise<void> {
26
+ return CacheManager.store.set(prefixed(key), value, ttl ?? CacheManager.config.ttl)
27
+ },
28
+
29
+ /** Check if a key exists and is not expired. */
30
+ async has(key: string): Promise<boolean> {
31
+ return CacheManager.store.has(prefixed(key))
32
+ },
33
+
34
+ /** Remove a cached value. */
35
+ async forget(key: string): Promise<void> {
36
+ return CacheManager.store.forget(prefixed(key))
37
+ },
38
+
39
+ /** Clear all cached values. */
40
+ async flush(): Promise<void> {
41
+ return CacheManager.store.flush()
42
+ },
43
+
44
+ /**
45
+ * Cache-aside: return cached value or execute factory and cache the result.
46
+ *
47
+ * @example
48
+ * const user = await cache.remember(`user:${id}`, 300, () => User.find(id))
49
+ * const stats = await cache.remember('stats', 60, async () => ({
50
+ * users: await User.count(),
51
+ * projects: await Project.count(),
52
+ * }))
53
+ */
54
+ async remember<T>(key: string, ttl: number, factory: () => T | Promise<T>): Promise<T> {
55
+ const pk = prefixed(key)
56
+ const cached = await CacheManager.store.get<T>(pk)
57
+ if (cached !== null) return cached
58
+
59
+ const value = await factory()
60
+ await CacheManager.store.set(pk, value, ttl)
61
+ return value
62
+ },
63
+
64
+ /** Cache-aside with no expiry. */
65
+ async rememberForever<T>(key: string, factory: () => T | Promise<T>): Promise<T> {
66
+ const pk = prefixed(key)
67
+ const cached = await CacheManager.store.get<T>(pk)
68
+ if (cached !== null) return cached
69
+
70
+ const value = await factory()
71
+ await CacheManager.store.set(pk, value)
72
+ return value
73
+ },
74
+ }
@@ -0,0 +1,4 @@
1
+ export { default as CacheManager } from './cache_manager.ts'
2
+ export { MemoryCacheStore } from './memory_store.ts'
3
+ export { cache } from './helpers.ts'
4
+ export type { CacheStore, CacheConfig } from './cache_store.ts'
@@ -0,0 +1,63 @@
1
+ import type { CacheStore } from './cache_store.ts'
2
+
3
+ interface CacheEntry {
4
+ value: unknown
5
+ expiresAt: number | null // null = no expiry
6
+ }
7
+
8
+ /**
9
+ * In-memory cache store using a Map with lazy TTL eviction.
10
+ * Suitable for single-process deployments.
11
+ */
12
+ export class MemoryCacheStore implements CacheStore {
13
+ private entries = new Map<string, CacheEntry>()
14
+
15
+ async get<T = unknown>(key: string): Promise<T | null> {
16
+ const entry = this.entries.get(key)
17
+ if (!entry) return null
18
+
19
+ if (entry.expiresAt !== null && Date.now() >= entry.expiresAt) {
20
+ this.entries.delete(key)
21
+ return null
22
+ }
23
+
24
+ return entry.value as T
25
+ }
26
+
27
+ async set(key: string, value: unknown, ttl?: number): Promise<void> {
28
+ const expiresAt = ttl != null ? Date.now() + ttl * 1000 : null
29
+
30
+ this.entries.set(key, { value, expiresAt })
31
+
32
+ if (this.entries.size > 10_000) this.cleanup()
33
+ }
34
+
35
+ async has(key: string): Promise<boolean> {
36
+ const entry = this.entries.get(key)
37
+ if (!entry) return false
38
+
39
+ if (entry.expiresAt !== null && Date.now() >= entry.expiresAt) {
40
+ this.entries.delete(key)
41
+ return false
42
+ }
43
+
44
+ return true
45
+ }
46
+
47
+ async forget(key: string): Promise<void> {
48
+ this.entries.delete(key)
49
+ }
50
+
51
+ async flush(): Promise<void> {
52
+ this.entries.clear()
53
+ }
54
+
55
+ private cleanup(): void {
56
+ const now = Date.now()
57
+ for (const [key, entry] of this.entries) {
58
+ if (entry.expiresAt !== null && now >= entry.expiresAt) {
59
+ this.entries.delete(key)
60
+ }
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,105 @@
1
+ import { readdirSync } from 'node:fs'
2
+ import { basename, join } from 'node:path'
3
+ import EnvLoader from './loaders/env_loader'
4
+ import TypescriptLoader from './loaders/typescript_loader'
5
+ import type { ConfigData, ConfigurationLoader } from './types'
6
+
7
+ export default class Configuration {
8
+ private data: ConfigData = {}
9
+ private configPath: string
10
+ private environment?: string
11
+ private loaders: ConfigurationLoader[] = [new EnvLoader(), new TypescriptLoader()]
12
+
13
+ constructor(configPath: string = './config', environment?: string) {
14
+ this.configPath = configPath
15
+ this.environment = environment
16
+ }
17
+
18
+ /**
19
+ * Scan the config directory and load every supported file.
20
+ * Each file is stored under a key derived from its name
21
+ * (e.g. `database.ts` → `"database"`).
22
+ */
23
+ async load(): Promise<void> {
24
+ let files: string[]
25
+ try {
26
+ files = readdirSync(this.configPath)
27
+ } catch {
28
+ return
29
+ }
30
+
31
+ for (const file of files) {
32
+ const filePath = join(this.configPath, file)
33
+ const loader = this.loaders.find(l => l.supports(filePath))
34
+ if (!loader) continue
35
+
36
+ const config = await loader.load(filePath, this.environment)
37
+ if (config === null) continue
38
+
39
+ let key = basename(file, '.' + file.split('.').pop())
40
+ // Handle dotfiles like .env where basename strips the entire name
41
+ if (!key) key = file.replace(/^\./, '')
42
+ this.data[key] = config
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Retrieve a config value using dot notation.
48
+ *
49
+ * @example
50
+ * config.get('database.host') // value of data.database.host
51
+ * config.get('database.port', 3306) // with fallback
52
+ */
53
+ get(key: string, defaultValue?: any): any {
54
+ const parts = key.split('.')
55
+ let current: any = this.data
56
+
57
+ for (const part of parts) {
58
+ if (current === undefined || current === null || typeof current !== 'object') {
59
+ return defaultValue
60
+ }
61
+ current = current[part]
62
+ }
63
+
64
+ return current !== undefined ? current : defaultValue
65
+ }
66
+
67
+ /** Check whether a key exists (even if its value is `undefined`). */
68
+ has(key: string): boolean {
69
+ const parts = key.split('.')
70
+ let current: any = this.data
71
+
72
+ for (const part of parts) {
73
+ if (current === undefined || current === null || typeof current !== 'object') {
74
+ return false
75
+ }
76
+ if (!(part in current)) {
77
+ return false
78
+ }
79
+ current = current[part]
80
+ }
81
+
82
+ return true
83
+ }
84
+
85
+ /** Set a config value using dot notation. */
86
+ set(key: string, value: any): void {
87
+ const parts = key.split('.')
88
+ const last = parts.pop()!
89
+ let current: any = this.data
90
+
91
+ for (const part of parts) {
92
+ if (current[part] === undefined || typeof current[part] !== 'object') {
93
+ current[part] = {}
94
+ }
95
+ current = current[part]
96
+ }
97
+
98
+ current[last] = value
99
+ }
100
+
101
+ /** Return all loaded config data. */
102
+ all(): ConfigData {
103
+ return this.data
104
+ }
105
+ }
@@ -0,0 +1,2 @@
1
+ export { default as Configuration } from './configuration'
2
+ export type { ConfigData, ConfigurationLoader } from './types'
@@ -0,0 +1,69 @@
1
+ import type { ConfigData, ConfigurationLoader } from '../types'
2
+
3
+ /**
4
+ * Abstract base class for configuration loaders.
5
+ *
6
+ * Subclasses declare the file extensions they handle via {@link extensions} and
7
+ * implement {@link load} to parse a specific file format. Shared helpers for
8
+ * extension matching and environment-based config extraction are provided here.
9
+ */
10
+ export abstract class BaseConfigurationLoader implements ConfigurationLoader {
11
+ /** File extensions this loader can handle (without the leading dot). */
12
+ protected extensions: string[] = []
13
+
14
+ /**
15
+ * Load and parse a configuration file.
16
+ *
17
+ * @param filePath - Absolute path to the configuration file.
18
+ * @param environment - Optional environment key (e.g. `"production"`) used
19
+ * to select a nested section from the loaded config.
20
+ * @returns The parsed configuration data, or `null` if the file does not exist.
21
+ */
22
+ abstract load(filePath: string, environment?: string): Promise<ConfigData>
23
+
24
+ /**
25
+ * Check whether this loader supports the given file path based on its
26
+ * extension.
27
+ *
28
+ * @param filePath - Path to test.
29
+ * @returns `true` if the file's extension is in {@link extensions}.
30
+ */
31
+ supports(filePath: string): boolean {
32
+ const ext = this.getFileExtension(filePath)
33
+ return this.extensions.includes(ext)
34
+ }
35
+
36
+ /**
37
+ * Extract the lowercase file extension from a path.
38
+ *
39
+ * @param filePath - The file path to inspect.
40
+ * @returns The extension without a leading dot, or an empty string if none.
41
+ */
42
+ protected getFileExtension(filePath: string): string {
43
+ return filePath.split('.').pop()?.toLowerCase() || ''
44
+ }
45
+
46
+ /**
47
+ * Return the environment-specific subset of a configuration object.
48
+ *
49
+ * If `environment` is provided and the config object contains a matching
50
+ * top-level key, that nested value is returned. Otherwise the full config
51
+ * is returned unchanged.
52
+ *
53
+ * @param config - The full configuration object.
54
+ * @param environment - Optional environment key to look up.
55
+ * @returns The environment subset, or the original config if no match.
56
+ */
57
+ protected extractEnvironmentConfig(config: any, environment?: string): any {
58
+ if (!environment || typeof config !== 'object' || config === null) {
59
+ return config
60
+ }
61
+
62
+ // Check if config has environment-specific sections
63
+ if (config[environment]) {
64
+ return config[environment]
65
+ }
66
+
67
+ return config
68
+ }
69
+ }
@@ -0,0 +1,112 @@
1
+ import { BaseConfigurationLoader } from './base_loader'
2
+ import { dirname, join } from 'node:path'
3
+
4
+ /**
5
+ * Configuration loader for `.env` files.
6
+ *
7
+ * Loads the base `.env` file first, then merges values from an
8
+ * environment-specific file (e.g. `.env.test`, `.env.production`) on top,
9
+ * so environment-specific values override the base ones.
10
+ *
11
+ * @example
12
+ * // .env
13
+ * DB_HOST=localhost
14
+ * DB_PORT=5432
15
+ *
16
+ * // .env.production
17
+ * DB_HOST=db.example.com
18
+ *
19
+ * // Result when environment is "production":
20
+ * // { DB_HOST: "db.example.com", DB_PORT: "5432" }
21
+ */
22
+ export default class EnvLoader extends BaseConfigurationLoader {
23
+ protected override extensions = ['env']
24
+
25
+ /**
26
+ * Load a `.env` file and optionally merge an environment-specific override
27
+ * file on top.
28
+ *
29
+ * Given a `filePath` of `/app/config/.env`, this will:
30
+ * 1. Parse `/app/config/.env` (if it exists)
31
+ * 2. Parse `/app/config/.env.{environment}` (if `environment` is provided and the file exists)
32
+ * 3. Return the merged result, with environment-specific values taking precedence.
33
+ *
34
+ * @param filePath - Absolute path to the base `.env` file.
35
+ * @param environment - Optional environment name (e.g. `"test"`, `"production"`).
36
+ * @returns The merged key-value pairs, or `null` if the base file does not exist.
37
+ * @throws If a file exists but cannot be read or parsed.
38
+ */
39
+ async load(filePath: string, environment?: string): Promise<any> {
40
+ const baseConfig = await this.parseEnvFile(filePath)
41
+
42
+ if (baseConfig === null) {
43
+ return null
44
+ }
45
+
46
+ if (!environment) {
47
+ return baseConfig
48
+ }
49
+
50
+ const dir = dirname(filePath)
51
+ const envFilePath = join(dir, `.env.${environment}`)
52
+ const envConfig = await this.parseEnvFile(envFilePath)
53
+
54
+ if (envConfig === null) {
55
+ return baseConfig
56
+ }
57
+
58
+ return { ...baseConfig, ...envConfig }
59
+ }
60
+
61
+ /**
62
+ * Parse a single `.env` file into a key-value object.
63
+ *
64
+ * Supports:
65
+ * - `KEY=value` pairs
66
+ * - Quoted values (single and double quotes are stripped)
67
+ * - Comments (lines starting with `#`)
68
+ * - Blank lines (ignored)
69
+ *
70
+ * @param filePath - Absolute path to the `.env` file.
71
+ * @returns The parsed key-value pairs, or `null` if the file does not exist.
72
+ */
73
+ private async parseEnvFile(filePath: string): Promise<Record<string, string> | null> {
74
+ const file = Bun.file(filePath)
75
+
76
+ if (!(await file.exists())) {
77
+ return null
78
+ }
79
+
80
+ const content = await file.text()
81
+ const result: Record<string, string> = {}
82
+
83
+ for (const line of content.split('\n')) {
84
+ const trimmed = line.trim()
85
+
86
+ // Skip empty lines and comments
87
+ if (trimmed === '' || trimmed.startsWith('#')) {
88
+ continue
89
+ }
90
+
91
+ const separatorIndex = trimmed.indexOf('=')
92
+ if (separatorIndex === -1) {
93
+ continue
94
+ }
95
+
96
+ const key = trimmed.slice(0, separatorIndex).trim()
97
+ let value = trimmed.slice(separatorIndex + 1).trim()
98
+
99
+ // Strip surrounding quotes
100
+ if (
101
+ (value.startsWith('"') && value.endsWith('"')) ||
102
+ (value.startsWith("'") && value.endsWith("'"))
103
+ ) {
104
+ value = value.slice(1, -1)
105
+ }
106
+
107
+ result[key] = value
108
+ }
109
+
110
+ return result
111
+ }
112
+ }
@@ -0,0 +1,56 @@
1
+ import { BaseConfigurationLoader } from './base_loader'
2
+ import { existsSync } from 'node:fs'
3
+ import { pathToFileURL } from 'node:url'
4
+
5
+ /**
6
+ * Configuration loader for TypeScript and JavaScript files.
7
+ *
8
+ * Dynamically imports `.ts` and `.js` configuration files using Bun's native
9
+ * TypeScript support, and optionally extracts an environment-specific section
10
+ * from the exported config object.
11
+ *
12
+ * Config files should default-export a configuration object. When an
13
+ * `environment` is provided, the loader looks for a matching top-level key
14
+ * and returns that subset instead of the full object.
15
+ *
16
+ * @example
17
+ * // config/database.ts
18
+ * export default {
19
+ * development: { host: 'localhost', port: 5432 },
20
+ * production: { host: 'db.example.com', port: 5432 },
21
+ * }
22
+ */
23
+ export default class TypescriptLoader extends BaseConfigurationLoader {
24
+ protected override extensions = ['ts', 'js']
25
+
26
+ /**
27
+ * Load and evaluate a TypeScript or JavaScript configuration file.
28
+ *
29
+ * @param filePath - Absolute path to the config file.
30
+ * @param environment - Optional environment key (e.g. `"production"`) used
31
+ * to extract a nested section from the config object.
32
+ * @returns The resolved configuration object, the environment-specific
33
+ * subset if found, or `null` if the file does not exist.
34
+ * @throws If the file exists but cannot be imported or evaluated.
35
+ */
36
+ async load(filePath: string, environment?: string): Promise<any> {
37
+ if (!existsSync(filePath)) {
38
+ return null
39
+ }
40
+
41
+ try {
42
+ // Convert to file URL for dynamic import
43
+ const fileUrl = pathToFileURL(filePath).href
44
+ const module = await import(fileUrl + '?t=' + Date.now())
45
+
46
+ let config = module.default || module
47
+
48
+ // Extract environment-specific config
49
+ config = this.extractEnvironmentConfig(config, environment)
50
+
51
+ return config
52
+ } catch (error) {
53
+ throw new Error(`Failed to load TypeScript/JavaScript config from ${filePath}: ${error}`)
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,8 @@
1
+ export interface ConfigData {
2
+ [key: string]: any
3
+ }
4
+
5
+ export interface ConfigurationLoader {
6
+ supports(filePath: string): boolean
7
+ load(filePath: string, environment?: string): Promise<ConfigData>
8
+ }