@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/README.md
ADDED
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
# @raubjo/architect
|
|
2
|
+
|
|
3
|
+
`@raubjo/architect` is a Laravel-inspired application container for frontend applications.
|
|
4
|
+
|
|
5
|
+
It gives you a predictable boot process, a shared dependency container, service providers, framework-specific runtime helpers, and small configuration/storage/cache primitives that can be used consistently across React, Solid, Svelte, and Vue projects.
|
|
6
|
+
|
|
7
|
+
The package is intentionally small. It does not try to replace your framework state tools. Instead, it gives you a structured place to register services and application infrastructure, then resolve those services from components and startup code.
|
|
8
|
+
|
|
9
|
+
The current runtime is browser-oriented. `Application.run()` expects `window`, and renderer adapters expect a DOM mount node.
|
|
10
|
+
|
|
11
|
+
## Table of contents
|
|
12
|
+
|
|
13
|
+
- [Why this package exists](#why-this-package-exists)
|
|
14
|
+
- [What it includes](#what-it-includes)
|
|
15
|
+
- [Installation](#installation)
|
|
16
|
+
- [Quick start](#quick-start)
|
|
17
|
+
- [Core mental model](#core-mental-model)
|
|
18
|
+
- [Application lifecycle](#application-lifecycle)
|
|
19
|
+
- [Container adapters](#container-adapters)
|
|
20
|
+
- [Working with service providers](#working-with-service-providers)
|
|
21
|
+
- [Framework integrations](#framework-integrations)
|
|
22
|
+
- [Configuration](#configuration)
|
|
23
|
+
- [Cache and storage](#cache-and-storage)
|
|
24
|
+
- [Facades](#facades)
|
|
25
|
+
- [Low-level utilities](#low-level-utilities)
|
|
26
|
+
- [Public exports](#public-exports)
|
|
27
|
+
- [Examples](#examples)
|
|
28
|
+
- [Development](#development)
|
|
29
|
+
- [Notes and constraints](#notes-and-constraints)
|
|
30
|
+
|
|
31
|
+
## Why this package exists
|
|
32
|
+
|
|
33
|
+
In small frontend applications, service wiring usually starts in components, top-level files, or ad hoc singleton modules. That works until:
|
|
34
|
+
|
|
35
|
+
- the same dependencies need to be created in multiple places
|
|
36
|
+
- startup and teardown logic become hard to reason about
|
|
37
|
+
- configuration is scattered across modules
|
|
38
|
+
- services need to be shared across framework components and non-UI code
|
|
39
|
+
- you want clearer separation between business logic and rendering
|
|
40
|
+
|
|
41
|
+
`@raubjo/architect` solves that by giving you:
|
|
42
|
+
|
|
43
|
+
- a central application object
|
|
44
|
+
- a container abstraction
|
|
45
|
+
- provider-based registration and boot hooks
|
|
46
|
+
- consistent setup and cleanup ordering
|
|
47
|
+
- runtime-specific `useService(...)` helpers
|
|
48
|
+
- optional facade-style access for config, cache, and storage
|
|
49
|
+
|
|
50
|
+
If you are familiar with Laravel service providers and container bindings, the design will feel familiar.
|
|
51
|
+
|
|
52
|
+
## What it includes
|
|
53
|
+
|
|
54
|
+
The current codebase provides:
|
|
55
|
+
|
|
56
|
+
- `Application` orchestration with `register -> services -> boot -> startup -> render`
|
|
57
|
+
- a built-in dependency injection container
|
|
58
|
+
- an Inversify adapter behind the same container contract
|
|
59
|
+
- service providers with cleanup support
|
|
60
|
+
- runtime helpers for React, Solid, Svelte, and Vue
|
|
61
|
+
- renderer adapters for React, Solid, Svelte, and Vue
|
|
62
|
+
- `ConfigRepository` with dot-notation lookups and typed getters
|
|
63
|
+
- `StorageManager` with memory, local storage, and IndexedDB-backed adapters
|
|
64
|
+
- `CacheManager` built on the same adapter contract as storage
|
|
65
|
+
- facades for config, cache, and storage
|
|
66
|
+
- small utility exports such as `env`, `Str`, and filesystem/config helpers
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
bun add @raubjo/architect reflect-metadata
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Framework integrations are imported from subpaths:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { ContextProvider, useService } from "@raubjo/architect/react";
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Optional peer dependencies depend on the runtime you use:
|
|
81
|
+
|
|
82
|
+
- `react` and `react-dom` for React
|
|
83
|
+
- `solid-js` for Solid
|
|
84
|
+
- `svelte` for Svelte
|
|
85
|
+
- `vue` for Vue
|
|
86
|
+
- `inversify` if you want the Inversify container adapter
|
|
87
|
+
|
|
88
|
+
If you use decorator-based constructor injection with the built-in container, or Inversify, load `reflect-metadata` before resolving those classes:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import "reflect-metadata";
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Quick start
|
|
95
|
+
|
|
96
|
+
This is the smallest useful application shape:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import "reflect-metadata";
|
|
100
|
+
import { Application, ServiceProvider, type ServiceProviderContext } from "@raubjo/architect";
|
|
101
|
+
import { ContextProvider, useService } from "@raubjo/architect/react";
|
|
102
|
+
import ReactDOM from "react-dom/client";
|
|
103
|
+
import { createElement } from "react";
|
|
104
|
+
|
|
105
|
+
class CounterService {
|
|
106
|
+
protected count = 0;
|
|
107
|
+
|
|
108
|
+
increment(): number {
|
|
109
|
+
this.count += 1;
|
|
110
|
+
return this.count;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
current(): number {
|
|
114
|
+
return this.count;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class CounterProvider extends ServiceProvider {
|
|
119
|
+
register({ container }: ServiceProviderContext): void {
|
|
120
|
+
container.singleton(CounterService, CounterService);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function App() {
|
|
125
|
+
const counter = useService(CounterService);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<button
|
|
129
|
+
onClick={() => {
|
|
130
|
+
counter.increment();
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
Count: {counter.current()}
|
|
134
|
+
</button>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const application = Application.configure({
|
|
139
|
+
container: { adapter: "builtin" },
|
|
140
|
+
config: {
|
|
141
|
+
app: { name: "Architect Example" },
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
.withProviders([new CounterProvider()]);
|
|
145
|
+
|
|
146
|
+
const root = ReactDOM.createRoot(document.getElementById("root")!);
|
|
147
|
+
root.render(createElement(ContextProvider, { application }, createElement(App)));
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`Application.run()` returns:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
{
|
|
154
|
+
container,
|
|
155
|
+
stop,
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The returned `container` is the active runtime container. `stop()` runs cleanup callbacks in reverse order, clears facade caches, and flushes the container.
|
|
160
|
+
|
|
161
|
+
## Core mental model
|
|
162
|
+
|
|
163
|
+
The package is built around a few simple ideas:
|
|
164
|
+
|
|
165
|
+
### 1. The application owns startup
|
|
166
|
+
|
|
167
|
+
`Application.configure(...).run()` is the entry point. It creates the container, registers built-in services, runs provider hooks, runs startup callbacks, and mounts a renderer when one is configured.
|
|
168
|
+
|
|
169
|
+
### 2. Providers own wiring
|
|
170
|
+
|
|
171
|
+
Register application services once in providers instead of rebuilding them inside components.
|
|
172
|
+
|
|
173
|
+
### 3. Components resolve services, not construction rules
|
|
174
|
+
|
|
175
|
+
Framework code should usually call `useService(...)` or consume already-registered abstractions. Construction details stay in providers and service registrars.
|
|
176
|
+
|
|
177
|
+
### 4. Reactivity is your service’s responsibility
|
|
178
|
+
|
|
179
|
+
`useService(...)` resolves an object from the container. It does not automatically subscribe a component to changes inside that object.
|
|
180
|
+
|
|
181
|
+
If a service exposes mutable state, you still need a framework-appropriate reactive mechanism such as:
|
|
182
|
+
|
|
183
|
+
- component state
|
|
184
|
+
- signals
|
|
185
|
+
- framework stores
|
|
186
|
+
- subscriptions
|
|
187
|
+
- Zustand or another state library
|
|
188
|
+
|
|
189
|
+
The examples in this repository show both simple imperative services and services that expose reactive state mechanisms.
|
|
190
|
+
|
|
191
|
+
## Application lifecycle
|
|
192
|
+
|
|
193
|
+
`Application.run()` executes in this order:
|
|
194
|
+
|
|
195
|
+
1. provider `register()`
|
|
196
|
+
2. `.withServices(...)` callbacks
|
|
197
|
+
3. provider `boot()`
|
|
198
|
+
4. `.withStartup(...)` callbacks
|
|
199
|
+
|
|
200
|
+
Cleanup runs in reverse order when `stop()` is called:
|
|
201
|
+
|
|
202
|
+
1. startup cleanup
|
|
203
|
+
2. provider `boot()` cleanup
|
|
204
|
+
3. service registrar cleanup
|
|
205
|
+
4. provider `register()` cleanup
|
|
206
|
+
|
|
207
|
+
The application also registers a `beforeunload` listener and calls `stop()` once when the page unloads.
|
|
208
|
+
|
|
209
|
+
### Built-in services registered by `Application`
|
|
210
|
+
|
|
211
|
+
Every application instance automatically registers:
|
|
212
|
+
|
|
213
|
+
- `"config"` and `ConfigRepository`
|
|
214
|
+
- `"storage"` and `StorageManager`
|
|
215
|
+
- `"cache"` and `CacheManager`
|
|
216
|
+
|
|
217
|
+
That means you can resolve them by string identifier or by class identifier.
|
|
218
|
+
|
|
219
|
+
### Application builder methods
|
|
220
|
+
|
|
221
|
+
The main fluent API is:
|
|
222
|
+
|
|
223
|
+
- `Application.configure(basePath?: string)`
|
|
224
|
+
- `Application.configure(options?: { basePath?: string; container?: ...; config?: ... })`
|
|
225
|
+
- `.withProviders(providers)`
|
|
226
|
+
- `.withServices(registerServices)`
|
|
227
|
+
- `.withStartup(startupHandler)`
|
|
228
|
+
- `.run()`
|
|
229
|
+
|
|
230
|
+
Static resolution is also available:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
const config = Application.make("config");
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
That only works after `run()`. Calling `Application.make(...)` before the app starts throws.
|
|
237
|
+
|
|
238
|
+
## Container adapters
|
|
239
|
+
|
|
240
|
+
The package exposes a common `ContainerContract` and currently ships with two concrete adapters:
|
|
241
|
+
|
|
242
|
+
- built-in container
|
|
243
|
+
- Inversify container
|
|
244
|
+
|
|
245
|
+
### Selecting an adapter
|
|
246
|
+
|
|
247
|
+
Container runtime options look like this:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
{
|
|
251
|
+
adapter?: "auto" | "builtin" | "inversify";
|
|
252
|
+
factory?: (() => ContainerContract) | null;
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Resolution rules are:
|
|
257
|
+
|
|
258
|
+
- if `factory` is provided, it wins
|
|
259
|
+
- if `adapter` is `"builtin"`, the built-in container is used
|
|
260
|
+
- if `adapter` is `"inversify"`, the runtime expects an Inversify factory to be registered
|
|
261
|
+
- if `adapter` is `"auto"`, the runtime prefers Inversify when it detects an `inversify` dependency and a factory is available; otherwise it falls back to the built-in container
|
|
262
|
+
|
|
263
|
+
### Built-in container
|
|
264
|
+
|
|
265
|
+
The built-in container supports:
|
|
266
|
+
|
|
267
|
+
- `bind(...).to(...)`
|
|
268
|
+
- `bind(...).toConstantValue(...)`
|
|
269
|
+
- `singleton(identifier, concrete)`
|
|
270
|
+
- `bind(identifier, concrete)`
|
|
271
|
+
- `instance(identifier, value)`
|
|
272
|
+
- `make(...)` and `get(...)`
|
|
273
|
+
- `bound(...)` and `has(...)`
|
|
274
|
+
- `unbind(...)`, `unbindAll()`, and `flush()`
|
|
275
|
+
|
|
276
|
+
Constructor injection is supported through `design:paramtypes` metadata. Parameter-level token overrides are supported through the exported `injectDependency(...)` decorator.
|
|
277
|
+
|
|
278
|
+
Example:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import "reflect-metadata";
|
|
282
|
+
import {
|
|
283
|
+
Application,
|
|
284
|
+
injectDependency,
|
|
285
|
+
} from "@raubjo/architect";
|
|
286
|
+
|
|
287
|
+
class Logger {
|
|
288
|
+
log(message: string) {
|
|
289
|
+
console.log(message);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
class Greeter {
|
|
294
|
+
constructor(
|
|
295
|
+
public readonly logger: Logger,
|
|
296
|
+
@injectDependency("app.name") public readonly appName: string,
|
|
297
|
+
) {}
|
|
298
|
+
|
|
299
|
+
greet() {
|
|
300
|
+
this.logger.log(`Hello from ${this.appName}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const { container } = Application.configure({
|
|
305
|
+
container: { adapter: "builtin" },
|
|
306
|
+
})
|
|
307
|
+
.withServices(({ container }) => {
|
|
308
|
+
container.singleton(Logger, Logger);
|
|
309
|
+
container.instance("app.name", "Architect");
|
|
310
|
+
container.bind(Greeter, Greeter);
|
|
311
|
+
})
|
|
312
|
+
.run();
|
|
313
|
+
|
|
314
|
+
container.make(Greeter).greet();
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Important built-in container behaviors:
|
|
318
|
+
|
|
319
|
+
- string, symbol, and class identifiers are supported
|
|
320
|
+
- unbound class identifiers can still be resolved directly
|
|
321
|
+
- circular constructor dependencies throw
|
|
322
|
+
- missing constructor metadata for a parameter throws
|
|
323
|
+
- singletons are cached after first resolution
|
|
324
|
+
- transient factories/classes resolve anew each time
|
|
325
|
+
|
|
326
|
+
### Inversify adapter
|
|
327
|
+
|
|
328
|
+
If you want to use Inversify, install `inversify` and register an adapter factory on `globalThis.__iocContainerFactoryRegistry`:
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
import "reflect-metadata";
|
|
332
|
+
import { Application } from "@raubjo/architect";
|
|
333
|
+
import InversifyContainer from "@raubjo/architect/container/adapters/inversify";
|
|
334
|
+
|
|
335
|
+
globalThis.__iocContainerFactoryRegistry = {
|
|
336
|
+
inversify: () => new InversifyContainer(),
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
Application.configure({
|
|
340
|
+
container: { adapter: "inversify" },
|
|
341
|
+
}).run();
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
If you explicitly request `adapter: "inversify"` and no factory is registered, the runtime throws a configuration error.
|
|
345
|
+
|
|
346
|
+
## Working with service providers
|
|
347
|
+
|
|
348
|
+
Providers are the main extension point for application wiring.
|
|
349
|
+
|
|
350
|
+
The base class is:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
class ServiceProvider {
|
|
354
|
+
register(context): void | Cleanup {}
|
|
355
|
+
boot(context): void | Cleanup {}
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Use `register()` for bindings and `boot()` for work that should happen after all providers and manual service registrations are complete.
|
|
360
|
+
|
|
361
|
+
Both methods may optionally return a cleanup callback.
|
|
362
|
+
|
|
363
|
+
`DeferrableServiceProvider` is also exported, but in the current implementation it is only a base class with a `provides()` method. The application runtime does not yet perform automatic deferred loading based on that contract.
|
|
364
|
+
|
|
365
|
+
Example:
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
import {
|
|
369
|
+
ServiceProvider,
|
|
370
|
+
type Cleanup,
|
|
371
|
+
type ServiceProviderContext,
|
|
372
|
+
} from "@raubjo/architect";
|
|
373
|
+
|
|
374
|
+
class HeartbeatService {
|
|
375
|
+
protected intervalId: number | null = null;
|
|
376
|
+
protected ticksValue = 0;
|
|
377
|
+
|
|
378
|
+
ticks() {
|
|
379
|
+
return this.ticksValue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
start(): Cleanup {
|
|
383
|
+
if (this.intervalId !== null) {
|
|
384
|
+
return () => this.stop();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
this.intervalId = window.setInterval(() => {
|
|
388
|
+
this.ticksValue += 1;
|
|
389
|
+
}, 1000);
|
|
390
|
+
|
|
391
|
+
return () => this.stop();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
stop() {
|
|
395
|
+
if (this.intervalId === null) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
window.clearInterval(this.intervalId);
|
|
400
|
+
this.intervalId = null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
class HeartbeatProvider extends ServiceProvider {
|
|
405
|
+
register({ container }: ServiceProviderContext): void {
|
|
406
|
+
container.singleton(HeartbeatService, HeartbeatService);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
boot({ container }: ServiceProviderContext): Cleanup {
|
|
410
|
+
return container.get(HeartbeatService).start();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
You can mix providers with `.withServices(...)` and `.withStartup(...)` when a full provider class would be excessive.
|
|
416
|
+
|
|
417
|
+
## Framework integrations
|
|
418
|
+
|
|
419
|
+
Each supported framework has:
|
|
420
|
+
|
|
421
|
+
- a runtime entrypoint with service resolution helpers
|
|
422
|
+
- a context/provider helper so you choose the context boundary
|
|
423
|
+
- an optional renderer adapter you can use directly if you want a turnkey mount
|
|
424
|
+
|
|
425
|
+
### React
|
|
426
|
+
|
|
427
|
+
Import from `@raubjo/architect/react`:
|
|
428
|
+
|
|
429
|
+
- `ContextProvider`
|
|
430
|
+
- `ApplicationProvider`
|
|
431
|
+
- `Renderer`
|
|
432
|
+
- `useService`
|
|
433
|
+
|
|
434
|
+
`ContextProvider` runs the application and provides the container through React context, so you can decide where the application boundary lives in your component tree.
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
import { useService } from "@raubjo/architect/react";
|
|
438
|
+
|
|
439
|
+
function App() {
|
|
440
|
+
const service = useService(MyService);
|
|
441
|
+
return <div>{service.value()}</div>;
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Solid
|
|
446
|
+
|
|
447
|
+
Import from `@raubjo/architect/solid`:
|
|
448
|
+
|
|
449
|
+
- `ContextProvider`
|
|
450
|
+
- `ApplicationProvider`
|
|
451
|
+
- `Renderer`
|
|
452
|
+
- `useService`
|
|
453
|
+
|
|
454
|
+
`ContextProvider` runs the application and provides the container through Solid context.
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
import { useService } from "@raubjo/architect/solid";
|
|
458
|
+
|
|
459
|
+
function App() {
|
|
460
|
+
const service = useService(MyService);
|
|
461
|
+
return <div>{service.value()}</div>;
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Svelte
|
|
466
|
+
|
|
467
|
+
Import from `@raubjo/architect/svelte`:
|
|
468
|
+
|
|
469
|
+
- `Renderer`
|
|
470
|
+
- `containerKey`
|
|
471
|
+
- `provideContainer` (alias: `ContextProvider`)
|
|
472
|
+
- `useService`
|
|
473
|
+
|
|
474
|
+
The Svelte renderer passes `container` as a prop to the root component. Inside the component, call `provideContainer(container)` (or `ContextProvider(container)`) before using `useService(...)`.
|
|
475
|
+
|
|
476
|
+
```svelte
|
|
477
|
+
<script lang="ts">
|
|
478
|
+
import { provideContainer, useService } from "@raubjo/architect/svelte";
|
|
479
|
+
import CounterService from "./counter/service";
|
|
480
|
+
|
|
481
|
+
export let container: unknown;
|
|
482
|
+
|
|
483
|
+
provideContainer(container as never);
|
|
484
|
+
const counter = useService(CounterService);
|
|
485
|
+
</script>
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Vue
|
|
489
|
+
|
|
490
|
+
Import from `@raubjo/architect/vue`:
|
|
491
|
+
|
|
492
|
+
- `ContextProvider`
|
|
493
|
+
- `Renderer`
|
|
494
|
+
- `useService`
|
|
495
|
+
|
|
496
|
+
`ContextProvider` runs the application, provides the container via Vue injection, and renders its slot content.
|
|
497
|
+
|
|
498
|
+
```vue
|
|
499
|
+
<script setup lang="ts">
|
|
500
|
+
import { useService } from "@raubjo/architect/vue";
|
|
501
|
+
|
|
502
|
+
const service = useService(MyService);
|
|
503
|
+
</script>
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Renderer behavior
|
|
507
|
+
|
|
508
|
+
All renderers:
|
|
509
|
+
|
|
510
|
+
- look up a DOM mount node by `rootElementId`
|
|
511
|
+
- throw if the mount node does not exist
|
|
512
|
+
- return a cleanup function that unmounts the rendered tree
|
|
513
|
+
|
|
514
|
+
Renderers are not coupled to `Application`. If you want turnkey mounting, call `app.run()` yourself, then pass the returned `container` into a renderer (or into a framework `ContextProvider`).
|
|
515
|
+
|
|
516
|
+
## Configuration
|
|
517
|
+
|
|
518
|
+
`Application` does not implicitly load config files during startup. The configuration source for an app instance is the object you pass into `Application.configure({ config })`.
|
|
519
|
+
|
|
520
|
+
Example:
|
|
521
|
+
|
|
522
|
+
```ts
|
|
523
|
+
const running = Application.configure({
|
|
524
|
+
config: {
|
|
525
|
+
app: {
|
|
526
|
+
name: "Architect",
|
|
527
|
+
timezone: "UTC",
|
|
528
|
+
},
|
|
529
|
+
storage: {
|
|
530
|
+
driver: "memory",
|
|
531
|
+
},
|
|
532
|
+
cache: {
|
|
533
|
+
default: "memory",
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
}).run();
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
That data is cloned per application instance before it is wrapped in `ConfigRepository`.
|
|
540
|
+
|
|
541
|
+
### `ConfigRepository`
|
|
542
|
+
|
|
543
|
+
`ConfigRepository` supports:
|
|
544
|
+
|
|
545
|
+
- dot-notation reads
|
|
546
|
+
- `has(...)`
|
|
547
|
+
- `get(...)`
|
|
548
|
+
- `getMany(...)`
|
|
549
|
+
- typed accessors: `string`, `integer`, `float`, `boolean`, `array`
|
|
550
|
+
- writes via `set(...)`
|
|
551
|
+
- array mutation helpers via `prepend(...)` and `push(...)`
|
|
552
|
+
- offset-style helpers like `offsetGet(...)` and `offsetUnset(...)`
|
|
553
|
+
|
|
554
|
+
Example:
|
|
555
|
+
|
|
556
|
+
```ts
|
|
557
|
+
import { ConfigRepository } from "@raubjo/architect";
|
|
558
|
+
|
|
559
|
+
const config = new ConfigRepository({
|
|
560
|
+
app: {
|
|
561
|
+
name: "Architect",
|
|
562
|
+
retries: 3,
|
|
563
|
+
tags: ["frontend"],
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
config.string("app.name"); // "Architect"
|
|
568
|
+
config.integer("app.retries"); // 3
|
|
569
|
+
config.push("app.tags", "ioc");
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### `env(...)`
|
|
573
|
+
|
|
574
|
+
The exported `env(...)` helper reads from:
|
|
575
|
+
|
|
576
|
+
1. `import.meta.env`
|
|
577
|
+
2. `process.env`
|
|
578
|
+
|
|
579
|
+
It normalizes common string forms:
|
|
580
|
+
|
|
581
|
+
- `"true"` and `"(true)"` -> `true`
|
|
582
|
+
- `"false"` and `"(false)"` -> `false`
|
|
583
|
+
- `"null"` and `"(null)"` -> `null`
|
|
584
|
+
- `"empty"` and `"(empty)"` -> `""`
|
|
585
|
+
|
|
586
|
+
It is also registered globally as `env` when the module is loaded.
|
|
587
|
+
|
|
588
|
+
## Cache and storage
|
|
589
|
+
|
|
590
|
+
The package ships with two manager abstractions:
|
|
591
|
+
|
|
592
|
+
- `StorageManager`
|
|
593
|
+
- `CacheManager`
|
|
594
|
+
|
|
595
|
+
Both use the same async adapter contract:
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
interface Adapter {
|
|
599
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
600
|
+
set<T = unknown>(key: string, value: T): Promise<void>;
|
|
601
|
+
has(key: string): Promise<boolean>;
|
|
602
|
+
delete(key: string): Promise<void>;
|
|
603
|
+
clear(): Promise<void>;
|
|
604
|
+
keys(): Promise<string[]>;
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### Storage
|
|
609
|
+
|
|
610
|
+
`StorageManager` reads its active driver from `storage.driver`.
|
|
611
|
+
|
|
612
|
+
Supported built-in drivers:
|
|
613
|
+
|
|
614
|
+
- `memory`
|
|
615
|
+
- `local`
|
|
616
|
+
- `indexed`
|
|
617
|
+
|
|
618
|
+
Default behavior:
|
|
619
|
+
|
|
620
|
+
- `memory` uses an in-memory `Map`
|
|
621
|
+
- `local` uses `window.localStorage` when available, otherwise falls back to memory
|
|
622
|
+
- `indexed` uses `indexedDB` when available, otherwise falls back to memory
|
|
623
|
+
|
|
624
|
+
Example:
|
|
625
|
+
|
|
626
|
+
```ts
|
|
627
|
+
const app = Application.configure({
|
|
628
|
+
config: {
|
|
629
|
+
storage: {
|
|
630
|
+
driver: "local",
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
}).run();
|
|
634
|
+
|
|
635
|
+
const storage = app.container.get("storage");
|
|
636
|
+
await storage.set("draft", { title: "Post" });
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
You can also switch drivers manually:
|
|
640
|
+
|
|
641
|
+
```ts
|
|
642
|
+
storage.use("memory");
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Cache
|
|
646
|
+
|
|
647
|
+
`CacheManager` is intentionally very similar, but it reads from a cache-specific configuration shape:
|
|
648
|
+
|
|
649
|
+
```ts
|
|
650
|
+
{
|
|
651
|
+
cache: {
|
|
652
|
+
default: "memory",
|
|
653
|
+
stores: {
|
|
654
|
+
memory: { driver: "memory" },
|
|
655
|
+
persistent: { driver: "indexed" },
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
}
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
`cache.default` selects the active store, and `cache.stores` maps store names to drivers.
|
|
662
|
+
|
|
663
|
+
If a configured store points to an unknown driver, the implementation falls back to the memory driver.
|
|
664
|
+
|
|
665
|
+
Example:
|
|
666
|
+
|
|
667
|
+
```ts
|
|
668
|
+
const cache = app.container.get("cache");
|
|
669
|
+
|
|
670
|
+
await cache.set("session", { ok: true });
|
|
671
|
+
await cache.get("session");
|
|
672
|
+
|
|
673
|
+
cache.use("persistent");
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### IndexedDB fallback behavior
|
|
677
|
+
|
|
678
|
+
The IndexedDB adapter is resilient by design:
|
|
679
|
+
|
|
680
|
+
- if IndexedDB is unavailable, it falls back to another adapter
|
|
681
|
+
- if database open or request operations fail, it falls back to the configured fallback adapter
|
|
682
|
+
- the default fallback is an in-memory adapter
|
|
683
|
+
|
|
684
|
+
That makes it safe to use in mixed browser or test environments without adding a separate compatibility layer.
|
|
685
|
+
|
|
686
|
+
## Facades
|
|
687
|
+
|
|
688
|
+
Facade classes provide a static access pattern similar to Laravel:
|
|
689
|
+
|
|
690
|
+
- `Config`
|
|
691
|
+
- `Cache`
|
|
692
|
+
- `Store`
|
|
693
|
+
|
|
694
|
+
Example:
|
|
695
|
+
|
|
696
|
+
```ts
|
|
697
|
+
import { Config, Cache, Store } from "@raubjo/architect";
|
|
698
|
+
|
|
699
|
+
const name = Config.get("app.name");
|
|
700
|
+
await Cache.set("token", "abc");
|
|
701
|
+
await Store.set("draft", { id: 1 });
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
Facade resolution works through `Application.make(...)`, and resolved instances are cached per facade accessor until the application is stopped or facade caches are cleared.
|
|
705
|
+
|
|
706
|
+
That means facades only work correctly after the application has started.
|
|
707
|
+
|
|
708
|
+
## Low-level utilities
|
|
709
|
+
|
|
710
|
+
The codebase also exports a few lower-level building blocks.
|
|
711
|
+
|
|
712
|
+
### `FileSystem` and local config loading
|
|
713
|
+
|
|
714
|
+
`FileSystem` is a small wrapper around a `FileSystemAdapter`. The bundled `LocalAdapter` can discover config modules from `config/*` and `src/config/*` using `import.meta.glob(...)`.
|
|
715
|
+
|
|
716
|
+
This is not used automatically by `Application` today, but it is available if you want explicit config-module loading in your own bootstrap code.
|
|
717
|
+
|
|
718
|
+
### `loadConfig(...)`
|
|
719
|
+
|
|
720
|
+
`@raubjo/architect/config/adapters/esm` exports `loadConfig(modules)`, which turns an ESM module map into a config object keyed by filename.
|
|
721
|
+
|
|
722
|
+
### `Str`
|
|
723
|
+
|
|
724
|
+
`Str` provides a few common string helpers:
|
|
725
|
+
|
|
726
|
+
- `lower`
|
|
727
|
+
- `upper`
|
|
728
|
+
- `length`
|
|
729
|
+
- `contains`
|
|
730
|
+
- `startsWith`
|
|
731
|
+
- `endsWith`
|
|
732
|
+
- `replace`
|
|
733
|
+
- `snake`
|
|
734
|
+
- `kebab`
|
|
735
|
+
- `studly`
|
|
736
|
+
- `camel`
|
|
737
|
+
- `slug`
|
|
738
|
+
|
|
739
|
+
It is also registered globally as `Str` when the package is loaded.
|
|
740
|
+
|
|
741
|
+
## Public exports
|
|
742
|
+
|
|
743
|
+
The main export surface is defined in `package.json`.
|
|
744
|
+
|
|
745
|
+
Important root exports include:
|
|
746
|
+
|
|
747
|
+
- `Application`
|
|
748
|
+
- `BuiltinContainer`
|
|
749
|
+
- `injectDependency`
|
|
750
|
+
- `ServiceProvider`
|
|
751
|
+
- `DeferrableServiceProvider`
|
|
752
|
+
- `ConfigRepository`
|
|
753
|
+
- `env`
|
|
754
|
+
- `Str`
|
|
755
|
+
- `Config`, `Cache`, `Store`
|
|
756
|
+
- `CacheManager`
|
|
757
|
+
- `StorageManager`
|
|
758
|
+
- `MemoryStorageAdapter`
|
|
759
|
+
- `LocalStorageAdapter`
|
|
760
|
+
- `IndexedDbAdapter`
|
|
761
|
+
- `FileSystem`
|
|
762
|
+
- `LocalAdapter`
|
|
763
|
+
|
|
764
|
+
Important subpath exports include:
|
|
765
|
+
|
|
766
|
+
- `@raubjo/architect/react`
|
|
767
|
+
- `@raubjo/architect/solid`
|
|
768
|
+
- `@raubjo/architect/svelte`
|
|
769
|
+
- `@raubjo/architect/vue`
|
|
770
|
+
- `@raubjo/architect/application`
|
|
771
|
+
- `@raubjo/architect/container/contract`
|
|
772
|
+
- `@raubjo/architect/container/adapters/inversify`
|
|
773
|
+
- `@raubjo/architect/container/adapters/builtin`
|
|
774
|
+
- `@raubjo/architect/config/adapters/esm`
|
|
775
|
+
- `@raubjo/architect/config/env`
|
|
776
|
+
- `@raubjo/architect/config/repository`
|
|
777
|
+
- `@raubjo/architect/cache/manager`
|
|
778
|
+
- `@raubjo/architect/storage/manager`
|
|
779
|
+
- `@raubjo/architect/filesystem/filesystem`
|
|
780
|
+
- renderer and facade subpaths
|
|
781
|
+
|
|
782
|
+
## Examples
|
|
783
|
+
|
|
784
|
+
The repository contains runnable examples under `examples/`:
|
|
785
|
+
|
|
786
|
+
- `examples/react`
|
|
787
|
+
- `examples/solid`
|
|
788
|
+
- `examples/svelte`
|
|
789
|
+
- `examples/vue`
|
|
790
|
+
|
|
791
|
+
Each example shows:
|
|
792
|
+
|
|
793
|
+
- application bootstrap
|
|
794
|
+
- provider registration
|
|
795
|
+
- service resolution from components
|
|
796
|
+
- a user-driven counter service
|
|
797
|
+
- a heartbeat/background service with startup and cleanup behavior
|
|
798
|
+
|
|
799
|
+
To run one:
|
|
800
|
+
|
|
801
|
+
```sh
|
|
802
|
+
cd examples/react
|
|
803
|
+
bun install
|
|
804
|
+
bun run dev
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
The example applications are intentionally minimal and are the best place to inspect framework-specific bootstrapping details.
|
|
808
|
+
|
|
809
|
+
## Development
|
|
810
|
+
|
|
811
|
+
This repository is written in TypeScript and tested with Bun.
|
|
812
|
+
|
|
813
|
+
Useful scripts:
|
|
814
|
+
|
|
815
|
+
```sh
|
|
816
|
+
bun test
|
|
817
|
+
bun test --coverage
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
Formatting:
|
|
821
|
+
|
|
822
|
+
```sh
|
|
823
|
+
bun run fix
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
The project uses:
|
|
827
|
+
|
|
828
|
+
- ESM modules
|
|
829
|
+
- `strict` TypeScript mode
|
|
830
|
+
- `moduleResolution: "Bundler"`
|
|
831
|
+
- JSX enabled for React-oriented source files
|
|
832
|
+
|
|
833
|
+
Tests cover:
|
|
834
|
+
|
|
835
|
+
- application lifecycle ordering and cleanup
|
|
836
|
+
- built-in container behavior
|
|
837
|
+
- Inversify adapter behavior
|
|
838
|
+
- runtime helpers for each supported framework
|
|
839
|
+
- configuration helpers
|
|
840
|
+
- storage adapters and fallbacks
|
|
841
|
+
- cache manager behavior
|
|
842
|
+
- facade resolution
|
|
843
|
+
|
|
844
|
+
## Notes and constraints
|
|
845
|
+
|
|
846
|
+
The current implementation has a few important constraints worth knowing up front:
|
|
847
|
+
|
|
848
|
+
- `Application.configure(basePath)` is still supported, but startup configuration is driven by `configure({ config })`, not automatic filesystem discovery.
|
|
849
|
+
- `Application.clearConfigCache()` exists for compatibility but is currently a no-op.
|
|
850
|
+
- `useService(...)` resolves a dependency from the active container; it does not subscribe components to service internals automatically.
|
|
851
|
+
- `DeferrableServiceProvider` does not currently enable deferred provider loading by itself.
|
|
852
|
+
- Svelte root components are expected to receive `container` as a prop and call `provideContainer(...)` before using `useService(...)`.
|
|
853
|
+
- Explicit `adapter: "inversify"` requires a registered factory on `globalThis.__iocContainerFactoryRegistry`.
|
|
854
|
+
- If you set a root component without a renderer, or a renderer without a root component, the application throws.
|
|
855
|
+
- `Application.run()` is designed for browser/client execution and assumes `window` is available.
|
|
856
|
+
- All renderer adapters require the target mount node to exist in the DOM.
|
|
857
|
+
|
|
858
|
+
## License
|
|
859
|
+
|
|
860
|
+
MIT
|