@raubjo/architect 0.5.1
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.
- package/README.md +860 -0
- package/package.json +121 -0
- package/src/cache/cache.ts +46 -0
- package/src/cache/contract.ts +9 -0
- package/src/cache/manager.ts +110 -0
- package/src/cache/provider.ts +11 -0
- package/src/config/contract.ts +63 -0
- package/src/config/discovery.ts +99 -0
- package/src/config/env.global.d.ts +6 -0
- package/src/config/env.ts +68 -0
- package/src/config/index.ts +5 -0
- package/src/config/provider.ts +17 -0
- package/src/config/repository.ts +164 -0
- package/src/container/adapters/builtin.ts +323 -0
- package/src/container/contract.ts +43 -0
- package/src/container/runtime.ts +29 -0
- package/src/events/bus.ts +174 -0
- package/src/events/concerns/dispatchable.ts +10 -0
- package/src/events/provider.ts +9 -0
- package/src/events/types.ts +9 -0
- package/src/foundation/application.ts +136 -0
- package/src/foundation/current-application.ts +20 -0
- package/src/index.ts +58 -0
- package/src/log/contract.ts +21 -0
- package/src/log/drivers/console.ts +54 -0
- package/src/log/drivers/null.ts +23 -0
- package/src/log/drivers/stack.ts +46 -0
- package/src/log/manager.ts +76 -0
- package/src/log/provider.ts +11 -0
- package/src/react.ts +2 -0
- package/src/renderers/adapters/react.tsx +25 -0
- package/src/renderers/adapters/solid.tsx +26 -0
- package/src/renderers/adapters/svelte.ts +73 -0
- package/src/renderers/adapters/vue.ts +22 -0
- package/src/renderers/contract.ts +12 -0
- package/src/runtimes/react.tsx +81 -0
- package/src/runtimes/solid.tsx +47 -0
- package/src/runtimes/svelte.ts +17 -0
- package/src/runtimes/vue.ts +34 -0
- package/src/solid.ts +2 -0
- package/src/store/adapters/contract.ts +11 -0
- package/src/store/adapters/indexed-db.ts +187 -0
- package/src/store/adapters/local-storage.ts +48 -0
- package/src/store/adapters/memory.ts +35 -0
- package/src/store/manager.ts +68 -0
- package/src/store/provider.ts +10 -0
- package/src/store/store.ts +1 -0
- package/src/support/arr.ts +372 -0
- package/src/support/collection.ts +889 -0
- package/src/support/facades/cache.ts +6 -0
- package/src/support/facades/config.ts +6 -0
- package/src/support/facades/event.ts +6 -0
- package/src/support/facades/facade.ts +146 -0
- package/src/support/facades/index.ts +5 -0
- package/src/support/facades/log.ts +6 -0
- package/src/support/facades/store.ts +6 -0
- package/src/support/fluent.ts +56 -0
- package/src/support/globals.ts +8 -0
- package/src/support/lazy-collection.ts +341 -0
- package/src/support/manager.ts +53 -0
- package/src/support/num.ts +50 -0
- package/src/support/pipeline.ts +29 -0
- package/src/support/service-provider.ts +19 -0
- package/src/support/str.ts +682 -0
- package/src/svelte.ts +2 -0
- package/src/types/peer-deps.d.ts +10 -0
- package/src/vue.ts +2 -0
- package/tsconfig.json +15 -0
package/package.json
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@raubjo/architect",
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "bun test",
|
|
12
|
+
"test:coverage": "bun test --coverage --coverage-reporter=lcov --coverage-dir=coverage && node scripts/lcov-to-istanbul.js",
|
|
13
|
+
"fallow": "npm run test:coverage && npx fallow health --coverage coverage/coverage-final.json",
|
|
14
|
+
"fix": "biome check --fix ./src/**/* && fallow fix",
|
|
15
|
+
"sandcastle": "bun .sandcastle/main.ts",
|
|
16
|
+
"version:major": "npm verison major",
|
|
17
|
+
"version:minor": "npm verison minor",
|
|
18
|
+
"version:patch": "npm verison patch",
|
|
19
|
+
"release": "npm publish",
|
|
20
|
+
"prepare": "bun husky"
|
|
21
|
+
},
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./src/index.ts",
|
|
24
|
+
"./react": "./src/react.ts",
|
|
25
|
+
"./solid": "./src/solid.ts",
|
|
26
|
+
"./svelte": "./src/svelte.ts",
|
|
27
|
+
"./vue": "./src/vue.ts",
|
|
28
|
+
"./application": "./src/foundation/application.ts",
|
|
29
|
+
"./container/contract": "./src/container/contract.ts",
|
|
30
|
+
"./container/adapters/builtin": "./src/container/adapters/builtin.ts",
|
|
31
|
+
"./config/env": "./src/config/env.ts",
|
|
32
|
+
"./config/repository": "./src/config/repository.ts",
|
|
33
|
+
"./cache/manager": "./src/cache/manager.ts",
|
|
34
|
+
"./cache/cache": "./src/cache/cache.ts",
|
|
35
|
+
"./cache/provider": "./src/cache/provider.ts",
|
|
36
|
+
"./facades/cache": "./src/support/facades/cache.ts",
|
|
37
|
+
"./facades/event": "./src/support/facades/event.ts",
|
|
38
|
+
"./facades/log": "./src/support/facades/log.ts",
|
|
39
|
+
"./log/contract": "./src/log/contract.ts",
|
|
40
|
+
"./log/manager": "./src/log/manager.ts",
|
|
41
|
+
"./log/provider": "./src/log/provider.ts",
|
|
42
|
+
"./log/drivers/console": "./src/log/drivers/console.ts",
|
|
43
|
+
"./log/drivers/null": "./src/log/drivers/null.ts",
|
|
44
|
+
"./log/drivers/stack": "./src/log/drivers/stack.ts",
|
|
45
|
+
"./config/provider": "./src/config/provider.ts",
|
|
46
|
+
"./events/bus": "./src/events/bus.ts",
|
|
47
|
+
"./events/concerns/dispatchable": "./src/events/concerns/dispatchable.ts",
|
|
48
|
+
"./events/provider": "./src/events/provider.ts",
|
|
49
|
+
"./store/manager": "./src/store/manager.ts",
|
|
50
|
+
"./store/adapters/contract": "./src/store/adapters/contract.ts",
|
|
51
|
+
"./store/store": "./src/store/store.ts",
|
|
52
|
+
"./store/adapters/memory": "./src/store/adapters/memory.ts",
|
|
53
|
+
"./store/adapters/local-storage": "./src/store/adapters/local-storage.ts",
|
|
54
|
+
"./store/adapters/indexed-db": "./src/store/adapters/indexed-db.ts",
|
|
55
|
+
"./store/provider": "./src/store/provider.ts",
|
|
56
|
+
"./facades/store": "./src/support/facades/store.ts",
|
|
57
|
+
"./filesystem/filesystem": "./src/filesystem/filesystem.ts",
|
|
58
|
+
"./filesystem/local-adapter": "./src/filesystem/adapters/local.ts",
|
|
59
|
+
"./renderers/contract": "./src/renderers/contract.ts",
|
|
60
|
+
"./renderers/react": "./src/renderers/adapters/react.tsx",
|
|
61
|
+
"./renderers/solid": "./src/renderers/adapters/solid.tsx",
|
|
62
|
+
"./renderers/svelte": "./src/renderers/adapters/svelte.ts",
|
|
63
|
+
"./renderers/vue": "./src/renderers/adapters/vue.ts",
|
|
64
|
+
"./support/facades": "./src/support/facades/index.ts",
|
|
65
|
+
"./facades/config": "./src/support/facades/config.ts",
|
|
66
|
+
"./runtimes/react": "./src/runtimes/react.tsx",
|
|
67
|
+
"./runtimes/solid": "./src/runtimes/solid.tsx",
|
|
68
|
+
"./runtimes/svelte": "./src/runtimes/svelte.ts",
|
|
69
|
+
"./runtimes/vue": "./src/runtimes/vue.ts",
|
|
70
|
+
"./str": "./src/support/str.ts",
|
|
71
|
+
"./arr": "./src/support/arr.ts",
|
|
72
|
+
"./collection": "./src/support/collection.ts",
|
|
73
|
+
"./lazy-collection": "./src/support/lazy-collection.ts",
|
|
74
|
+
"./fluent": "./src/support/fluent.ts",
|
|
75
|
+
"./num": "./src/support/num.ts",
|
|
76
|
+
"./pipeline": "./src/support/pipeline.ts",
|
|
77
|
+
"./service-provider": "./src/support/service-provider.ts",
|
|
78
|
+
"./manager": "./src/support/manager.ts",
|
|
79
|
+
"./facade": "./src/support/facades/facade.ts"
|
|
80
|
+
},
|
|
81
|
+
"peerDependencies": {
|
|
82
|
+
"react": "^19.0.0",
|
|
83
|
+
"react-dom": "^19.0.0",
|
|
84
|
+
"reflect-metadata": "^0.2.2",
|
|
85
|
+
"solid-js": "^1.9.0",
|
|
86
|
+
"svelte": "",
|
|
87
|
+
"vue": "^3.0.0"
|
|
88
|
+
},
|
|
89
|
+
"peerDependenciesMeta": {
|
|
90
|
+
"react": {
|
|
91
|
+
"optional": true
|
|
92
|
+
},
|
|
93
|
+
"react-dom": {
|
|
94
|
+
"optional": true
|
|
95
|
+
},
|
|
96
|
+
"solid-js": {
|
|
97
|
+
"optional": true
|
|
98
|
+
},
|
|
99
|
+
"svelte": {
|
|
100
|
+
"optional": true
|
|
101
|
+
},
|
|
102
|
+
"vue": {
|
|
103
|
+
"optional": true
|
|
104
|
+
},
|
|
105
|
+
"reflect-metadata": {
|
|
106
|
+
"optional": false
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"devDependencies": {
|
|
110
|
+
"@types/react": "^19.2.14",
|
|
111
|
+
"@types/react-dom": "^19.2.3",
|
|
112
|
+
"bun-types": "^1.3.13",
|
|
113
|
+
"fallow": "^2.67.0",
|
|
114
|
+
"husky": "^9.1.7",
|
|
115
|
+
"solid-js": "^1.9.12",
|
|
116
|
+
"vue": "^3.5.34"
|
|
117
|
+
},
|
|
118
|
+
"trustedDependencies": [
|
|
119
|
+
"@fallow-cli/fallow-cov"
|
|
120
|
+
]
|
|
121
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Adapter } from "../store/adapters/contract"
|
|
2
|
+
import type { Contract } from "./contract"
|
|
3
|
+
|
|
4
|
+
type Envelope<T> = { v: T; e: number | null }
|
|
5
|
+
|
|
6
|
+
export class Cache implements Contract {
|
|
7
|
+
constructor(protected readonly adapter: Adapter) {}
|
|
8
|
+
|
|
9
|
+
async get(key: string): Promise<unknown>
|
|
10
|
+
async get<T>(key: string): Promise<T | null>
|
|
11
|
+
async get<T = unknown>(key: string): Promise<T | null> {
|
|
12
|
+
const raw = await this.adapter.get<Envelope<T>>(key)
|
|
13
|
+
if (raw === null) return null
|
|
14
|
+
if (raw.e !== null && Date.now() >= raw.e) return null
|
|
15
|
+
return raw.v
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async set<T = unknown>(key: string, value: T, ttl?: number | null): Promise<void> {
|
|
19
|
+
const e = typeof ttl === "number" ? Date.now() + ttl * 1000 : null
|
|
20
|
+
return this.adapter.set<Envelope<T>>(key, { v: value, e })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async has(key: string): Promise<boolean> {
|
|
24
|
+
return (await this.get(key)) !== null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async delete(key: string): Promise<void> {
|
|
28
|
+
return this.adapter.delete(key)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async clear(): Promise<void> {
|
|
32
|
+
return this.adapter.clear()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async keys(): Promise<string[]> {
|
|
36
|
+
const allKeys = await this.adapter.keys()
|
|
37
|
+
const now = Date.now()
|
|
38
|
+
const results = await Promise.all(
|
|
39
|
+
allKeys.map(async (k) => {
|
|
40
|
+
const raw = await this.adapter.get<Envelope<unknown>>(k)
|
|
41
|
+
return raw !== null && (raw.e === null || now < raw.e) ? k : null
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
return results.filter((k): k is string => k !== null)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface Contract {
|
|
2
|
+
get(key: string): Promise<unknown>
|
|
3
|
+
get<T>(key: string): Promise<T | null>
|
|
4
|
+
set<T = unknown>(key: string, value: T, ttl?: number | null): Promise<void>
|
|
5
|
+
has(key: string): Promise<boolean>
|
|
6
|
+
delete(key: string): Promise<void>
|
|
7
|
+
clear(): Promise<void>
|
|
8
|
+
keys(): Promise<string[]>
|
|
9
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type ConfigRepository from "../config/repository"
|
|
2
|
+
import type { Adapter } from "../store/adapters/contract"
|
|
3
|
+
import IndexedDbAdapter from "../store/adapters/indexed-db"
|
|
4
|
+
import LocalStorageAdapter from "../store/adapters/local-storage"
|
|
5
|
+
import MemoryStoreAdapter from "../store/adapters/memory"
|
|
6
|
+
import Manager from "../support/manager"
|
|
7
|
+
import { Cache } from "./cache"
|
|
8
|
+
import type { Contract } from "./contract"
|
|
9
|
+
|
|
10
|
+
type StoreConfig = {
|
|
11
|
+
driver?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default class CacheManager extends Manager<Contract, Adapter> implements Adapter {
|
|
19
|
+
protected createDriver(raw: Adapter): Contract {
|
|
20
|
+
return new Cache(raw)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
protected defaultDriverName(): string {
|
|
24
|
+
return "memory" in this.drivers ? "memory" : super.defaultDriverName()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
protected driverType(): string {
|
|
28
|
+
return "Cache store"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static fromConfig(config: ConfigRepository): CacheManager {
|
|
32
|
+
const stores = CacheManager.storesFromConfig(config)
|
|
33
|
+
const active = config.get<string>("cache.default", "memory")
|
|
34
|
+
|
|
35
|
+
return new CacheManager(stores, active, config)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
protected static storesFromConfig(config: ConfigRepository): Record<string, Contract> {
|
|
39
|
+
const baseDrivers = CacheManager.defaultDrivers()
|
|
40
|
+
const configured = config.get<Record<string, unknown>>("cache.stores", {}) ?? {}
|
|
41
|
+
if (hasNoConfiguredStores(configured)) {
|
|
42
|
+
return baseDrivers
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return Object.fromEntries(
|
|
46
|
+
Object.entries(configured).map(([name, storeConfig]) => {
|
|
47
|
+
const driver = resolveDriver(storeConfig, name)
|
|
48
|
+
return [name, baseDrivers[driver] ?? baseDrivers.memory]
|
|
49
|
+
}),
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
protected static defaultDrivers(): Record<string, Contract> {
|
|
54
|
+
const hasWindow = typeof window !== "undefined"
|
|
55
|
+
const hasLocal = hasWindow && typeof window.localStorage !== "undefined"
|
|
56
|
+
const hasIndexed = typeof globalThis.indexedDB !== "undefined"
|
|
57
|
+
|
|
58
|
+
const rawMemory = new MemoryStoreAdapter()
|
|
59
|
+
const memory = new Cache(rawMemory)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
memory,
|
|
63
|
+
local: new Cache(hasLocal ? new LocalStorageAdapter(window.localStorage) : rawMemory),
|
|
64
|
+
indexed: new Cache(hasIndexed ? new IndexedDbAdapter() : rawMemory),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
store(name?: string): Contract {
|
|
69
|
+
return this.resolve(name ?? this.active)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get(key: string): Promise<unknown>
|
|
73
|
+
get<T>(key: string): Promise<T | null>
|
|
74
|
+
get<T = unknown>(key: string): Promise<T | null> {
|
|
75
|
+
return this.store().get<T>(key)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
set<T = unknown>(key: string, value: T, ttl?: number | null): Promise<void> {
|
|
79
|
+
return this.store().set<T>(key, value, ttl)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
has(key: string): Promise<boolean> {
|
|
83
|
+
return this.store().has(key)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
delete(key: string): Promise<void> {
|
|
87
|
+
return this.store().delete(key)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
clear(): Promise<void> {
|
|
91
|
+
return this.store().clear()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
keys(): Promise<string[]> {
|
|
95
|
+
return this.store().keys()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function hasNoConfiguredStores(value: unknown): boolean {
|
|
100
|
+
return !isRecord(value) || Object.keys(value).length === 0
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveDriver(value: unknown, fallback: string): string {
|
|
104
|
+
if (!isRecord(value)) {
|
|
105
|
+
return fallback
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const storeConfig = value as StoreConfig
|
|
109
|
+
return typeof storeConfig.driver === "string" ? storeConfig.driver : fallback
|
|
110
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type ConfigRepository from "../config/repository"
|
|
2
|
+
import type { ServiceProviderContext } from "../support/service-provider"
|
|
3
|
+
import ServiceProvider from "../support/service-provider"
|
|
4
|
+
import CacheManager from "./manager"
|
|
5
|
+
|
|
6
|
+
export class CacheProvider extends ServiceProvider {
|
|
7
|
+
register({ container }: ServiceProviderContext) {
|
|
8
|
+
container.singleton("cache", (c) => CacheManager.fromConfig(c.make<ConfigRepository>("config")))
|
|
9
|
+
container.singleton(CacheManager, (c) => c.make<CacheManager>("cache"))
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration contract defining the interface for configuration management
|
|
3
|
+
*/
|
|
4
|
+
export interface Contract {
|
|
5
|
+
/**
|
|
6
|
+
* Determine if a configuration key exists
|
|
7
|
+
*/
|
|
8
|
+
has(key: string | string[]): boolean
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get a configuration value by key
|
|
12
|
+
*/
|
|
13
|
+
get(key: string): unknown
|
|
14
|
+
get<T>(key: string): T | null
|
|
15
|
+
get<T>(key: string, defaultValue: T | (() => T)): T
|
|
16
|
+
get<T>(key: string, defaultValue: T | (() => T) | null): T | null
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get multiple configuration values
|
|
20
|
+
*/
|
|
21
|
+
getMany(keys: string[] | Record<string, unknown>): Record<string, unknown>
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Set a configuration value
|
|
25
|
+
*/
|
|
26
|
+
set(key: string | Record<string, unknown>, value?: unknown): void
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Prepend a value to an array configuration
|
|
30
|
+
*/
|
|
31
|
+
prepend(key: string, value: unknown): void
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Append a value to an array configuration
|
|
35
|
+
*/
|
|
36
|
+
push(key: string, value: unknown): void
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get all configuration items
|
|
40
|
+
*/
|
|
41
|
+
all(): Record<string, unknown>
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if a configuration key exists (offset-style)
|
|
45
|
+
*/
|
|
46
|
+
offsetExists(key: string): boolean
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get a configuration value by offset (offset-style)
|
|
50
|
+
*/
|
|
51
|
+
offsetGet(key: string): unknown
|
|
52
|
+
offsetGet<T>(key: string): T | null
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Set a configuration value by offset (offset-style)
|
|
56
|
+
*/
|
|
57
|
+
offsetSet(key: string, value: unknown): void
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Remove a configuration key (offset-style)
|
|
61
|
+
*/
|
|
62
|
+
offsetUnset(key: string): void
|
|
63
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ConfigItems } from "./repository"
|
|
2
|
+
import ConfigRepository from "./repository"
|
|
3
|
+
|
|
4
|
+
type GlobLoader = (pattern: string | string[], options?: { eager?: boolean }) => Record<string, unknown>
|
|
5
|
+
|
|
6
|
+
type ConfigModule = { default?: unknown }
|
|
7
|
+
|
|
8
|
+
export interface ConfigLoader {
|
|
9
|
+
load(basePath: string, staticItems?: ConfigItems): ConfigItems
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function fileNameWithoutExtension(path: string): string {
|
|
13
|
+
const file = path.split("/").pop() ?? path
|
|
14
|
+
return file.replace(/\.[^/.]+$/, "")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function loadConfigFromModules(modules: Record<string, unknown>): ConfigItems {
|
|
18
|
+
const discovered: ConfigItems = {}
|
|
19
|
+
|
|
20
|
+
for (const [path, loaded] of Object.entries(modules)) {
|
|
21
|
+
const key = fileNameWithoutExtension(path)
|
|
22
|
+
const value = readConfigModuleDefault(loaded)
|
|
23
|
+
if (key !== "index" && value !== undefined) {
|
|
24
|
+
discovered[key] = value
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return discovered
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readConfigModuleDefault(loaded: unknown): unknown {
|
|
32
|
+
const module = loaded as ConfigModule
|
|
33
|
+
return module && "default" in module ? module.default : undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function cloneItems(items: ConfigItems): ConfigItems {
|
|
37
|
+
if (typeof structuredClone === "function") {
|
|
38
|
+
return structuredClone(items) as ConfigItems
|
|
39
|
+
}
|
|
40
|
+
return JSON.parse(JSON.stringify(items)) as ConfigItems
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function loadEsmConfigModules(basePath: string): Record<string, unknown> {
|
|
44
|
+
const glob = resolveConfigGlob()
|
|
45
|
+
if (!glob) {
|
|
46
|
+
return {}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const modules = glob(configPatternForBasePath(basePath), { eager: true })
|
|
50
|
+
return modules as Record<string, unknown>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveConfigGlob(): GlobLoader | null {
|
|
54
|
+
const testGlob = (
|
|
55
|
+
globalThis as {
|
|
56
|
+
__iocConfigGlobForTests?: GlobLoader
|
|
57
|
+
}
|
|
58
|
+
).__iocConfigGlobForTests
|
|
59
|
+
|
|
60
|
+
const viteGlob = (
|
|
61
|
+
import.meta as ImportMeta & {
|
|
62
|
+
glob?: GlobLoader
|
|
63
|
+
}
|
|
64
|
+
).glob
|
|
65
|
+
|
|
66
|
+
return typeof testGlob === "function" ? testGlob : typeof viteGlob === "function" ? viteGlob : null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function configPatternForBasePath(basePath: string): string {
|
|
70
|
+
return basePath.endsWith("/") ? `${basePath}config/**/*.ts` : `${basePath}/config/**/*.ts`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class EsmConfigLoader implements ConfigLoader {
|
|
74
|
+
load(basePath: string, staticItems: ConfigItems = {}): ConfigItems {
|
|
75
|
+
const shouldLoadEsm = Object.keys(staticItems).length === 0
|
|
76
|
+
|
|
77
|
+
const esmItems = shouldLoadEsm ? loadConfigFromModules(loadEsmConfigModules(basePath)) : {}
|
|
78
|
+
|
|
79
|
+
return { ...esmItems, ...cloneItems(staticItems) }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function createConfigRepository(
|
|
84
|
+
basePath: string,
|
|
85
|
+
staticItems: ConfigItems = {},
|
|
86
|
+
loader?: ConfigLoader,
|
|
87
|
+
): ConfigRepository {
|
|
88
|
+
const configLoader = loader ?? new EsmConfigLoader()
|
|
89
|
+
const items = configLoader.load(basePath, staticItems)
|
|
90
|
+
return new ConfigRepository(items)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const ConfigFactory = {
|
|
94
|
+
create: createConfigRepository,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createConfig(basePath: string, staticItems: ConfigItems = {}): ConfigRepository {
|
|
98
|
+
return ConfigFactory.create(basePath, staticItems)
|
|
99
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type EnvValue = string | number | boolean | null
|
|
2
|
+
type EnvMap = Record<string, EnvValue | undefined>
|
|
3
|
+
const normalizedEnvValues: Record<string, EnvValue> = {
|
|
4
|
+
"(empty)": "",
|
|
5
|
+
"(false)": false,
|
|
6
|
+
"(null)": null,
|
|
7
|
+
"(true)": true,
|
|
8
|
+
empty: "",
|
|
9
|
+
false: false,
|
|
10
|
+
null: null,
|
|
11
|
+
true: true,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeEnvValue(value: EnvValue | undefined): EnvValue | undefined {
|
|
15
|
+
if (typeof value !== "string") {
|
|
16
|
+
return value
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const normalized = value.trim().toLowerCase()
|
|
20
|
+
return normalized in normalizedEnvValues ? normalizedEnvValues[normalized] : value
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveProcessEnv(): EnvMap {
|
|
24
|
+
const processValue = (globalThis as { process?: { env?: EnvMap } }).process
|
|
25
|
+
|
|
26
|
+
if (!processValue?.env) {
|
|
27
|
+
return {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return processValue.env
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveImportMetaEnv(): EnvMap {
|
|
34
|
+
const testEnv = (
|
|
35
|
+
globalThis as {
|
|
36
|
+
__iocImportMetaEnvForTests?: EnvMap
|
|
37
|
+
}
|
|
38
|
+
).__iocImportMetaEnvForTests
|
|
39
|
+
if (testEnv) {
|
|
40
|
+
return testEnv
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const meta = import.meta as ImportMeta & { env?: EnvMap }
|
|
44
|
+
return meta.env ?? {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function env(key: string): EnvValue | undefined
|
|
48
|
+
export function env<T>(key: string, defaultValue: T): EnvValue | T
|
|
49
|
+
export function env<T>(key: string, defaultValue?: T): EnvValue | T | undefined {
|
|
50
|
+
const importMetaEnv = resolveImportMetaEnv()
|
|
51
|
+
if (key in importMetaEnv) {
|
|
52
|
+
return normalizeEnvValue(importMetaEnv[key])
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const processEnv = resolveProcessEnv()
|
|
56
|
+
if (key in processEnv) {
|
|
57
|
+
return normalizeEnvValue(processEnv[key])
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return defaultValue
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function registerGlobalEnv(): void {
|
|
64
|
+
const globalScope = globalThis as { env?: typeof env }
|
|
65
|
+
if (typeof globalScope.env !== "function") {
|
|
66
|
+
globalScope.env = env
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ServiceProviderContext } from "../support/service-provider"
|
|
2
|
+
import ServiceProvider from "../support/service-provider"
|
|
3
|
+
import ConfigRepository from "./repository"
|
|
4
|
+
|
|
5
|
+
export class ConfigProvider extends ServiceProvider {
|
|
6
|
+
protected readonly repository: ConfigRepository
|
|
7
|
+
|
|
8
|
+
constructor(repository: ConfigRepository) {
|
|
9
|
+
super()
|
|
10
|
+
this.repository = repository
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
register({ container }: ServiceProviderContext) {
|
|
14
|
+
container.instance("config", this.repository)
|
|
15
|
+
container.instance(ConfigRepository, this.repository)
|
|
16
|
+
}
|
|
17
|
+
}
|