@venizia/ignis-boot 0.1.1-2 → 0.1.1-4

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 CHANGED
@@ -1,2689 +1,230 @@
1
1
  <div align="center">
2
2
 
3
- # :fire: IGNIS - @venizia/ignis-boot
3
+ <br />
4
4
 
5
- **Convention-based auto-discovery and bootstrapping for the IGNIS Framework**
5
+ # :fire: IGNIS - `@venizia/ignis-boot`
6
6
 
7
- [![npm](https://img.shields.io/npm/v/@venizia/ignis-boot.svg?style=flat-square&color=cb3837)](https://www.npmjs.com/package/@venizia/ignis-boot)
8
- [![License](https://img.shields.io/badge/License-MIT-3DA639.svg?style=flat-square)](https://opensource.org/licenses/MIT)
9
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6.svg?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
7
+ **Convention-based discovery and bootstrapping.**
10
8
 
11
- Discovers artifact files (controllers, services, repositories, datasources) by glob patterns and registers them into the IoC container during application startup. Three-phase lifecycle (configure, discover, load) with the Template Method pattern. Inspired by [LoopBack 4's boot system](https://loopback.io/doc/en/lb4/Booting-an-Application.html).
9
+ [![Docs](https://img.shields.io/badge/Docs-ignis.venizia.ai-2563EB.svg?style=flat-square)](https://ignis.venizia.ai/references/base/bootstrapping)
10
+ [![npm](https://img.shields.io/npm/v/@venizia/ignis-boot.svg?style=flat-square&color=cb3837&label=@venizia/ignis-boot)](https://www.npmjs.com/package/@venizia/ignis-boot)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-3DA639.svg?style=flat-square)](LICENSE.md)
12
+ [![TypeScript](https://img.shields.io/badge/TypeScript-6.x-3178C6.svg?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
12
13
 
13
- [Installation](#installation) &#8226; [Quick Start](#quick-start) &#8226; [API Reference](#core-concepts) &#8226; [Documentation](https://venizia-ai.github.io/ignis)
14
+ [Bootstrapping reference](https://ignis.venizia.ai/references/base/bootstrapping) &#8226;
15
+ [Application](https://ignis.venizia.ai/references/base/application) &#8226;
16
+ [Dependency injection](https://ignis.venizia.ai/references/base/dependency-injection)
14
17
 
15
18
  </div>
16
19
 
17
- ## Highlights
18
-
19
- | | Feature | |
20
- | :---: | :--- | :--- |
21
- | **1** | **Three-Phase Lifecycle** | Configure, discover, load -- structured and predictable |
22
- | **2** | **Convention Over Configuration** | Default dirs and extensions just work out of the box |
23
- | **3** | **4 Built-in Booters** | Controllers, services, repositories, and datasources |
24
- | **4** | **Template Method Pattern** | Extend `BaseArtifactBooter` for custom artifact types |
25
- | **5** | **Performance Timing** | Built-in `performance.now()` boot report |
26
-
27
20
  ---
28
21
 
29
- ## Table of Contents
22
+ Finds your controllers, services, repositories, and datasources by filename, then binds each one into
23
+ the IoC container - so you never maintain a registration list that drifts from the files on disk.
30
24
 
31
- - [Installation](#installation)
32
- - [Quick Start](#quick-start)
33
- - [Core Concepts](#core-concepts)
34
- - [Three-Phase Lifecycle](#three-phase-lifecycle)
35
- - [BaseArtifactBooter](#baseartifactbooter)
36
- - [Built-in Booters](#built-in-booters)
37
- - [Bootstrapper](#bootstrapper)
38
- - [BootMixin](#bootmixin)
39
- - [Configuration](#configuration)
40
- - [IBootOptions](#ibootoptions)
41
- - [IArtifactOptions](#iartifactoptions)
42
- - [Pattern Generation](#pattern-generation)
43
- - [Complete Lifecycle Walkthrough](#complete-lifecycle-walkthrough)
44
- - [BaseArtifactBooter Internals](#baseartifactbooter-internals)
45
- - [Protected Properties](#protected-properties)
46
- - [Constructor](#baseartifactbooter-constructor)
47
- - [configure() -- Phase 1 Internals](#configure----phase-1-internals)
48
- - [discover() -- Phase 2 Internals](#discover----phase-2-internals)
49
- - [load() -- Phase 3 Internals](#load----phase-3-internals)
50
- - [getPattern() -- Pattern Builder Logic](#getpattern----pattern-builder-logic)
51
- - [Pattern Generation Deep Dive](#pattern-generation-deep-dive)
52
- - [Single Dir + Single Extension](#single-dir--single-extension)
53
- - [Multiple Dirs + Multiple Extensions](#multiple-dirs--multiple-extensions)
54
- - [Nested vs Non-Nested](#nested-vs-non-nested)
55
- - [Custom Glob Override](#custom-glob-override)
56
- - [Dot Stripping from Extensions](#dot-stripping-from-extensions)
57
- - [Pattern Summary Table](#pattern-summary-table)
58
- - [Built-in Booters Deep Dive](#built-in-booters-deep-dive)
59
- - [ControllerBooter](#controllerbooter)
60
- - [ServiceBooter](#servicebooter)
61
- - [RepositoryBooter](#repositorybooter)
62
- - [DatasourceBooter](#datasourcebooter)
63
- - [Why Are Datasources Singletons?](#why-are-datasources-singletons)
64
- - [BootMixin Deep Dive](#bootmixin-deep-dive)
65
- - [Constructor Internals](#constructor-internals)
66
- - [boot() Method](#boot-method)
67
- - [Bootstrapper Internals](#bootstrapper-internals)
68
- - [discoverBooters()](#discoverbooters)
69
- - [runPhase()](#runphase)
70
- - [Error Wrapping](#error-wrapping)
71
- - [Performance Timing](#performance-timing)
72
- - [IBootReport Structure](#ibootreport-structure)
73
- - [Advanced Usage](#advanced-usage)
74
- - [Creating Custom Booters](#creating-custom-booters)
75
- - [Multiple Custom Booter Examples](#multiple-custom-booter-examples)
76
- - [Custom Glob Patterns](#custom-glob-patterns)
77
- - [Boot Options Override Examples](#boot-options-override-examples)
78
- - [Selective Phase Execution](#selective-phase-execution)
79
- - [Boot Report and Performance Timing](#boot-report-and-performance-timing)
80
- - [Integration with BaseApplication](#integration-with-baseapplication)
81
- - [Error Scenarios](#error-scenarios)
82
- - [Debugging Boot](#debugging-boot)
83
- - [Performance Tuning](#performance-tuning)
84
- - [File Naming Conventions](#file-naming-conventions)
85
- - [Boot Utilities](#boot-utilities)
86
- - [Complete Type Reference](#complete-type-reference)
87
- - [Constants Reference](#constants-reference)
88
- - [Testing Patterns](#testing-patterns)
89
- - [License](#license)
90
-
91
- ---
25
+ **Most applications never import this package.** `BaseApplication` from `@venizia/ignis` already depends
26
+ on it, registers the four built-in booters, and exposes `boot()`. You reach for it directly only to
27
+ write a custom booter for an artifact type of your own.
92
28
 
93
- ## Installation
29
+ ## Install
94
30
 
95
31
  ```bash
96
32
  bun add @venizia/ignis-boot
97
33
  ```
98
34
 
99
- **Peer dependencies:**
100
-
101
- ```bash
102
- bun add @venizia/ignis-inversion @venizia/ignis-helpers
103
- ```
104
-
105
- ---
35
+ Already a transitive dependency of `@venizia/ignis`.
106
36
 
107
- ## Quick Start
37
+ ## Use it
108
38
 
109
- The fastest way to use `@venizia/ignis-boot` is through `BootMixin`, which enhances any `Container` subclass with auto-discovery capabilities:
39
+ Convention boot is opt-in: call `boot()` before `start()`.
110
40
 
111
41
  ```typescript
112
- import { BootMixin, IBootOptions } from '@venizia/ignis-boot';
113
- import { Container } from '@venizia/ignis-inversion';
42
+ import { BaseApplication, IApplicationInfo } from '@venizia/ignis';
114
43
 
115
- class MyApplication extends BootMixin(Container) {
116
- bootOptions: IBootOptions = {
117
- controllers: { dirs: ['controllers'], isNested: true },
118
- services: { dirs: ['services'], isNested: true },
119
- repositories: { dirs: ['repositories'] },
120
- datasources: { dirs: ['datasources'] },
121
- };
122
- }
44
+ class App extends BaseApplication {
45
+ getAppInfo(): IApplicationInfo {
46
+ return { name: 'my-app', version: '1.0.0', description: 'My app' };
47
+ }
123
48
 
124
- // Bootstrap and start
125
- const app = new MyApplication();
126
- const report = await app.boot();
127
- // All controllers, services, repositories, and datasources
128
- // are now discovered and registered in the IoC container.
129
- ```
49
+ staticConfigure() {}
50
+ preConfigure() {}
51
+ postConfigure() {}
52
+ setupMiddlewares() {}
53
+ }
130
54
 
131
- When used with `@venizia/ignis` (the core framework), `BaseApplication` already applies the `BootMixin` internally, so you only need to pass `bootOptions` through your application configs:
55
+ const application = new App({
56
+ scope: 'App',
57
+ config: {
58
+ host: '0.0.0.0',
59
+ port: 3000,
60
+ path: { base: '/api', isStrict: false },
61
+ bootOptions: {}, // the defaults cover the conventional layout
62
+ },
63
+ });
132
64
 
133
- ```typescript
134
- import { BaseApplication, IApplicationConfigs } from '@venizia/ignis';
65
+ await application.boot(); // discover and bind every artifact
66
+ await application.start();
67
+ ```
135
68
 
136
- const appConfigs: IApplicationConfigs = {
137
- name: 'MyApp',
138
- bootOptions: {
139
- controllers: { dirs: ['controllers'], isNested: true },
140
- services: { dirs: ['services'], isNested: true },
141
- repositories: { dirs: ['repositories'] },
142
- datasources: { dirs: ['datasources'] },
143
- },
144
- };
69
+ Given the layout below, `boot()` binds `datasources.PostgresDataSource`, `repositories.UserRepository`,
70
+ `services.UserService`, and `controllers.UserController` - no further code.
145
71
 
146
- class Application extends BaseApplication {
147
- constructor() {
148
- super(appConfigs);
149
- }
150
- }
72
+ ```
73
+ <projectRoot>/
74
+ datasources/postgres.datasource.js
75
+ repositories/user.repository.js
76
+ services/user.service.js
77
+ controllers/user.controller.js
151
78
  ```
152
79
 
153
- ---
80
+ ## Discovery conventions
154
81
 
155
- ## Core Concepts
82
+ Each booter has a default directory and a default file suffix, and binds into its own namespace.
156
83
 
157
- ### Three-Phase Lifecycle
84
+ | Booter | Default dir | Default suffix | Binding key | Tag | Scope |
85
+ | :--- | :--- | :--- | :--- | :--- | :--- |
86
+ | `DatasourceBooter` | `datasources` | `.datasource.js` | `datasources.<ClassName>` | `datasources` | singleton |
87
+ | `RepositoryBooter` | `repositories` | `.repository.js` | `repositories.<ClassName>` | `repositories` | transient |
88
+ | `ServiceBooter` | `services` | `.service.js` | `services.<ClassName>` | `services` | transient |
89
+ | `ControllerBooter` | `controllers` | `.controller.js` | `controllers.<ClassName>` | `controllers` | transient |
158
90
 
159
- Every booter follows a strict three-phase execution order. Phases run sequentially, and all registered booters execute within each phase before the next phase begins.
91
+ Datasources are singletons because a connection pool must be shared; everything else resolves per use.
92
+ Directories resolve relative to the application's `getProjectRoot()`, which defaults to `process.cwd()`.
93
+ Subdirectories are included - `isNested` defaults to `true`.
160
94
 
161
- ```
162
- Phase 1: CONFIGURE Phase 2: DISCOVER Phase 3: LOAD
163
- +-----------------+ +-----------------+ +-----------------+
164
- | Merge user opts | | Build glob | | Dynamic import |
165
- | with defaults |-->| pattern from |-->| discovered |
166
- | (dirs, exts, | | configured opts | | files, filter |
167
- | isNested, glob)| | and scan FS | | class exports, |
168
- +-----------------+ +-----------------+ | bind to DI |
169
- +-----------------+
170
- ```
95
+ Booters are registered datasources -> repositories -> services -> controllers, so a repository can
96
+ always resolve the datasource it injects.
171
97
 
172
- **Phase 1 -- Configure:** Merges user-provided options (`dirs`, `extensions`, `isNested`, `glob`) with the booter's defaults. If you provide `dirs: ['custom-controllers']`, it replaces the default `['controllers']`. If you provide nothing, defaults are used.
98
+ > [!IMPORTANT]
99
+ > **The default suffixes end in `.js`, not `.ts`.** Discovery globs files on disk and `import`s them, and
100
+ > it is designed to run against your **compiled output**. Build first, and have `getProjectRoot()` point
101
+ > at the built directory - returning `__dirname` from the compiled entrypoint does exactly that. If
102
+ > `boot()` reports zero artifacts, this is almost always why. To boot from TypeScript sources instead,
103
+ > set the extensions explicitly: `{ controllers: { extensions: ['.controller.ts'] } }`.
173
104
 
174
- **Phase 2 -- Discover:** Builds a glob pattern from the configured options and scans the filesystem relative to the project root. The result is an array of absolute file paths stored in `discoveredFiles`.
105
+ ## Phases
175
106
 
176
- **Phase 3 -- Load:** Dynamically imports each discovered file, filters all exports to keep only class constructors (using `isClass`), and then calls `bind()` to register each class in the IoC container with the appropriate namespace and scope.
107
+ `Bootstrapper.boot()` runs three phases across **all** booters - phase by phase, not booter by booter.
177
108
 
178
- ### BaseArtifactBooter
109
+ | Phase | What happens |
110
+ | :--- | :--- |
111
+ | `configure` | Fill in defaults for `dirs`, `extensions`, `isNested`, then build the glob pattern |
112
+ | `discover` | Run the glob under the project root, collecting absolute file paths |
113
+ | `load` | `import()` each file, keep the exported classes, bind them into the container |
179
114
 
180
- The `BaseArtifactBooter` implements the [Template Method pattern](https://refactoring.guru/design-patterns/template-method). It defines the algorithm skeleton (configure, discover, load) while deferring booter-specific behavior to subclasses through abstract methods.
115
+ `boot()` resolves to an `IBootReport`: the booter class names that ran, per-phase `durationMs`, and
116
+ `totalDurationMs`. A throw in any phase is rewrapped naming both the phase and the booter, with the
117
+ original error kept as `cause`.
181
118
 
182
- ```
183
- BaseArtifactBooter (extends BaseHelper, implements IBooter)
184
- |
185
- |-- configure() # Phase 1: merge options with defaults
186
- |-- discover() # Phase 2: glob filesystem for files
187
- |-- load() # Phase 3: import files + call bind()
188
- |
189
- |-- abstract getDefaultDirs() # Subclass: default directories
190
- |-- abstract getDefaultExtensions() # Subclass: default file extensions
191
- |-- abstract bind() # Subclass: register classes in container
192
- |
193
- |-- getPattern() # Build glob pattern from options
194
- |
195
- |-- (protected) root # Project root directory
196
- |-- (protected) artifactOptions # Merged configuration
197
- |-- (protected) discoveredFiles # Result of discover phase
198
- |-- (protected) loadedClasses # Result of load phase
199
- ```
119
+ ## Configuring discovery
200
120
 
201
- The base class handles all the common logic (option merging, glob execution, file importing, class filtering), so subclasses only need to declare their conventions and binding strategy.
121
+ Pass `bootOptions` in the application config. Every artifact type takes the same `IArtifactOptions`.
202
122
 
203
- ### Built-in Booters
123
+ | Option | Type | Meaning |
124
+ | :--- | :--- | :--- |
125
+ | `dirs` | `string[]` | Directories to scan, relative to the project root |
126
+ | `extensions` | `string[]` | File suffixes to match |
127
+ | `isNested` | `boolean` | Include subdirectories - default `true` |
128
+ | `glob` | `string` | Raw glob pattern; overrides the three above |
204
129
 
205
- The package ships with four built-in booters covering the most common artifact types:
130
+ ```typescript
131
+ bootOptions: {
132
+ controllers: { dirs: ['controllers', 'api'], extensions: ['.controller.js'] },
133
+ services: { glob: 'modules/**/*.service.js' },
134
+ }
135
+ ```
206
136
 
207
- | Booter | Default Directory | Default Extension | Namespace | Scope | Binding Key Example |
208
- | -------------------- | ----------------- | ------------------- | -------------- | ------------- | -------------------------------- |
209
- | `ControllerBooter` | `controllers/` | `.controller.js` | `controllers` | transient | `controllers.UserController` |
210
- | `ServiceBooter` | `services/` | `.service.js` | `services` | transient | `services.AuthService` |
211
- | `RepositoryBooter` | `repositories/` | `.repository.js` | `repositories` | transient | `repositories.UserRepository` |
212
- | `DatasourceBooter` | `datasources/` | `.datasource.js` | `datasources` | **singleton** | `datasources.PostgresDataSource` |
137
+ A key that is present but `undefined` - the shape `dirs: process.env.APP_DIRS?.split(',')` produces - is
138
+ treated as not provided and falls back to the default. An empty `dirs` or `extensions` array is not: it
139
+ throws when the pattern is built.
213
140
 
214
- **Why are datasources singletons?** Datasources manage connection pools and shared resources (database connections, Redis clients, etc.). Creating new instances per injection would leak connections and defeat pool sharing. All other artifact types default to transient scope.
141
+ ## Custom booters
215
142
 
216
- Each built-in booter receives its configuration via constructor injection:
143
+ Extend `BaseArtifactBooter` and implement its three hooks. The base class handles configure, discover,
144
+ and load; you decide only where to look and what to bind.
217
145
 
218
146
  ```typescript
219
- export class ControllerBooter extends BaseArtifactBooter {
147
+ import { BaseArtifactBooter, IApplication, IBootOptions } from '@venizia/ignis-boot';
148
+ import { BindingKeys, inject } from '@venizia/ignis-inversion';
149
+
150
+ export class ObserverBooter extends BaseArtifactBooter {
220
151
  constructor(
221
152
  @inject({ key: '@app/project_root' }) root: string,
222
153
  @inject({ key: '@app/instance' }) private readonly application: IApplication,
223
154
  @inject({ key: '@app/boot-options' }) bootOptions: IBootOptions,
224
155
  ) {
225
- super({
226
- scope: ControllerBooter.name,
227
- root,
228
- artifactOptions: bootOptions.controllers ?? {},
229
- });
156
+ super({ scope: ObserverBooter.name, root, artifactOptions: bootOptions.observers ?? {} });
230
157
  }
231
158
 
232
159
  protected override getDefaultDirs(): string[] {
233
- return ['controllers'];
160
+ return ['observers'];
234
161
  }
235
162
 
236
163
  protected override getDefaultExtensions(): string[] {
237
- return ['.controller.js'];
164
+ return ['.observer.js'];
238
165
  }
239
166
 
240
167
  protected override async bind(): Promise<void> {
241
168
  for (const cls of this.loadedClasses) {
242
- const key = BindingKeys.build({ namespace: 'controllers', key: cls.name });
243
- this.application.bind({ key }).toClass(cls).setTags('controllers');
169
+ const key = BindingKeys.build({ namespace: 'observers', key: cls.name });
170
+ this.application.bind({ key }).toClass(cls).setTags('observers');
244
171
  }
245
172
  }
246
173
  }
247
174
  ```
248
175
 
249
- ### Bootstrapper
250
-
251
- The `Bootstrapper` is the orchestrator that coordinates all booters through all phases. It is responsible for:
252
-
253
- 1. **Discovering booters** -- finds all bindings tagged with `'booter'` in the container
254
- 2. **Running phases** -- executes each phase sequentially on all discovered booters
255
- 3. **Error handling** -- wraps errors with context (phase name + booter class name)
256
- 4. **Performance timing** -- tracks `performance.now()` timestamps per phase
257
-
258
- ```typescript
259
- export class Bootstrapper extends BaseHelper implements IBootstrapper {
260
- constructor(
261
- @inject({ key: '@app/instance' }) private readonly application: IApplication,
262
- ) {
263
- super({ scope: Bootstrapper.name });
264
- }
265
-
266
- async boot(opts: IBootExecutionOptions): Promise<IBootReport> {
267
- const { phases = BOOT_PHASES, booters } = opts;
268
-
269
- await this.discoverBooters();
270
- for (const phase of phases) {
271
- await this.runPhase({ phase, booterNames: booters });
272
- }
273
- return this.generateReport();
274
- }
275
- }
276
- ```
277
-
278
- **Execution flow:**
279
-
280
- ```
281
- boot()
282
- |-> discoverBooters() # Find all 'booter'-tagged bindings
283
- |-> runPhase('configure') # All booters: configure()
284
- |-> runPhase('discover') # All booters: discover()
285
- |-> runPhase('load') # All booters: load()
286
- |-> generateReport() # Return performance data
287
- ```
288
-
289
- Within each phase, booters are executed sequentially in registration order. This is important because the registration order in `BootMixin` is:
290
-
291
- 1. `DatasourceBooter` -- datasources first (others may depend on them)
292
- 2. `RepositoryBooter` -- repositories second (depend on datasources)
293
- 3. `ServiceBooter` -- services third (depend on repositories)
294
- 4. `ControllerBooter` -- controllers last (depend on services)
295
-
296
- ### BootMixin
297
-
298
- `BootMixin` is a mixin function that enhances any `Container` subclass with boot capabilities. It handles all the wiring automatically:
299
-
300
- ```typescript
301
- import { BootMixin } from '@venizia/ignis-boot';
302
- import { Container } from '@venizia/ignis-inversion';
303
-
304
- class MyApp extends BootMixin(Container) {
305
- bootOptions = { /* ... */ };
306
- }
307
- ```
308
-
309
- **What it registers in the constructor:**
310
-
311
- | Binding Key | Value | Tags | Scope |
312
- | --------------------------- | ----------------------- | -------- | --------- |
313
- | `@app/boot-options` | User's `bootOptions` | -- | -- |
314
- | `booter.DatasourceBooter` | `DatasourceBooter` class | `booter` | transient |
315
- | `booter.RepositoryBooter` | `RepositoryBooter` class | `booter` | transient |
316
- | `booter.ServiceBooter` | `ServiceBooter` class | `booter` | transient |
317
- | `booter.ControllerBooter` | `ControllerBooter` class | `booter` | transient |
318
- | `bootstrapper` | `Bootstrapper` class | -- | singleton |
319
-
320
- **What it adds to the class:**
321
-
322
- - `bootOptions?: IBootOptions` -- property for user configuration
323
- - `boot(): Promise<IBootReport>` -- method that resolves the `Bootstrapper` singleton and calls `boot({})`
324
-
325
- ---
326
-
327
- ## Configuration
328
-
329
- ### IBootOptions
330
-
331
- The top-level configuration object maps artifact type names to their discovery options. It includes four built-in keys and supports arbitrary extension:
332
-
333
- ```typescript
334
- interface IBootOptions {
335
- controllers?: IArtifactOptions;
336
- services?: IArtifactOptions;
337
- repositories?: IArtifactOptions;
338
- datasources?: IArtifactOptions;
339
- [artifactType: string]: IArtifactOptions | undefined; // Extensible for custom booters
340
- }
341
- ```
342
-
343
- **Example:**
344
-
345
- ```typescript
346
- const bootOptions: IBootOptions = {
347
- controllers: {
348
- dirs: ['controllers'],
349
- isNested: true,
350
- },
351
- services: {
352
- dirs: ['services'],
353
- extensions: ['.service.js'],
354
- },
355
- repositories: {
356
- dirs: ['repositories'],
357
- },
358
- datasources: {
359
- dirs: ['datasources'],
360
- },
361
- // Custom booter options
362
- handlers: {
363
- dirs: ['handlers'],
364
- extensions: ['.handler.js'],
365
- },
366
- };
367
- ```
368
-
369
- ### IArtifactOptions
370
-
371
- Configuration for a single artifact type's discovery behavior:
372
-
373
- ```typescript
374
- interface IArtifactOptions {
375
- dirs?: string[]; // Directories to scan (relative to project root)
376
- extensions?: string[]; // File extensions to match (e.g., '.controller.js')
377
- isNested?: boolean; // Scan subdirectories? Default: true
378
- glob?: string; // Custom glob pattern (overrides dirs/extensions entirely)
379
- }
380
- ```
381
-
382
- | Option | Type | Default | Description |
383
- | ------------ | ---------- | ---------------- | --------------------------------------------------------------------- |
384
- | `dirs` | `string[]` | Booter-specific | Directories to scan, relative to project root |
385
- | `extensions` | `string[]` | Booter-specific | File extension patterns to match |
386
- | `isNested` | `boolean` | `true` | Whether to recurse into subdirectories |
387
- | `glob` | `string` | `undefined` | Custom glob pattern; if set, `dirs` and `extensions` are ignored |
388
-
389
- ### Pattern Generation
390
-
391
- When no custom `glob` is provided, `BaseArtifactBooter.getPattern()` builds a glob pattern from `dirs`, `extensions`, and `isNested`:
392
-
393
- **Single directory, single extension:**
394
-
395
- ```
396
- dirs: ['repositories']
397
- extensions: ['.repository.js']
398
- isNested: true
399
-
400
- Result: repositories/{**/*,*}.repository.js
401
- ```
402
-
403
- **Multiple directories or extensions:**
404
-
405
- ```
406
- dirs: ['dir1', 'dir2']
407
- extensions: ['.ext1.js', '.ext2.js']
408
- isNested: true
409
-
410
- Result: {dir1,dir2}/{**/*,*}.{ext1.js,ext2.js}
411
- ```
412
-
413
- **Non-nested (single level only):**
414
-
415
- ```
416
- dirs: ['controllers']
417
- extensions: ['.controller.js']
418
- isNested: false
419
-
420
- Result: controllers/*.controller.js
421
- ```
422
-
423
- **Custom glob (overrides everything):**
424
-
425
- ```
426
- glob: 'custom/glob/pattern/**/*.js'
427
-
428
- Result: custom/glob/pattern/**/*.js
429
- ```
430
-
431
- The leading dot in extensions is automatically stripped during pattern generation. For example, `.controller.js` becomes `controller.js` in the glob pattern.
432
-
433
- ---
434
-
435
- ## Complete Lifecycle Walkthrough
436
-
437
- This section walks through what happens step-by-step when you call `app.boot()` on an application with the following configuration:
438
-
439
- ```typescript
440
- const bootOptions: IBootOptions = {
441
- controllers: { dirs: ['controllers'], isNested: true },
442
- services: { dirs: ['services'] },
443
- repositories: { dirs: ['repositories'] },
444
- datasources: { dirs: ['datasources'] },
445
- };
446
- ```
447
-
448
- Assume the project's `dist/` directory has this structure:
449
-
450
- ```
451
- dist/
452
- controllers/
453
- user.controller.js (exports UserController)
454
- admin/
455
- admin.controller.js (exports AdminController)
456
- services/
457
- auth.service.js (exports AuthService)
458
- repositories/
459
- user.repository.js (exports UserRepository)
460
- datasources/
461
- postgres.datasource.js (exports PostgresDataSource)
462
- ```
463
-
464
- ### Step 1: boot() is called
465
-
466
- The `boot()` method (from `BootMixin` or `BaseApplication`) resolves the `Bootstrapper` singleton from the container:
467
-
468
- ```typescript
469
- boot(): Promise<IBootReport> {
470
- const bootstrapper = this.get<Bootstrapper>({ key: 'bootstrapper' });
471
- return bootstrapper.boot({});
472
- }
473
- ```
474
-
475
- ### Step 2: Bootstrapper.boot() begins
476
-
477
- The `Bootstrapper` is instantiated (or retrieved from singleton cache) and runs:
478
-
479
- ```
480
- [Bootstrapper][boot] Starting boot | Number of booters: 4
481
- ```
482
-
483
- ### Step 3: discoverBooters()
484
-
485
- The `Bootstrapper` calls `this.application.findByTag({ tag: 'booter' })` to find all bindings tagged `'booter'`. It resolves each binding (instantiating the booter class via the IoC container), which triggers constructor injection of `@app/project_root`, `@app/instance`, and `@app/boot-options`.
486
-
487
- ```
488
- [Bootstrapper][discoverBooters] Discovered booter: booter.DatasourceBooter
489
- [Bootstrapper][discoverBooters] Discovered booter: booter.RepositoryBooter
490
- [Bootstrapper][discoverBooters] Discovered booter: booter.ServiceBooter
491
- [Bootstrapper][discoverBooters] Discovered booter: booter.ControllerBooter
492
- ```
493
-
494
- The booters are stored in an array in registration order. This order matters -- datasources are discovered and loaded before repositories, which are discovered before services, etc.
495
-
496
- ### Step 4: Phase CONFIGURE
497
-
498
- The `Bootstrapper` calls `configure()` on each booter sequentially.
499
-
500
- Each booter merges user-provided options with its defaults. If the user specified `dirs: ['controllers']`, that is used. If the user did not specify `extensions`, the booter falls back to its `getDefaultExtensions()` return value.
501
-
502
- ```
503
- [Bootstrapper][runPhase] Starting phase: CONFIGURE
504
-
505
- [DatasourceBooter][configure] Configured: {"dirs":["datasources"],"extensions":[".datasource.js"],"isNested":true}
506
- [RepositoryBooter][configure] Configured: {"dirs":["repositories"],"extensions":[".repository.js"],"isNested":true}
507
- [ServiceBooter][configure] Configured: {"dirs":["services"],"extensions":[".service.js"],"isNested":true}
508
- [ControllerBooter][configure] Configured: {"dirs":["controllers"],"extensions":[".controller.js"],"isNested":true}
509
-
510
- [Bootstrapper][runPhase] Completed phase: CONFIGURE | Took: 0.42 ms
511
- ```
512
-
513
- ### Step 5: Phase DISCOVER
514
-
515
- Each booter calls `getPattern()` to build a glob string, then uses the `glob` library to scan the filesystem from the project root.
516
-
517
- ```
518
- [Bootstrapper][runPhase] Starting phase: DISCOVER
519
-
520
- [DatasourceBooter][discover] Root: /app/dist | Using pattern: datasources/{**/*,*}.datasource.js | Discovered file: ["/app/dist/datasources/postgres.datasource.js"]
521
- [RepositoryBooter][discover] Root: /app/dist | Using pattern: repositories/{**/*,*}.repository.js | Discovered file: ["/app/dist/repositories/user.repository.js"]
522
- [ServiceBooter][discover] Root: /app/dist | Using pattern: services/{**/*,*}.service.js | Discovered file: ["/app/dist/services/auth.service.js"]
523
- [ControllerBooter][discover] Root: /app/dist | Using pattern: controllers/{**/*,*}.controller.js | Discovered file: ["/app/dist/controllers/user.controller.js","/app/dist/controllers/admin/admin.controller.js"]
524
-
525
- [Bootstrapper][runPhase] Completed phase: DISCOVER | Took: 12.8 ms
526
- ```
527
-
528
- Note how `controllers/admin/admin.controller.js` was discovered because `isNested: true` generates the pattern `{**/*,*}` which matches both the root level and subdirectories.
529
-
530
- ### Step 6: Phase LOAD
531
-
532
- Each booter dynamically imports its discovered files using `await import(file)`, iterates all named exports, filters for class constructors using `isClass()`, and then calls its `bind()` method to register classes in the IoC container.
533
-
534
- ```
535
- [Bootstrapper][runPhase] Starting phase: LOAD
536
-
537
- [DatasourceBooter][bind] Bound key: datasources.PostgresDataSource (scope: singleton)
538
- [RepositoryBooter][bind] Bound key: repositories.UserRepository (scope: transient)
539
- [ServiceBooter][bind] Bound key: services.AuthService (scope: transient)
540
- [ControllerBooter][bind] Bound key: controllers.UserController (scope: transient)
541
- [ControllerBooter][bind] Bound key: controllers.AdminController (scope: transient)
542
-
543
- [Bootstrapper][runPhase] Completed phase: LOAD | Took: 45.3 ms
544
- ```
545
-
546
- ### Step 7: generateReport()
547
-
548
- The `Bootstrapper` returns an `IBootReport` object. The current implementation returns an empty object (`{}`), but the infrastructure for phase timing is in place for future extension.
549
-
550
- ```
551
- [Bootstrapper][generateReport] Boot report: {}
552
- ```
553
-
554
- ### Final State
555
-
556
- After boot completes, the IoC container holds:
557
-
558
- | Binding Key | Class | Scope | Tags |
559
- | ---------------------------------- | -------------------- | --------- | ------------- |
560
- | `datasources.PostgresDataSource` | `PostgresDataSource` | singleton | `datasources` |
561
- | `repositories.UserRepository` | `UserRepository` | transient | `repositories`|
562
- | `services.AuthService` | `AuthService` | transient | `services` |
563
- | `controllers.UserController` | `UserController` | transient | `controllers` |
564
- | `controllers.AdminController` | `AdminController` | transient | `controllers` |
565
-
566
- These can now be resolved anywhere via `@inject({ key: 'services.AuthService' })` or `app.get({ key: 'controllers.UserController' })`.
567
-
568
- ---
569
-
570
- ## BaseArtifactBooter Internals
571
-
572
- This section documents every protected property, method, and internal behavior of `BaseArtifactBooter`.
573
-
574
- ### Protected Properties
575
-
576
- ```typescript
577
- export abstract class BaseArtifactBooter extends BaseHelper implements IBooter {
578
- protected root: string = '';
579
- protected artifactOptions: IArtifactOptions = {};
580
- protected discoveredFiles: string[] = [];
581
- protected loadedClasses: TClass<any>[] = [];
582
-
583
- // Inherited from BaseHelper:
584
- // scope: string
585
- // identifier: string
586
- // logger: Logger
587
- }
588
- ```
589
-
590
- | Property | Type | Initial Value | Description |
591
- | ----------------- | ----------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
592
- | `root` | `string` | `''` | Absolute path to the project root directory. Set from constructor `opts.root`. Glob patterns are resolved relative to this path. |
593
- | `artifactOptions` | `IArtifactOptions` | `{}` | The merged configuration after `configure()` runs. Before `configure()`, holds whatever the user passed (or `{}`). After `configure()`, has all fields populated with defaults. |
594
- | `discoveredFiles` | `string[]` | `[]` | Array of absolute file paths populated by `discover()`. Reset to `[]` at the start of each `discover()` call. |
595
- | `loadedClasses` | `TClass<any>[]` | `[]` | Array of class constructors extracted from discovered files by `load()`. Reset to `[]` at the start of each `load()` call. |
596
-
597
- ### BaseArtifactBooter Constructor
598
-
599
- ```typescript
600
- constructor(opts: IBooterOptions) {
601
- super({ scope: opts.scope });
602
-
603
- this.artifactOptions = opts.artifactOptions;
604
- this.root = opts.root;
605
- }
606
- ```
607
-
608
- The constructor receives an `IBooterOptions` object with three fields:
609
-
610
- | Field | Type | Description |
611
- | ------------------ | ------------------ | ---------------------------------------------------------------------------------------------- |
612
- | `scope` | `string` | Logger scope name, typically `BooterClassName.name` (e.g., `"ControllerBooter"`) |
613
- | `root` | `string` | Absolute path to project root (injected as `@app/project_root`) |
614
- | `artifactOptions` | `IArtifactOptions` | User-provided options from `bootOptions.controllers` (or whichever artifact key) |
615
-
616
- The `super({ scope })` call initializes the `BaseHelper` which sets up scoped logging via `LoggerFactory.getLogger([scope])`.
617
-
618
- ### configure() -- Phase 1 Internals
619
-
620
- ```typescript
621
- async configure(): Promise<void> {
622
- this.artifactOptions = {
623
- dirs: this.artifactOptions?.dirs ?? this.getDefaultDirs(),
624
- extensions: this.artifactOptions?.extensions ?? this.getDefaultExtensions(),
625
- isNested: this.artifactOptions?.isNested ?? true,
626
- glob: this.artifactOptions?.glob,
627
- ...this.artifactOptions,
628
- };
629
-
630
- this.logger.for(this.configure.name).debug(`Configured: %j`, this.artifactOptions);
631
- }
632
- ```
633
-
634
- **Merge behavior in detail:**
635
-
636
- 1. A new object is created with defaults computed from `getDefaultDirs()` and `getDefaultExtensions()`
637
- 2. The spread `...this.artifactOptions` at the end means any user-provided values override the defaults
638
- 3. If the user provided `dirs: ['custom']`, the default is computed as `['controllers']` but then overwritten by the spread
639
- 4. If the user provided nothing for `dirs`, `this.artifactOptions?.dirs` is `undefined`, so `getDefaultDirs()` is used
640
- 5. `isNested` defaults to `true` if not provided
641
- 6. `glob` is passed through as-is (no default)
642
-
643
- **Important:** Because of the spread at the end, user options always win. This means if you pass `{ dirs: ['custom-controllers'], extensions: ['.ctrl.js'] }`, both fields override defaults.
644
-
645
- ### discover() -- Phase 2 Internals
646
-
647
- ```typescript
648
- async discover(): Promise<void> {
649
- const pattern = this.getPattern();
650
-
651
- try {
652
- this.discoveredFiles = []; // Reset discovered files
653
- this.discoveredFiles = await discoverFiles({ root: this.root, pattern });
654
- this.logger
655
- .for(this.discover.name)
656
- .debug(
657
- `Root: %s | Using pattern: %s | Discovered file: %j`,
658
- this.root,
659
- pattern,
660
- this.discoveredFiles,
661
- );
662
- } catch (error) {
663
- throw getError({
664
- message: `[discover] Failed to discover files using pattern: ${pattern} | Error: ${(error as Error)?.message}`,
665
- });
666
- }
667
- }
668
- ```
669
-
670
- **Key behaviors:**
671
-
672
- 1. `discoveredFiles` is reset to `[]` before each discovery run -- this means calling `discover()` multiple times replaces previous results
673
- 2. `getPattern()` is called to build the glob string (see next section)
674
- 3. `discoverFiles()` uses the `glob` npm package with `{ cwd: root, absolute: true }` options
675
- 4. Results are absolute file paths (e.g., `/app/dist/controllers/user.controller.js`)
676
- 5. If the glob pattern matches zero files, `discoveredFiles` is simply an empty array -- this is not an error
677
- 6. If the `glob` library itself throws (e.g., invalid pattern syntax), the error is caught and re-thrown with context
678
-
679
- ### load() -- Phase 3 Internals
680
-
681
- ```typescript
682
- async load(): Promise<void> {
683
- if (!this.discoveredFiles.length) {
684
- this.logger.for(this.load.name).debug(`No files discovered to load.`);
685
- return;
686
- }
687
-
688
- try {
689
- this.loadedClasses = []; // Reset loaded classes
690
- this.loadedClasses = await loadClasses({ files: this.discoveredFiles, root: this.root });
691
- await this.bind();
692
- } catch (error) {
693
- throw getError({
694
- message: `[load] Failed to load classes from discovered files | Error: ${(error as Error)?.message}`,
695
- });
696
- }
697
- }
698
- ```
699
-
700
- **Key behaviors:**
701
-
702
- 1. If `discoveredFiles` is empty, `load()` returns immediately with a debug log -- no error thrown
703
- 2. `loadedClasses` is reset to `[]` before each load
704
- 3. `loadClasses()` iterates each file, calls `await import(file)`, and checks every named export with `isClass()`
705
- 4. `isClass()` checks: `typeof target === 'function' && target.prototype !== undefined`
706
- 5. Arrow functions fail the `isClass` check because they have no `prototype` property
707
- 6. After loading classes, the abstract `bind()` method is called -- this is where subclasses register classes in the container
708
- 7. If any file import fails or `bind()` throws, the error is caught and re-thrown with context
709
-
710
- ### getPattern() -- Pattern Builder Logic
176
+ `IBootOptions` carries an index signature, so `bootOptions.observers` type-checks with no change to the
177
+ framework. Booters are found by the `booter` tag, so registering is all that remains - and it must
178
+ happen before `boot()` runs, which is earlier than `preConfigure()`:
711
179
 
712
180
  ```typescript
713
- protected getPattern(): string {
714
- // Use custom glob if provided
715
- if (this.artifactOptions.glob) {
716
- return this.artifactOptions.glob;
717
- }
718
-
719
- if (!this.artifactOptions.dirs?.length) {
720
- throw getError({
721
- message: `[getPattern] No directories specified for artifact discovery`,
722
- });
723
- }
724
-
725
- if (!this.artifactOptions.extensions?.length) {
726
- throw getError({
727
- message: `[${this.scope}][getPattern] No file extensions specified for artifact discovery`,
728
- });
729
- }
730
-
731
- const dirs = this.artifactOptions.dirs.join(',');
732
- const exts = this.artifactOptions.extensions
733
- .map(e => (e.startsWith('.') ? e.slice(1) : e))
734
- .join(',');
735
-
736
- const nested = this.artifactOptions.isNested ? '{**/*,*}' : '*';
737
-
738
- if (this.artifactOptions.dirs.length > 1 || this.artifactOptions.extensions.length > 1) {
739
- return `{${dirs}}/${nested}.{${exts}}`;
740
- } else {
741
- return `${dirs}/${nested}.${exts}`;
742
- }
181
+ override async boot() {
182
+ this.booter(ObserverBooter);
183
+ return super.boot();
743
184
  }
744
185
  ```
745
186
 
746
- **Decision tree:**
747
-
748
- 1. If `glob` is set, return it immediately (no validation of dirs/extensions)
749
- 2. If no `dirs` provided, throw an error
750
- 3. If no `extensions` provided, throw an error
751
- 4. Strip leading dots from extensions: `.controller.js` becomes `controller.js`
752
- 5. If there is exactly one dir AND exactly one extension, use the simple format: `dir/nested.ext`
753
- 6. If there are multiple dirs OR multiple extensions, use brace expansion: `{dirs}/nested.{exts}`
754
-
755
- **Why `{**/*,*}` for nested?** This glob alternation matches files in both subdirectories (`**/file`) and the root directory (`file`). Without the `,*` part, files directly in the target directory (not in a subdirectory) would not be matched.
756
-
757
- ---
758
-
759
- ## Pattern Generation Deep Dive
760
-
761
- ### Single Dir + Single Extension
762
-
763
- **Input:**
764
-
765
- ```typescript
766
- { dirs: ['controllers'], extensions: ['.controller.js'], isNested: true }
767
- ```
768
-
769
- **Processing:**
187
+ Every constructor parameter must carry `@inject` - a container-instantiated class with one bare
188
+ parameter is refused at boot.
770
189
 
771
- - `dirs.join(',')` => `"controllers"`
772
- - Extensions: `.controller.js` => strip dot => `"controller.js"`
773
- - `dirs.length === 1 && extensions.length === 1` => simple format
774
- - `nested = '{**/*,*}'`
190
+ ## What you must know
775
191
 
776
- **Output:**
192
+ - **Every exported class in a matched file is bound**, not only the one whose name matches the file.
193
+ Keep helper classes out of `*.controller.js` and friends, or move them to their own module.
194
+ - **Discovery is filename-driven, not decorator-driven.** A correctly decorated controller in the wrong
195
+ directory is invisible to it - register that one manually with `this.controller(...)`.
196
+ - Calling `boot()` twice does not double-register: the bootstrapper resets its booter list on each run.
197
+ - `reflect-metadata` must be imported once at your entrypoint, and `experimentalDecorators` plus
198
+ `emitDecoratorMetadata` must be `true` **inline** in your `tsconfig.json` - Bun does not resolve them
199
+ through `extends`, and `@inject` is silently dropped without them.
777
200
 
778
- ```
779
- controllers/{**/*,*}.controller.js
780
- ```
781
-
782
- **Matches:** `controllers/user.controller.js`, `controllers/admin/user.controller.js`, `controllers/a/b/c/deep.controller.js`
783
-
784
- **Does NOT match:** `controllers/user.service.js`, `other-dir/user.controller.js`
785
-
786
- ### Multiple Dirs + Multiple Extensions
787
-
788
- **Input:**
789
-
790
- ```typescript
791
- { dirs: ['private-controllers', 'public-controllers'], extensions: ['.controller.js', '.ctrl.js'], isNested: true }
792
- ```
793
-
794
- **Processing:**
795
-
796
- - `dirs.join(',')` => `"private-controllers,public-controllers"`
797
- - Extensions: `.controller.js` => `controller.js`, `.ctrl.js` => `ctrl.js` => `"controller.js,ctrl.js"`
798
- - `dirs.length === 2 || extensions.length === 2` => brace expansion format
799
-
800
- **Output:**
201
+ ## Surface
801
202
 
802
- ```
803
- {private-controllers,public-controllers}/{**/*,*}.{controller.js,ctrl.js}
804
- ```
203
+ | Export | What it is |
204
+ | :--- | :--- |
205
+ | `BaseArtifactBooter` | Abstract base implementing the three phases; extend it for custom artifacts |
206
+ | `Bootstrapper` | Runs the phases across every `booter`-tagged binding and returns the report |
207
+ | `BootMixin` | Adds `boot()`, `bootOptions`, and the four built-in booters to a raw `Container` |
208
+ | `ControllerBooter` / `ServiceBooter` / `RepositoryBooter` / `DatasourceBooter` | The built-in booters |
209
+ | `discoverFiles` / `loadClasses` | The glob and dynamic-import primitives, usable standalone |
210
+ | `BootPhases` | `configure`, `discover`, `load` |
211
+ | `IBootOptions` / `IArtifactOptions` / `IBootReport` / `IBooter` | Public types |
805
212
 
806
- **Matches:** `private-controllers/user.controller.js`, `public-controllers/admin/settings.ctrl.js`
213
+ `BootMixin` is for applications not built on `BaseApplication` - it wires the same four booters onto any
214
+ `Container`. `BaseApplication` does its own registration and does not use the mixin.
807
215
 
808
- ### Multiple Dirs + Single Extension
216
+ Full detail: [Bootstrapping reference](https://ignis.venizia.ai/references/base/bootstrapping).
809
217
 
810
- **Input:**
218
+ ## Links
811
219
 
812
- ```typescript
813
- { dirs: ['api', 'admin'], extensions: ['.controller.js'], isNested: true }
814
- ```
815
-
816
- **Processing:**
817
-
818
- - `dirs.length === 2` => brace expansion triggered
819
-
820
- **Output:**
821
-
822
- ```
823
- {api,admin}/{**/*,*}.controller.js
824
- ```
825
-
826
- ### Single Dir + Multiple Extensions
827
-
828
- **Input:**
829
-
830
- ```typescript
831
- { dirs: ['services'], extensions: ['.service.js', '.svc.js'], isNested: true }
832
- ```
833
-
834
- **Processing:**
835
-
836
- - `extensions.length === 2` => brace expansion triggered
837
-
838
- **Output:**
839
-
840
- ```
841
- {services}/{**/*,*}.{service.js,svc.js}
842
- ```
843
-
844
- ### Nested vs Non-Nested
845
-
846
- **Nested (default, `isNested: true`):**
847
-
848
- ```typescript
849
- { dirs: ['repositories'], extensions: ['.repository.js'], isNested: true }
850
- ```
851
-
852
- **Output:**
853
-
854
- ```
855
- repositories/{**/*,*}.repository.js
856
- ```
857
-
858
- The `{**/*,*}` alternation ensures files are matched at any depth, including the root of the directory.
859
-
860
- **Non-nested (`isNested: false`):**
861
-
862
- ```typescript
863
- { dirs: ['repositories'], extensions: ['.repository.js'], isNested: false }
864
- ```
865
-
866
- **Output:**
867
-
868
- ```
869
- repositories/*.repository.js
870
- ```
871
-
872
- Only matches files directly inside `repositories/` -- no subdirectory scanning.
873
-
874
- ### Custom Glob Override
875
-
876
- **Input:**
877
-
878
- ```typescript
879
- { dirs: ['ignored'], extensions: ['.ignored.js'], glob: 'modules/**/handlers/*.handler.js' }
880
- ```
881
-
882
- **Output:**
883
-
884
- ```
885
- modules/**/handlers/*.handler.js
886
- ```
887
-
888
- When `glob` is set, `dirs`, `extensions`, and `isNested` are all completely ignored. The custom glob is returned as-is.
889
-
890
- ### Dot Stripping from Extensions
891
-
892
- The `getPattern()` method strips the leading dot from each extension before building the pattern:
893
-
894
- ```typescript
895
- const exts = this.artifactOptions.extensions
896
- .map(e => (e.startsWith('.') ? e.slice(1) : e))
897
- .join(',');
898
- ```
899
-
900
- | Input Extension | After Stripping | In Pattern |
901
- | --------------------- | --------------------- | ----------------------- |
902
- | `.controller.js` | `controller.js` | `*.controller.js` |
903
- | `.service.js` | `service.js` | `*.service.js` |
904
- | `handler.js` | `handler.js` (no dot) | `*.handler.js` |
905
- | `.my.custom.ext.js` | `my.custom.ext.js` | `*.my.custom.ext.js` |
906
-
907
- This means files need a dot before the extension in their filename. For example, the pattern `*.controller.js` matches `user.controller.js` but does NOT match `usercontrollerjs` or `user-controller.js`.
908
-
909
- ### Pattern Summary Table
910
-
911
- | dirs | extensions | isNested | glob | Generated Pattern |
912
- | --------------------- | ---------------------------------- | -------- | ------------------- | ------------------------------------------------------ |
913
- | `['controllers']` | `['.controller.js']` | `true` | -- | `controllers/{**/*,*}.controller.js` |
914
- | `['controllers']` | `['.controller.js']` | `false` | -- | `controllers/*.controller.js` |
915
- | `['api', 'admin']` | `['.controller.js']` | `true` | -- | `{api,admin}/{**/*,*}.controller.js` |
916
- | `['services']` | `['.service.js', '.svc.js']` | `true` | -- | `{services}/{**/*,*}.{service.js,svc.js}` |
917
- | `['a', 'b']` | `['.x.js', '.y.js']` | `true` | -- | `{a,b}/{**/*,*}.{x.js,y.js}` |
918
- | `['a', 'b']` | `['.x.js', '.y.js']` | `false` | -- | `{a,b}/*.{x.js,y.js}` |
919
- | any | any | any | `'custom/**/*.js'` | `custom/**/*.js` |
920
-
921
- ---
922
-
923
- ## Built-in Booters Deep Dive
924
-
925
- All four built-in booters share the same structure. They differ only in their default directories, default extensions, binding namespace, and binding scope. Each one receives three constructor-injected values:
926
-
927
- | Injection Key | Type | Description |
928
- | -------------------- | -------------- | -------------------------------------------------------- |
929
- | `@app/project_root` | `string` | Absolute path to the project's build output directory |
930
- | `@app/instance` | `IApplication` | The application container instance (for binding classes) |
931
- | `@app/boot-options` | `IBootOptions` | The user's boot options object |
932
-
933
- ### ControllerBooter
934
-
935
- **Source:** `src/booters/controller.booter.ts`
936
-
937
- ```typescript
938
- export class ControllerBooter extends BaseArtifactBooter {
939
- constructor(
940
- @inject({ key: '@app/project_root' }) root: string,
941
- @inject({ key: '@app/instance' }) private readonly application: IApplication,
942
- @inject({ key: '@app/boot-options' }) bootOptions: IBootOptions,
943
- ) {
944
- super({ scope: ControllerBooter.name, root, artifactOptions: bootOptions.controllers ?? {} });
945
- }
946
-
947
- protected override getDefaultDirs(): string[] {
948
- return ['controllers'];
949
- }
950
-
951
- protected override getDefaultExtensions(): string[] {
952
- return ['.controller.js'];
953
- }
954
-
955
- protected override async bind(): Promise<void> {
956
- for (const cls of this.loadedClasses) {
957
- const key = BindingKeys.build({ namespace: 'controllers', key: cls.name });
958
- this.application.bind({ key }).toClass(cls).setTags('controllers');
959
- this.logger.for(this.bind.name).debug('Bound key: %s', key);
960
- }
961
- }
962
- }
963
- ```
964
-
965
- | Property | Value |
966
- | -------------------- | -------------------------------------------------------------- |
967
- | Default dirs | `['controllers']` |
968
- | Default extensions | `['.controller.js']` |
969
- | Namespace | `controllers` |
970
- | Binding key format | `controllers.{ClassName}` (e.g., `controllers.UserController`) |
971
- | Scope | transient (default) |
972
- | Tags | `controllers` |
973
-
974
- ### ServiceBooter
975
-
976
- **Source:** `src/booters/service.booter.ts`
977
-
978
- ```typescript
979
- export class ServiceBooter extends BaseArtifactBooter {
980
- constructor(
981
- @inject({ key: '@app/project_root' }) root: string,
982
- @inject({ key: '@app/instance' }) protected application: IApplication,
983
- @inject({ key: '@app/boot-options' }) bootOptions: IBootOptions,
984
- ) {
985
- super({ scope: ServiceBooter.name, root, artifactOptions: bootOptions.services ?? {} });
986
- }
987
-
988
- protected override getDefaultDirs(): string[] {
989
- return ['services'];
990
- }
991
-
992
- protected override getDefaultExtensions(): string[] {
993
- return ['.service.js'];
994
- }
995
-
996
- protected override async bind(): Promise<void> {
997
- for (const cls of this.loadedClasses) {
998
- const key = BindingKeys.build({ namespace: 'services', key: cls.name });
999
- this.application.bind({ key }).toClass(cls).setTags('services');
1000
- this.logger.for(this.bind.name).debug('Bound key: %s', key);
1001
- }
1002
- }
1003
- }
1004
- ```
1005
-
1006
- | Property | Value |
1007
- | -------------------- | ---------------------------------------------------------- |
1008
- | Default dirs | `['services']` |
1009
- | Default extensions | `['.service.js']` |
1010
- | Namespace | `services` |
1011
- | Binding key format | `services.{ClassName}` (e.g., `services.AuthService`) |
1012
- | Scope | transient (default) |
1013
- | Tags | `services` |
1014
-
1015
- ### RepositoryBooter
1016
-
1017
- **Source:** `src/booters/repository.booter.ts`
1018
-
1019
- ```typescript
1020
- export class RepositoryBooter extends BaseArtifactBooter {
1021
- constructor(
1022
- @inject({ key: '@app/project_root' }) root: string,
1023
- @inject({ key: '@app/instance' }) protected application: IApplication,
1024
- @inject({ key: '@app/boot-options' }) bootOptions: IBootOptions,
1025
- ) {
1026
- super({ scope: RepositoryBooter.name, root, artifactOptions: bootOptions.repositories ?? {} });
1027
- }
1028
-
1029
- protected override getDefaultDirs(): string[] {
1030
- return ['repositories'];
1031
- }
1032
-
1033
- protected override getDefaultExtensions(): string[] {
1034
- return ['.repository.js'];
1035
- }
1036
-
1037
- protected override async bind(): Promise<void> {
1038
- for (const cls of this.loadedClasses) {
1039
- const key = BindingKeys.build({ namespace: 'repositories', key: cls.name });
1040
- this.application.bind({ key }).toClass(cls).setTags('repositories');
1041
- this.logger.for(this.bind.name).debug('Bound key: %s', key);
1042
- }
1043
- }
1044
- }
1045
- ```
1046
-
1047
- | Property | Value |
1048
- | -------------------- | ---------------------------------------------------------------------- |
1049
- | Default dirs | `['repositories']` |
1050
- | Default extensions | `['.repository.js']` |
1051
- | Namespace | `repositories` |
1052
- | Binding key format | `repositories.{ClassName}` (e.g., `repositories.UserRepository`) |
1053
- | Scope | transient (default) |
1054
- | Tags | `repositories` |
1055
-
1056
- ### DatasourceBooter
1057
-
1058
- **Source:** `src/booters/datasource.booter.ts`
1059
-
1060
- ```typescript
1061
- export class DatasourceBooter extends BaseArtifactBooter {
1062
- constructor(
1063
- @inject({ key: '@app/project_root' }) root: string,
1064
- @inject({ key: '@app/instance' }) private readonly application: IApplication,
1065
- @inject({ key: '@app/boot-options' }) bootOptions: IBootOptions,
1066
- ) {
1067
- super({ scope: DatasourceBooter.name, root, artifactOptions: bootOptions.datasources ?? {} });
1068
- }
1069
-
1070
- protected override getDefaultDirs(): string[] {
1071
- return ['datasources'];
1072
- }
1073
-
1074
- protected override getDefaultExtensions(): string[] {
1075
- return ['.datasource.js'];
1076
- }
1077
-
1078
- protected override async bind(): Promise<void> {
1079
- for (const cls of this.loadedClasses) {
1080
- const key = BindingKeys.build({ namespace: 'datasources', key: cls.name });
1081
- this.application.bind({ key }).toClass(cls).setTags('datasources').setScope('singleton');
1082
- this.logger.for(this.bind.name).debug('Bound key: %s', key);
1083
- }
1084
- }
1085
- }
1086
- ```
1087
-
1088
- | Property | Value |
1089
- | -------------------- | -------------------------------------------------------------------------- |
1090
- | Default dirs | `['datasources']` |
1091
- | Default extensions | `['.datasource.js']` |
1092
- | Namespace | `datasources` |
1093
- | Binding key format | `datasources.{ClassName}` (e.g., `datasources.PostgresDataSource`) |
1094
- | Scope | **singleton** |
1095
- | Tags | `datasources` |
1096
-
1097
- ### Why Are Datasources Singletons?
1098
-
1099
- The `DatasourceBooter` is the only built-in booter that sets `.setScope('singleton')` on its bindings. This is a critical design decision:
1100
-
1101
- 1. **Connection pooling.** A datasource typically creates a connection pool to the database (e.g., a `pg.Pool` with 10-20 connections). Creating a new pool per injection would exhaust database connections under load.
1102
-
1103
- 2. **Resource sharing.** Multiple repositories share the same datasource instance. If `UserRepository` and `OrderRepository` both reference `PostgresDataSource`, they share one connection pool rather than each managing their own.
1104
-
1105
- 3. **Schema discovery.** Datasources use `discoverSchema()` to collect all table schemas from their associated repositories. This discovery runs once during startup and caches the result. Multiple instances would repeat this work needlessly.
1106
-
1107
- 4. **Transaction coordination.** When two repositories participate in the same transaction, they must share the same underlying connection. Singleton datasources make this possible.
1108
-
1109
- All other booters use transient scope because controllers, services, and repositories are lightweight wrappers with no expensive resources to share. They are instantiated fresh for each container resolution.
1110
-
1111
- ---
1112
-
1113
- ## BootMixin Deep Dive
1114
-
1115
- **Source:** `src/boot.mixin.ts`
1116
-
1117
- The full source of `BootMixin`:
1118
-
1119
- ```typescript
1120
- import {
1121
- Bootstrapper,
1122
- ControllerBooter,
1123
- DatasourceBooter,
1124
- IBootableApplication,
1125
- IBootOptions,
1126
- IBootReport,
1127
- RepositoryBooter,
1128
- ServiceBooter,
1129
- } from '@venizia/ignis-boot';
1130
- import { TMixinTarget } from '@venizia/ignis-helpers';
1131
- import { BindingScopes, Container } from '@venizia/ignis-inversion';
1132
-
1133
- export const BootMixin = <T extends TMixinTarget<Container>>(baseClass: T) => {
1134
- class Mixed extends baseClass implements IBootableApplication {
1135
- constructor(...args: any[]) {
1136
- super(...args);
1137
-
1138
- this.bind({ key: `@app/boot-options` }).toValue(this.bootOptions ?? {});
1139
-
1140
- this.bind({ key: 'booter.DatasourceBooter' }).toClass(DatasourceBooter).setTags('booter');
1141
- this.bind({ key: 'booter.RepositoryBooter' }).toClass(RepositoryBooter).setTags('booter');
1142
- this.bind({ key: 'booter.ServiceBooter' }).toClass(ServiceBooter).setTags('booter');
1143
- this.bind({ key: 'booter.ControllerBooter' }).toClass(ControllerBooter).setTags('booter');
1144
-
1145
- this.bind({ key: 'bootstrapper' }).toClass(Bootstrapper).setScope(BindingScopes.SINGLETON);
1146
- }
1147
-
1148
- bootOptions?: IBootOptions | undefined;
1149
-
1150
- boot(): Promise<IBootReport> {
1151
- const bootstrapper = this.get<Bootstrapper>({ key: 'bootstrapper' });
1152
- return bootstrapper.boot({});
1153
- }
1154
- }
1155
-
1156
- return Mixed;
1157
- };
1158
- ```
1159
-
1160
- ### Constructor Internals
1161
-
1162
- When the `BootMixin` constructor runs, it performs exactly 6 binding registrations in this order:
1163
-
1164
- **1. Boot options binding:**
1165
-
1166
- ```typescript
1167
- this.bind({ key: '@app/boot-options' }).toValue(this.bootOptions ?? {});
1168
- ```
1169
-
1170
- Binds the user's `bootOptions` object (or `{}` if undefined) as a plain value. This is injected into every booter's constructor.
1171
-
1172
- **2-5. Booter class registrations (in dependency order):**
1173
-
1174
- ```typescript
1175
- this.bind({ key: 'booter.DatasourceBooter' }).toClass(DatasourceBooter).setTags('booter');
1176
- this.bind({ key: 'booter.RepositoryBooter' }).toClass(RepositoryBooter).setTags('booter');
1177
- this.bind({ key: 'booter.ServiceBooter' }).toClass(ServiceBooter).setTags('booter');
1178
- this.bind({ key: 'booter.ControllerBooter' }).toClass(ControllerBooter).setTags('booter');
1179
- ```
1180
-
1181
- Each booter is:
1182
-
1183
- - Bound as a **class** (not an instance) -- the container instantiates it on resolution with constructor injection
1184
- - Tagged with `'booter'` -- this is how the `Bootstrapper` discovers them via `findByTag({ tag: 'booter' })`
1185
- - Registered in **dependency order** -- datasources before repositories before services before controllers
1186
-
1187
- The registration order determines execution order within each phase. This ensures that during the LOAD phase, datasources are bound to the container before repositories try to resolve them.
1188
-
1189
- **6. Bootstrapper singleton:**
1190
-
1191
- ```typescript
1192
- this.bind({ key: 'bootstrapper' }).toClass(Bootstrapper).setScope(BindingScopes.SINGLETON);
1193
- ```
1194
-
1195
- The `Bootstrapper` is a singleton because it maintains internal state (the booter list, phase timings). Multiple `boot()` calls resolve the same `Bootstrapper` instance.
1196
-
1197
- ### boot() Method
1198
-
1199
- ```typescript
1200
- boot(): Promise<IBootReport> {
1201
- const bootstrapper = this.get<Bootstrapper>({ key: 'bootstrapper' });
1202
- return bootstrapper.boot({});
1203
- }
1204
- ```
1205
-
1206
- The `boot()` method:
1207
-
1208
- 1. Resolves the `Bootstrapper` singleton from the container (which triggers `@inject({ key: '@app/instance' })` constructor injection)
1209
- 2. Calls `bootstrapper.boot({})` with an empty options object, which means all phases run on all booters
1210
- 3. Returns the `IBootReport` promise
1211
-
1212
- ---
1213
-
1214
- ## Bootstrapper Internals
1215
-
1216
- **Source:** `src/bootstrapper.ts`
1217
-
1218
- ### Full Source
1219
-
1220
- ```typescript
1221
- export class Bootstrapper extends BaseHelper implements IBootstrapper {
1222
- private booters: IBooter[] = [];
1223
- private phaseStartTimings: Map<string, number> = new Map();
1224
- private phaseEndTimings: Map<string, number> = new Map();
1225
-
1226
- constructor(@inject({ key: '@app/instance' }) private readonly application: IApplication) {
1227
- super({ scope: Bootstrapper.name });
1228
- }
1229
-
1230
- async boot(opts: IBootExecutionOptions): Promise<IBootReport> {
1231
- const { phases = BOOT_PHASES, booters } = opts;
1232
-
1233
- await this.discoverBooters();
1234
- this.logger
1235
- .for(this.boot.name)
1236
- .debug(`Starting boot | Number of booters: %d`, this.booters.length);
1237
- for (const phase of phases) {
1238
- await this.runPhase({ phase, booterNames: booters });
1239
- }
1240
- return this.generateReport();
1241
- }
1242
-
1243
- // ...
1244
- }
1245
- ```
1246
-
1247
- ### discoverBooters()
1248
-
1249
- ```typescript
1250
- private async discoverBooters(): Promise<void> {
1251
- const booterBindings = this.application.findByTag<IBooter>({ tag: 'booter' });
1252
-
1253
- for (const binding of booterBindings) {
1254
- this.booters.push(binding.getValue(this.application));
1255
- this.logger.for(this.discoverBooters.name).debug(`Discovered booter: %s`, binding.key);
1256
- }
1257
- }
1258
- ```
1259
-
1260
- This method:
1261
-
1262
- 1. Calls `this.application.findByTag({ tag: 'booter' })` which returns all `Binding` objects that have the `'booter'` tag
1263
- 2. For each binding, calls `binding.getValue(this.application)` which:
1264
- - Instantiates the booter class (since booters are bound via `.toClass()`)
1265
- - Performs constructor injection, resolving `@app/project_root`, `@app/instance`, and `@app/boot-options` from the container
1266
- 3. Pushes each instantiated booter into the `this.booters` array
1267
- 4. Logs the binding key for each discovered booter
1268
-
1269
- The order of `booterBindings` matches the order they were registered in the container. Since `BootMixin` registers them in the order Datasource -> Repository -> Service -> Controller, that is the execution order.
1270
-
1271
- ### runPhase()
1272
-
1273
- ```typescript
1274
- private async runPhase(opts: { phase: TBootPhase; booterNames?: string[] }): Promise<void> {
1275
- const { phase } = opts;
1276
- this.phaseStartTimings.set(phase, performance.now());
1277
- this.logger.for(this.runPhase.name).debug(`Starting phase: %s`, phase.toUpperCase());
1278
-
1279
- for (const booter of this.booters) {
1280
- const phaseMethod = booter[phase];
1281
- if (!phaseMethod) {
1282
- this.logger
1283
- .for(this.runPhase.name)
1284
- .debug(
1285
- `SKIP not implemented booter | Phase: %s | Booter: %s`,
1286
- phase,
1287
- booter.constructor.name,
1288
- );
1289
- continue;
1290
- }
1291
- if (typeof phaseMethod !== 'function') {
1292
- this.logger
1293
- .for(this.runPhase.name)
1294
- .debug(
1295
- `SKIP not a function booter | Phase: %s | Booter: %s`,
1296
- phase,
1297
- booter.constructor.name,
1298
- );
1299
- continue;
1300
- }
1301
-
1302
- try {
1303
- this.logger
1304
- .for(this.runPhase.name)
1305
- .debug(`Running | Phase: %s | Booter: %s`, phase, booter.constructor.name);
1306
- await phaseMethod.call(booter);
1307
- } catch (error) {
1308
- const errorMessage = (error as Error)?.message || String(error);
1309
-
1310
- throw getError({
1311
- message: `[Bootstrapper][runPhase] Error during phase '${phase}' on booter '${booter.constructor.name}': ${errorMessage}`,
1312
- });
1313
- }
1314
- }
1315
-
1316
- this.phaseEndTimings.set(phase, performance.now());
1317
- const start = this.phaseStartTimings.get(phase) ?? 0;
1318
- const end = this.phaseEndTimings.get(phase) ?? 0;
1319
- const duration = end - start;
1320
-
1321
- this.logger
1322
- .for(this.runPhase.name)
1323
- .debug(`Completed phase: %s | Took: %d ms`, phase.toUpperCase(), duration);
1324
- }
1325
- ```
1326
-
1327
- **Key behaviors:**
1328
-
1329
- 1. **Phase method lookup:** For each booter, accesses `booter[phase]` (e.g., `booter['configure']`). This is a dynamic property lookup using the phase string as a key.
1330
-
1331
- 2. **Guard checks:** Two guards skip booters that do not implement the phase:
1332
- - If `phaseMethod` is falsy (undefined/null), the booter is skipped with a debug log
1333
- - If `phaseMethod` is not a function, the booter is skipped with a debug log
1334
- - This allows custom booters to omit phases they do not need
1335
-
1336
- 3. **Method invocation:** Uses `phaseMethod.call(booter)` to ensure the correct `this` context when calling the method.
1337
-
1338
- 4. **Sequential execution:** Booters within a phase are executed sequentially (not in parallel). This guarantees ordering -- datasource bindings are available before repository loading begins.
1339
-
1340
- 5. **booterNames filtering:** The `opts.booterNames` parameter is accepted but not yet implemented (marked with a TODO comment). Future versions will allow running specific booters by name.
1341
-
1342
- ### Error Wrapping
1343
-
1344
- When a phase method throws an error, the `Bootstrapper` catches it and wraps it with context:
1345
-
1346
- ```typescript
1347
- throw getError({
1348
- message: `[Bootstrapper][runPhase] Error during phase '${phase}' on booter '${booter.constructor.name}': ${errorMessage}`,
1349
- });
1350
- ```
1351
-
1352
- Example error message:
1353
-
1354
- ```
1355
- [Bootstrapper][runPhase] Error during phase 'discover' on booter 'ControllerBooter': [discover] Failed to discover files using pattern: controllers/{**/*,*}.controller.js | Error: ENOENT: no such file or directory
1356
- ```
1357
-
1358
- This multi-layer error wrapping makes it clear:
1359
-
1360
- 1. Which component threw (`Bootstrapper`)
1361
- 2. Which phase was running (`discover`)
1362
- 3. Which booter failed (`ControllerBooter`)
1363
- 4. What the original error was
1364
-
1365
- ### Performance Timing
1366
-
1367
- The `Bootstrapper` uses two `Map<string, number>` instances to track timing:
1368
-
1369
- ```typescript
1370
- private phaseStartTimings: Map<string, number> = new Map();
1371
- private phaseEndTimings: Map<string, number> = new Map();
1372
- ```
1373
-
1374
- At the start of each phase, `performance.now()` is recorded. At the end, another timestamp is captured and the duration is logged:
1375
-
1376
- ```
1377
- [Bootstrapper][runPhase] Completed phase: CONFIGURE | Took: 0.42 ms
1378
- [Bootstrapper][runPhase] Completed phase: DISCOVER | Took: 12.8 ms
1379
- [Bootstrapper][runPhase] Completed phase: LOAD | Took: 45.3 ms
1380
- ```
1381
-
1382
- These timings are currently logged but not included in the `IBootReport`. The data is available in the `Bootstrapper` instance for future use.
1383
-
1384
- ### IBootReport Structure
1385
-
1386
- ```typescript
1387
- export interface IBootReport {}
1388
- ```
1389
-
1390
- The current `IBootReport` interface is intentionally empty. The `generateReport()` method returns `{}`:
1391
-
1392
- ```typescript
1393
- private generateReport(): IBootReport {
1394
- const report: IBootReport = {};
1395
- this.logger.for(this.generateReport.name).debug(`Boot report: %j`, report);
1396
- return report;
1397
- }
1398
- ```
1399
-
1400
- This is a deliberate extension point. Future versions can add fields like:
1401
-
1402
- ```typescript
1403
- // Potential future shape (not yet implemented)
1404
- interface IBootReport {
1405
- phases?: {
1406
- configure?: { startTime: number; endTime: number; duration: number };
1407
- discover?: { startTime: number; endTime: number; duration: number };
1408
- load?: { startTime: number; endTime: number; duration: number };
1409
- };
1410
- booters?: Array<{
1411
- name: string;
1412
- discoveredFiles: number;
1413
- loadedClasses: number;
1414
- }>;
1415
- totalDuration?: number;
1416
- }
1417
- ```
1418
-
1419
- ---
1420
-
1421
- ## Advanced Usage
1422
-
1423
- ### Creating Custom Booters
1424
-
1425
- To support a new artifact type, extend `BaseArtifactBooter` and implement the three abstract methods:
1426
-
1427
- ```typescript
1428
- import { BaseArtifactBooter, IApplication, IBootOptions } from '@venizia/ignis-boot';
1429
- import { BindingKeys, inject } from '@venizia/ignis-inversion';
1430
-
1431
- export class HandlerBooter extends BaseArtifactBooter {
1432
- constructor(
1433
- @inject({ key: '@app/project_root' }) root: string,
1434
- @inject({ key: '@app/instance' }) private readonly application: IApplication,
1435
- @inject({ key: '@app/boot-options' }) bootOptions: IBootOptions,
1436
- ) {
1437
- super({
1438
- scope: HandlerBooter.name,
1439
- root,
1440
- artifactOptions: bootOptions['handlers'] ?? {},
1441
- });
1442
- }
1443
-
1444
- protected override getDefaultDirs(): string[] {
1445
- return ['handlers'];
1446
- }
1447
-
1448
- protected override getDefaultExtensions(): string[] {
1449
- return ['.handler.js'];
1450
- }
1451
-
1452
- protected override async bind(): Promise<void> {
1453
- for (const cls of this.loadedClasses) {
1454
- const key = BindingKeys.build({ namespace: 'handlers', key: cls.name });
1455
- this.application.bind({ key }).toClass(cls).setTags('handlers');
1456
- }
1457
- }
1458
- }
1459
- ```
1460
-
1461
- **Register the custom booter** in your application before calling `boot()`:
1462
-
1463
- ```typescript
1464
- class MyApp extends BootMixin(Container) {
1465
- bootOptions: IBootOptions = {
1466
- controllers: { dirs: ['controllers'] },
1467
- handlers: { dirs: ['handlers'], isNested: true },
1468
- };
1469
-
1470
- constructor() {
1471
- super();
1472
-
1473
- // Register custom booter with the 'booter' tag so Bootstrapper discovers it
1474
- this.bind({ key: 'booter.HandlerBooter' })
1475
- .toClass(HandlerBooter)
1476
- .setTags('booter');
1477
- }
1478
- }
1479
- ```
1480
-
1481
- The `'booter'` tag is critical -- the `Bootstrapper` uses `findByTag({ tag: 'booter' })` to discover all booters at runtime.
1482
-
1483
- ### Multiple Custom Booter Examples
1484
-
1485
- #### Example 1: MiddlewareBooter -- Auto-discover Middleware
1486
-
1487
- Discover and register Hono middleware files automatically:
1488
-
1489
- ```typescript
1490
- import { BaseArtifactBooter, IApplication, IBootOptions } from '@venizia/ignis-boot';
1491
- import { BindingKeys, inject } from '@venizia/ignis-inversion';
1492
-
1493
- export class MiddlewareBooter extends BaseArtifactBooter {
1494
- constructor(
1495
- @inject({ key: '@app/project_root' }) root: string,
1496
- @inject({ key: '@app/instance' }) private readonly application: IApplication,
1497
- @inject({ key: '@app/boot-options' }) bootOptions: IBootOptions,
1498
- ) {
1499
- super({
1500
- scope: MiddlewareBooter.name,
1501
- root,
1502
- artifactOptions: bootOptions['middlewares'] ?? {},
1503
- });
1504
- }
1505
-
1506
- protected override getDefaultDirs(): string[] {
1507
- return ['middlewares'];
1508
- }
1509
-
1510
- protected override getDefaultExtensions(): string[] {
1511
- return ['.middleware.js'];
1512
- }
1513
-
1514
- protected override async bind(): Promise<void> {
1515
- for (const cls of this.loadedClasses) {
1516
- const key = BindingKeys.build({ namespace: 'middlewares', key: cls.name });
1517
- // Middlewares are singletons -- they are stateless functions typically
1518
- this.application.bind({ key }).toClass(cls).setTags('middlewares').setScope('singleton');
1519
- this.logger.for(this.bind.name).debug('Bound middleware: %s', key);
1520
- }
1521
- }
1522
- }
1523
- ```
1524
-
1525
- **Registration:**
1526
-
1527
- ```typescript
1528
- class MyApp extends BootMixin(Container) {
1529
- bootOptions: IBootOptions = {
1530
- middlewares: { dirs: ['middlewares'], isNested: false },
1531
- controllers: { dirs: ['controllers'] },
1532
- };
1533
-
1534
- constructor() {
1535
- super();
1536
- this.bind({ key: 'booter.MiddlewareBooter' }).toClass(MiddlewareBooter).setTags('booter');
1537
- }
1538
- }
1539
- ```
1540
-
1541
- #### Example 2: MigrationBooter -- Auto-discover Migration Files
1542
-
1543
- Discover Drizzle migration files for programmatic migration execution:
1544
-
1545
- ```typescript
1546
- import { BaseArtifactBooter, IApplication, IBootOptions } from '@venizia/ignis-boot';
1547
- import { BindingKeys, inject } from '@venizia/ignis-inversion';
1548
-
1549
- export class MigrationBooter extends BaseArtifactBooter {
1550
- constructor(
1551
- @inject({ key: '@app/project_root' }) root: string,
1552
- @inject({ key: '@app/instance' }) private readonly application: IApplication,
1553
- @inject({ key: '@app/boot-options' }) bootOptions: IBootOptions,
1554
- ) {
1555
- super({
1556
- scope: MigrationBooter.name,
1557
- root,
1558
- artifactOptions: bootOptions['migrations'] ?? {},
1559
- });
1560
- }
1561
-
1562
- protected override getDefaultDirs(): string[] {
1563
- return ['migrations'];
1564
- }
1565
-
1566
- protected override getDefaultExtensions(): string[] {
1567
- return ['.migration.js'];
1568
- }
1569
-
1570
- protected override async bind(): Promise<void> {
1571
- for (const cls of this.loadedClasses) {
1572
- const key = BindingKeys.build({ namespace: 'migrations', key: cls.name });
1573
- // Migrations are transient -- each resolution creates a fresh instance
1574
- this.application.bind({ key }).toClass(cls).setTags('migrations');
1575
- this.logger.for(this.bind.name).debug('Bound migration: %s', key);
1576
- }
1577
- }
1578
- }
1579
- ```
1580
-
1581
- **Registration with custom extensions:**
1582
-
1583
- ```typescript
1584
- class MyApp extends BootMixin(Container) {
1585
- bootOptions: IBootOptions = {
1586
- controllers: { dirs: ['controllers'] },
1587
- migrations: {
1588
- dirs: ['migrations'],
1589
- extensions: ['.migration.js'],
1590
- isNested: false, // Migrations are usually flat
1591
- },
1592
- };
1593
-
1594
- constructor() {
1595
- super();
1596
- this.bind({ key: 'booter.MigrationBooter' }).toClass(MigrationBooter).setTags('booter');
1597
- }
1598
- }
1599
- ```
1600
-
1601
- #### Example 3: CronJobBooter -- Auto-discover Scheduled Tasks
1602
-
1603
- ```typescript
1604
- import { BaseArtifactBooter, IApplication, IBootOptions } from '@venizia/ignis-boot';
1605
- import { BindingKeys, inject } from '@venizia/ignis-inversion';
1606
-
1607
- export class CronJobBooter extends BaseArtifactBooter {
1608
- constructor(
1609
- @inject({ key: '@app/project_root' }) root: string,
1610
- @inject({ key: '@app/instance' }) private readonly application: IApplication,
1611
- @inject({ key: '@app/boot-options' }) bootOptions: IBootOptions,
1612
- ) {
1613
- super({
1614
- scope: CronJobBooter.name,
1615
- root,
1616
- artifactOptions: bootOptions['crons'] ?? {},
1617
- });
1618
- }
1619
-
1620
- protected override getDefaultDirs(): string[] {
1621
- return ['crons'];
1622
- }
1623
-
1624
- protected override getDefaultExtensions(): string[] {
1625
- return ['.cron.js'];
1626
- }
1627
-
1628
- protected override async bind(): Promise<void> {
1629
- for (const cls of this.loadedClasses) {
1630
- const key = BindingKeys.build({ namespace: 'crons', key: cls.name });
1631
- // Cron jobs are singletons -- they maintain internal timer state
1632
- this.application.bind({ key }).toClass(cls).setTags('crons').setScope('singleton');
1633
- this.logger.for(this.bind.name).debug('Bound cron job: %s', key);
1634
- }
1635
- }
1636
- }
1637
- ```
1638
-
1639
- ### Custom Glob Patterns
1640
-
1641
- When the default pattern generation does not fit your project structure, use the `glob` option to take full control:
1642
-
1643
- ```typescript
1644
- const bootOptions: IBootOptions = {
1645
- controllers: {
1646
- // Scan multiple top-level directories with specific depth
1647
- glob: '{api,admin}/**/controllers/*.controller.js',
1648
- },
1649
- services: {
1650
- // Only scan a specific subdirectory
1651
- glob: 'modules/**/services/*.service.js',
1652
- },
1653
- };
1654
- ```
1655
-
1656
- When `glob` is set, `dirs`, `extensions`, and `isNested` are all ignored.
1657
-
1658
- ### Boot Options Override Examples
1659
-
1660
- Each artifact type's options can be customized independently. Here are common override patterns:
1661
-
1662
- #### Override directories only
1663
-
1664
- ```typescript
1665
- const bootOptions: IBootOptions = {
1666
- controllers: {
1667
- dirs: ['api-controllers', 'admin-controllers'],
1668
- // extensions and isNested use defaults
1669
- },
1670
- };
1671
- ```
1672
-
1673
- Generated pattern: `{api-controllers,admin-controllers}/{**/*,*}.controller.js`
1674
-
1675
- #### Override extensions only
1676
-
1677
- ```typescript
1678
- const bootOptions: IBootOptions = {
1679
- services: {
1680
- extensions: ['.service.js', '.provider.js'],
1681
- // dirs uses default ['services'], isNested uses default true
1682
- },
1683
- };
1684
- ```
1685
-
1686
- Generated pattern: `{services}/{**/*,*}.{service.js,provider.js}`
1687
-
1688
- #### Disable nested scanning
1689
-
1690
- ```typescript
1691
- const bootOptions: IBootOptions = {
1692
- repositories: {
1693
- isNested: false,
1694
- // Only discover repositories at the root level, no subdirectories
1695
- },
1696
- };
1697
- ```
1698
-
1699
- Generated pattern: `repositories/*.repository.js`
1700
-
1701
- #### Full override with custom glob
1702
-
1703
- ```typescript
1704
- const bootOptions: IBootOptions = {
1705
- datasources: {
1706
- glob: 'config/datasources/*.datasource.js',
1707
- // dirs, extensions, isNested are all ignored when glob is set
1708
- },
1709
- };
1710
- ```
1711
-
1712
- Generated pattern: `config/datasources/*.datasource.js`
1713
-
1714
- #### Mixed overrides across artifact types
1715
-
1716
- ```typescript
1717
- const bootOptions: IBootOptions = {
1718
- controllers: {
1719
- dirs: ['controllers'],
1720
- isNested: true, // Deep scan for nested module controllers
1721
- },
1722
- services: {
1723
- dirs: ['services', 'providers'],
1724
- extensions: ['.service.js', '.provider.js'],
1725
- },
1726
- repositories: {
1727
- isNested: false, // Flat repository directory
1728
- },
1729
- datasources: {
1730
- dirs: ['datasources'],
1731
- // Defaults are fine -- singleton scope is set by the DatasourceBooter
1732
- },
1733
- };
1734
- ```
1735
-
1736
- ### Selective Phase Execution
1737
-
1738
- The `Bootstrapper.boot()` method accepts an `IBootExecutionOptions` object that lets you run only specific phases:
1739
-
1740
- ```typescript
1741
- const bootstrapper = app.get<Bootstrapper>({ key: 'bootstrapper' });
1742
-
1743
- // Run only configure and discover (skip loading/binding)
1744
- const report = await bootstrapper.boot({
1745
- phases: ['configure', 'discover'],
1746
- });
1747
-
1748
- // Run all phases (default)
1749
- const fullReport = await bootstrapper.boot({});
1750
- ```
1751
-
1752
- This is useful for testing scenarios where you want to verify discovery results before loading, or for dry-run analysis of what would be discovered.
1753
-
1754
- ### Boot Report and Performance Timing
1755
-
1756
- The `Bootstrapper` tracks `performance.now()` timestamps for the start and end of each phase. Phase timing is logged at debug level:
1757
-
1758
- ```
1759
- [Bootstrapper][runPhase] Completed phase: CONFIGURE | Took: 0.42 ms
1760
- [Bootstrapper][runPhase] Completed phase: DISCOVER | Took: 12.8 ms
1761
- [Bootstrapper][runPhase] Completed phase: LOAD | Took: 45.3 ms
1762
- ```
1763
-
1764
- The `boot()` method returns an `IBootReport` object, which can be extended in future versions to include detailed timing and discovery statistics.
1765
-
1766
- ---
1767
-
1768
- ## Integration with BaseApplication
1769
-
1770
- When using `@venizia/ignis` (the core framework), `BaseApplication` does not use `BootMixin` directly. Instead, it implements the same logic with additional framework-specific behavior:
1771
-
1772
- ```typescript
1773
- // In BaseApplication (packages/core/src/base/applications/base.ts)
1774
-
1775
- booter<Base extends IBooter, Args extends AnyObject = any>(
1776
- ctor: TClass<Base>,
1777
- opts?: TMixinOpts<Args>,
1778
- ): Binding<Base> {
1779
- return this.bind<Base>({
1780
- key: BindingKeys.build(
1781
- opts?.binding ?? { namespace: BindingNamespaces.BOOTERS, key: ctor.name },
1782
- ),
1783
- })
1784
- .toClass(ctor)
1785
- .setTags('booter');
1786
- }
1787
-
1788
- async registerBooters() {
1789
- await executeWithPerformanceMeasure({
1790
- logger: this.logger,
1791
- scope: this.registerDataSources.name,
1792
- description: 'Register application data sources',
1793
- task: async () => {
1794
- this.bind({ key: '@app/boot-options' }).toValue(this.configs.bootOptions ?? {});
1795
- this.bind({ key: 'bootstrapper' }).toClass(Bootstrapper).setScope(BindingScopes.SINGLETON);
1796
-
1797
- // Define default booters
1798
- this.booter(DatasourceBooter);
1799
- this.booter(RepositoryBooter);
1800
- this.booter(ServiceBooter);
1801
- this.booter(ControllerBooter);
1802
- },
1803
- });
1804
- }
1805
-
1806
- async boot(): Promise<IBootReport> {
1807
- await this.registerBooters();
1808
-
1809
- const bootstrapper = this.get<Bootstrapper>({ key: 'bootstrapper' });
1810
-
1811
- return bootstrapper.boot({});
1812
- }
1813
- ```
1814
-
1815
- **Key differences from BootMixin:**
1816
-
1817
- 1. **Namespace-aware binding keys:** `BaseApplication` uses `BindingNamespaces.BOOTERS` (which resolves to `"booters"`) as the namespace, producing keys like `booters.DatasourceBooter` instead of `booter.DatasourceBooter`. This is consistent with the core framework's namespace convention.
1818
-
1819
- 2. **Helper method:** The `booter()` method provides a shorthand for registering booters with the correct namespace and tag.
1820
-
1821
- 3. **Performance measurement:** `registerBooters()` is wrapped in `executeWithPerformanceMeasure()` which logs execution time.
1822
-
1823
- 4. **Deferred registration:** In `BaseApplication`, booter registration happens during the `initialize()` lifecycle, not in the constructor. This allows the application to complete its own setup before boot runs.
1824
-
1825
- 5. **Custom booter registration:** You can call `this.booter(MyCustomBooter)` in `preConfigure()` to register additional booters before boot runs:
1826
-
1827
- ```typescript
1828
- class MyApplication extends BaseApplication {
1829
- async preConfigure() {
1830
- // Register custom booters alongside the defaults
1831
- this.booter(HandlerBooter);
1832
- this.booter(CronJobBooter);
1833
- }
1834
- }
1835
- ```
1836
-
1837
- **Full application lifecycle with boot:**
1838
-
1839
- ```
1840
- 1. constructor() --> Bind app configs, project root, etc.
1841
- 2. initialize() --> Called by start()
1842
- 2a. registerDefaultMiddlewares()
1843
- 2b. staticConfigure()
1844
- 2c. preConfigure() --> User registers controllers/services/components
1845
- 2d. registerBooters() --> Registers booter bindings
1846
- 2e. boot() --> Discovers and loads all artifacts
1847
- 2f. registerDataSources() --> Configures datasources with discovered schemas
1848
- 2g. registerComponents() --> Initializes components
1849
- 2h. registerControllers() --> Mounts controller routes to Hono
1850
- 2i. postConfigure() --> User post-setup hooks
1851
- 2j. setupMiddlewares() --> User middleware registration
1852
- 3. start() --> Start HTTP server (Bun.serve or @hono/node-server)
1853
- ```
1854
-
1855
- ---
1856
-
1857
- ## Error Scenarios
1858
-
1859
- ### No files found (empty discoveredFiles)
1860
-
1861
- If a booter's glob pattern matches no files, `discoveredFiles` will be an empty array. This is **not an error**. The `load()` method handles it gracefully:
1862
-
1863
- ```typescript
1864
- async load(): Promise<void> {
1865
- if (!this.discoveredFiles.length) {
1866
- this.logger.for(this.load.name).debug(`No files discovered to load.`);
1867
- return;
1868
- }
1869
- // ...
1870
- }
1871
- ```
1872
-
1873
- **Debug output:**
1874
-
1875
- ```
1876
- [ServiceBooter][discover] Root: /app/dist | Using pattern: services/{**/*,*}.service.js | Discovered file: []
1877
- [ServiceBooter][load] No files discovered to load.
1878
- ```
1879
-
1880
- This is common during early development when you may not yet have files for every artifact type.
1881
-
1882
- ### File has no class exports
1883
-
1884
- If a discovered file exports only non-class values (arrow functions, constants, objects, primitives), `loadClasses()` will filter them all out. The `loadedClasses` array will be empty, and `bind()` will execute but do nothing (it iterates an empty array).
1885
-
1886
- **Example file** (`non.repository.ts`):
1887
-
1888
- ```typescript
1889
- // LET THIS FILE BE EMPTY
1890
- ```
1891
-
1892
- Or a file that exports only constants:
1893
-
1894
- ```typescript
1895
- export const CONFIG = { host: 'localhost' };
1896
- export const helper = () => 'helper';
1897
- ```
1898
-
1899
- Both result in zero loaded classes -- no error, no binding.
1900
-
1901
- ### Glob pattern matches wrong files
1902
-
1903
- If your glob pattern is too broad (e.g., `**/*.js`), you may discover files that are not artifact classes. The `isClass()` check prevents non-class exports from being bound, but unexpected classes could still be registered.
1904
-
1905
- **Prevention strategies:**
1906
-
1907
- - Use specific file extensions: `.controller.js`, `.service.js`
1908
- - Use dedicated directories: `controllers/`, `services/`
1909
- - Avoid overly broad custom globs
1910
- - Test discovery results using selective phase execution (see [Selective Phase Execution](#selective-phase-execution))
1911
-
1912
- ### No directories specified
1913
-
1914
- If `configure()` runs and neither the user options nor the defaults provide `dirs`, `getPattern()` will throw:
1915
-
1916
- ```
1917
- Error: [getPattern] No directories specified for artifact discovery
1918
- ```
1919
-
1920
- This only happens if a custom booter's `getDefaultDirs()` returns an empty array AND the user does not provide `dirs`.
1921
-
1922
- ### No extensions specified
1923
-
1924
- If `configure()` runs and neither the user options nor the defaults provide `extensions`, `getPattern()` will throw:
1925
-
1926
- ```
1927
- Error: [ControllerBooter][getPattern] No file extensions specified for artifact discovery
1928
- ```
1929
-
1930
- This only happens if a custom booter's `getDefaultExtensions()` returns an empty array AND the user does not provide `extensions`.
1931
-
1932
- ### Phase execution fails
1933
-
1934
- If any booter's phase method throws, the `Bootstrapper` catches the error, wraps it with context, and re-throws. This stops the entire boot process:
1935
-
1936
- ```
1937
- Error: [Bootstrapper][runPhase] Error during phase 'load' on booter 'RepositoryBooter': [load] Failed to load classes from discovered files | Error: Cannot find module '/app/dist/repositories/broken.repository.js'
1938
- ```
1939
-
1940
- The boot process does NOT continue to the next booter or the next phase after an error. If `RepositoryBooter` fails during LOAD, `ServiceBooter` and `ControllerBooter` will not execute their LOAD phases.
1941
-
1942
- ### File import fails
1943
-
1944
- If a discovered file cannot be imported (syntax error, missing dependency, runtime error during module evaluation), `loadClasses()` throws:
1945
-
1946
- ```
1947
- Error: Failed to load file: /app/dist/controllers/broken.controller.js | Error: SyntaxError: Unexpected token
1948
- ```
1949
-
1950
- This error is then wrapped by `BaseArtifactBooter.load()` and then again by `Bootstrapper.runPhase()`.
1951
-
1952
- ---
1953
-
1954
- ## Debugging Boot
1955
-
1956
- ### Enable debug logging
1957
-
1958
- The boot system uses `BaseHelper`'s logger (Winston-based) with `debug` level. To see all boot debug output, set the `DEBUG` environment variable or configure the logger level:
1959
-
1960
- ```bash
1961
- # Enable debug logging
1962
- DEBUG=* bun run server:dev
1963
-
1964
- # Or set logger level in your app configuration
1965
- APP_ENV_LOGGER_FORMAT=text
1966
- ```
1967
-
1968
- ### Debug output reference
1969
-
1970
- With debug logging enabled, a typical boot produces this output:
1971
-
1972
- ```
1973
- [Bootstrapper][discoverBooters] Discovered booter: booter.DatasourceBooter
1974
- [Bootstrapper][discoverBooters] Discovered booter: booter.RepositoryBooter
1975
- [Bootstrapper][discoverBooters] Discovered booter: booter.ServiceBooter
1976
- [Bootstrapper][discoverBooters] Discovered booter: booter.ControllerBooter
1977
- [Bootstrapper][boot] Starting boot | Number of booters: 4
1978
- [Bootstrapper][runPhase] Starting phase: CONFIGURE
1979
- [Bootstrapper][runPhase] Running | Phase: configure | Booter: DatasourceBooter
1980
- [DatasourceBooter][configure] Configured: {"dirs":["datasources"],"extensions":[".datasource.js"],"isNested":true}
1981
- [Bootstrapper][runPhase] Running | Phase: configure | Booter: RepositoryBooter
1982
- [RepositoryBooter][configure] Configured: {"dirs":["repositories"],"extensions":[".repository.js"],"isNested":true}
1983
- [Bootstrapper][runPhase] Running | Phase: configure | Booter: ServiceBooter
1984
- [ServiceBooter][configure] Configured: {"dirs":["services"],"extensions":[".service.js"],"isNested":true}
1985
- [Bootstrapper][runPhase] Running | Phase: configure | Booter: ControllerBooter
1986
- [ControllerBooter][configure] Configured: {"dirs":["controllers"],"extensions":[".controller.js"],"isNested":true}
1987
- [Bootstrapper][runPhase] Completed phase: CONFIGURE | Took: 0.42 ms
1988
- [Bootstrapper][runPhase] Starting phase: DISCOVER
1989
- [Bootstrapper][runPhase] Running | Phase: discover | Booter: DatasourceBooter
1990
- [DatasourceBooter][discover] Root: /app/dist | Using pattern: datasources/{**/*,*}.datasource.js | Discovered file: [...]
1991
- [Bootstrapper][runPhase] Running | Phase: discover | Booter: RepositoryBooter
1992
- [RepositoryBooter][discover] Root: /app/dist | Using pattern: repositories/{**/*,*}.repository.js | Discovered file: [...]
1993
- ...
1994
- [Bootstrapper][runPhase] Completed phase: DISCOVER | Took: 12.8 ms
1995
- [Bootstrapper][runPhase] Starting phase: LOAD
1996
- ...
1997
- [DatasourceBooter][bind] Bound key: datasources.PostgresDataSource
1998
- [RepositoryBooter][bind] Bound key: repositories.UserRepository
1999
- ...
2000
- [Bootstrapper][runPhase] Completed phase: LOAD | Took: 45.3 ms
2001
- [Bootstrapper][generateReport] Boot report: {}
2002
- ```
2003
-
2004
- ### Inspect boot internals programmatically
2005
-
2006
- You can access the `Bootstrapper` directly to inspect state:
2007
-
2008
- ```typescript
2009
- const bootstrapper = app.get<Bootstrapper>({ key: 'bootstrapper' });
2010
-
2011
- // Run only configure + discover phases
2012
- await bootstrapper.boot({ phases: ['configure', 'discover'] });
2013
-
2014
- // Now inspect what was discovered before loading
2015
- // (requires access to the booter instances via the container)
2016
- const controllerBooter = app.get<ControllerBooter>({ key: 'booter.ControllerBooter' });
2017
- console.log('Discovered controller files:', controllerBooter['discoveredFiles']);
2018
- ```
2019
-
2020
- ### Check discovered files and loaded classes
2021
-
2022
- Using bracket notation (since properties are `protected`), you can inspect the internal state of any booter:
2023
-
2024
- ```typescript
2025
- const repoBooter = app.get<RepositoryBooter>({ key: 'booter.RepositoryBooter' });
2026
-
2027
- // After boot
2028
- console.log('Options:', repoBooter['artifactOptions']);
2029
- console.log('Discovered:', repoBooter['discoveredFiles']);
2030
- console.log('Loaded:', repoBooter['loadedClasses'].map(cls => cls.name));
2031
- ```
2032
-
2033
- ### Verify container bindings after boot
2034
-
2035
- ```typescript
2036
- // Check all registered controllers
2037
- const controllerBindings = app.findByTag({ tag: 'controllers' });
2038
- for (const binding of controllerBindings) {
2039
- console.log('Controller:', binding.key);
2040
- }
2041
-
2042
- // Check all registered datasources
2043
- const datasourceBindings = app.findByTag({ tag: 'datasources' });
2044
- for (const binding of datasourceBindings) {
2045
- console.log('Datasource:', binding.key, '| Scope:', binding.scope);
2046
- }
2047
- ```
2048
-
2049
- ---
2050
-
2051
- ## Performance Tuning
2052
-
2053
- ### Limiting glob scope
2054
-
2055
- For large codebases with many files, overly broad glob patterns can slow down the DISCOVER phase. Here are strategies to optimize:
2056
-
2057
- **1. Disable nested scanning when not needed:**
2058
-
2059
- ```typescript
2060
- repositories: {
2061
- isNested: false, // Only scan the top-level directory
2062
- }
2063
- ```
2064
-
2065
- This changes the pattern from `repositories/{**/*,*}.repository.js` to `repositories/*.repository.js`, which is significantly faster for directories with deep structures.
2066
-
2067
- **2. Use specific directory lists instead of nested scanning:**
2068
-
2069
- ```typescript
2070
- controllers: {
2071
- dirs: ['controllers/api', 'controllers/admin'],
2072
- isNested: false,
2073
- // Targets specific subdirectories without recursive scanning
2074
- }
2075
- ```
2076
-
2077
- **3. Use custom glob patterns for surgical targeting:**
2078
-
2079
- ```typescript
2080
- controllers: {
2081
- glob: 'controllers/*.controller.js',
2082
- // Most precise -- only top-level controller files
2083
- }
2084
- ```
2085
-
2086
- ### Reducing import overhead
2087
-
2088
- The LOAD phase's cost comes from `await import(file)` calls. Each file import triggers module evaluation, which may cascade to importing dependencies. To reduce this:
2089
-
2090
- **1. Keep artifact files lean.** Controller/service/repository files should export a single class. Avoid heavy initialization in module scope.
2091
-
2092
- **2. Use barrel files carefully.** If a repository file re-exports from a model file that re-exports from a schema file, the import chain is longer. Keep imports direct.
2093
-
2094
- **3. Avoid side effects in module scope.** Database connections, API calls, or file I/O in module-level code will run during `import()` and slow boot.
2095
-
2096
- ### Monitoring boot performance
2097
-
2098
- Use the debug log output to identify slow phases:
2099
-
2100
- ```
2101
- [Bootstrapper][runPhase] Completed phase: CONFIGURE | Took: 0.42 ms -- Fast
2102
- [Bootstrapper][runPhase] Completed phase: DISCOVER | Took: 12.8 ms -- Normal
2103
- [Bootstrapper][runPhase] Completed phase: LOAD | Took: 245.3 ms -- Potentially slow
2104
- ```
2105
-
2106
- If DISCOVER is slow, your glob patterns may be too broad. If LOAD is slow, your imported files may have heavy module-level initialization.
2107
-
2108
- ---
2109
-
2110
- ## File Naming Conventions
2111
-
2112
- ### Why `.controller.js` instead of `.controller.ts`?
2113
-
2114
- The boot system discovers files in the **built output directory** (e.g., `dist/cjs/` or `dist/esm/`), not in the source directory. TypeScript source files (`.ts`) are compiled to JavaScript (`.js`) by `tsc` before the application runs. Therefore:
2115
-
2116
- - **Source files:** `src/controllers/user.controller.ts`
2117
- - **Built files:** `dist/cjs/controllers/user.controller.js`
2118
- - **Boot discovers:** `dist/cjs/controllers/user.controller.js`
2119
-
2120
- The default extensions use `.js` because that is what exists at runtime. If you are running TypeScript directly (e.g., via `bun run` which supports direct `.ts` execution), you would need to override the extensions:
2121
-
2122
- ```typescript
2123
- controllers: {
2124
- extensions: ['.controller.ts'],
2125
- }
2126
- ```
2127
-
2128
- However, the standard IGNIS workflow uses compiled output, so `.js` is the correct default.
2129
-
2130
- ### Naming convention for artifact files
2131
-
2132
- The convention follows the pattern: `{name}.{artifact-type}.{extension}`
2133
-
2134
- | Artifact Type | Convention | Example |
2135
- | ------------- | ------------------------ | -------------------------- |
2136
- | Controller | `{name}.controller.ts` | `user.controller.ts` |
2137
- | Service | `{name}.service.ts` | `auth.service.ts` |
2138
- | Repository | `{name}.repository.ts` | `user.repository.ts` |
2139
- | DataSource | `{name}.datasource.ts` | `postgres.datasource.ts` |
2140
-
2141
- These conventions are not enforced by the framework -- they are what the default extensions expect. You can use any naming scheme by overriding `extensions` or `glob` in your boot options.
2142
-
2143
- ### Directory conventions
2144
-
2145
- The default directory names match the artifact types:
2146
-
2147
- | Artifact Type | Default Directory |
2148
- | ------------- | ----------------- |
2149
- | Controllers | `controllers/` |
2150
- | Services | `services/` |
2151
- | Repositories | `repositories/` |
2152
- | DataSources | `datasources/` |
2153
-
2154
- These are relative to the project root (the `@app/project_root` binding). In a typical IGNIS application built with `tsc`, the project root is the `dist/cjs/` directory.
2155
-
2156
- ---
2157
-
2158
- ## Boot Utilities
2159
-
2160
- The `@venizia/ignis-boot` package exports three utility functions used internally by the boot system. These are also available for direct use:
2161
-
2162
- ### `discoverFiles`
2163
-
2164
- Discover files matching a glob pattern relative to a root directory. Returns absolute file paths.
2165
-
2166
- ```typescript
2167
- import { discoverFiles } from '@venizia/ignis-boot';
2168
-
2169
- const files = await discoverFiles({
2170
- root: '/path/to/project/dist',
2171
- pattern: 'controllers/{**/*,*}.controller.js',
2172
- });
2173
- // => ['/path/to/project/dist/controllers/user.controller.js', ...]
2174
- ```
2175
-
2176
- **Full signature:**
2177
-
2178
- ```typescript
2179
- const discoverFiles: (opts: {
2180
- pattern: string;
2181
- root: string;
2182
- }) => Promise<string[]>;
2183
- ```
2184
-
2185
- Internally uses the `glob` npm package with `{ cwd: root, absolute: true }` options. Throws a wrapped error if the glob operation fails.
2186
-
2187
- ### `loadClasses`
2188
-
2189
- Dynamically import an array of files and extract all class constructor exports. Non-class exports (arrow functions, objects, primitives) are filtered out.
2190
-
2191
- ```typescript
2192
- import { loadClasses } from '@venizia/ignis-boot';
2193
-
2194
- const classes = await loadClasses({
2195
- files: ['/abs/path/to/user.controller.js', '/abs/path/to/auth.controller.js'],
2196
- root: '/abs/path/to',
2197
- });
2198
- // => [class UserController, class AuthController]
2199
- ```
2200
-
2201
- **Full signature:**
2202
-
2203
- ```typescript
2204
- const loadClasses: (opts: {
2205
- files: string[];
2206
- root: string;
2207
- }) => Promise<AnyType[]>;
2208
- ```
2209
-
2210
- For each file, calls `await import(file)` and iterates all named exports. Uses `isClass()` to filter. If any file import fails, throws a wrapped error with the file path.
2211
-
2212
- ### `isClass`
2213
-
2214
- Type guard that checks whether a value is a class constructor (a function with a `prototype` property). Used internally by `loadClasses` to filter exports.
2215
-
2216
- ```typescript
2217
- import { isClass } from '@venizia/ignis-boot';
2218
-
2219
- class MyService {}
2220
- const arrowFn = () => {};
2221
- function RegularFunction() {}
2222
- abstract class AbstractClass {}
2223
-
2224
- isClass(MyService); // true
2225
- isClass(RegularFunction); // true (has prototype)
2226
- isClass(AbstractClass); // true
2227
- isClass(arrowFn); // false (arrow functions have no prototype)
2228
- isClass('string'); // false
2229
- isClass(42); // false
2230
- isClass(null); // false
2231
- isClass(undefined); // false
2232
- isClass({}); // false
2233
- ```
2234
-
2235
- **Full signature:**
2236
-
2237
- ```typescript
2238
- const isClass: <T>(target: AnyType) => target is TClass<T>;
2239
- ```
2240
-
2241
- **Implementation:**
2242
-
2243
- ```typescript
2244
- export const isClass = <T>(target: AnyType): target is TClass<T> => {
2245
- return typeof target === 'function' && target.prototype !== undefined;
2246
- };
2247
- ```
2248
-
2249
- Note: Regular function declarations and function expressions return `true` because they have a `prototype` property. Only arrow functions return `false`. This is intentional -- traditional function constructors are valid class-like constructs in JavaScript.
2250
-
2251
- ---
2252
-
2253
- ## Complete Type Reference
2254
-
2255
- All types are exported from `@venizia/ignis-boot`:
2256
-
2257
- ```typescript
2258
- // =============================================================================
2259
- // Artifact Configuration
2260
- // =============================================================================
2261
-
2262
- /**
2263
- * Configuration for a single artifact type's discovery behavior.
2264
- */
2265
- interface IArtifactOptions {
2266
- /** Directories to scan, relative to project root */
2267
- dirs?: string[];
2268
- /** File extensions to match (e.g., '.controller.js') */
2269
- extensions?: string[];
2270
- /** Recurse into subdirectories. Default: true */
2271
- isNested?: boolean;
2272
- /** Custom glob pattern. If set, dirs and extensions are ignored */
2273
- glob?: string;
2274
- }
2275
-
2276
- /**
2277
- * Top-level boot options mapping artifact type names to their discovery config.
2278
- * Includes four built-in keys and supports arbitrary extension via index signature.
2279
- */
2280
- interface IBootOptions {
2281
- controllers?: IArtifactOptions;
2282
- services?: IArtifactOptions;
2283
- repositories?: IArtifactOptions;
2284
- datasources?: IArtifactOptions;
2285
- /** Extensible for custom booters */
2286
- [artifactType: string]: IArtifactOptions | undefined;
2287
- }
2288
-
2289
- // =============================================================================
2290
- // Boot Phases
2291
- // =============================================================================
2292
-
2293
- /**
2294
- * The three boot phases, derived as const string values from BootPhases class.
2295
- */
2296
- type TBootPhase = 'configure' | 'discover' | 'load';
2297
-
2298
- /**
2299
- * Ordered array of all boot phases.
2300
- * Used as the default value when no specific phases are requested.
2301
- */
2302
- const BOOT_PHASES: TBootPhase[] = ['configure', 'discover', 'load'];
2303
-
2304
- // =============================================================================
2305
- // Booter Interfaces
2306
- // =============================================================================
2307
-
2308
- /**
2309
- * Interface that all booters must implement.
2310
- * Each method corresponds to one boot phase.
2311
- */
2312
- interface IBooter {
2313
- configure(): ValueOrPromise<void>;
2314
- discover(): ValueOrPromise<void>;
2315
- load(): ValueOrPromise<void>;
2316
- }
2317
-
2318
- /**
2319
- * Constructor options for BaseArtifactBooter.
2320
- */
2321
- interface IBooterOptions {
2322
- /** Logger scope name (typically the booter class name) */
2323
- scope: string;
2324
- /** Absolute path to project root directory */
2325
- root: string;
2326
- /** User-provided artifact discovery options */
2327
- artifactOptions: IArtifactOptions;
2328
- }
2329
-
2330
- // =============================================================================
2331
- // Bootstrapper
2332
- // =============================================================================
2333
-
2334
- /**
2335
- * Options for controlling which phases and booters to execute.
2336
- */
2337
- interface IBootExecutionOptions {
2338
- /** Phases to execute. Default: ['configure', 'discover', 'load'] */
2339
- phases?: TBootPhase[];
2340
- /** Specific booters to run by name. Default: all discovered booters */
2341
- booters?: string[];
2342
- }
2343
-
2344
- /**
2345
- * Interface for the Bootstrapper orchestrator.
2346
- */
2347
- interface IBootstrapper {
2348
- boot(opts: IBootExecutionOptions): Promise<IBootReport>;
2349
- }
2350
-
2351
- /**
2352
- * Boot execution report. Currently empty; designed as an extension point
2353
- * for future timing and statistics data.
2354
- */
2355
- interface IBootReport {}
2356
-
2357
- // =============================================================================
2358
- // Application
2359
- // =============================================================================
2360
-
2361
- /**
2362
- * Minimal application interface required by the boot system.
2363
- * Extends Container with a method to get the project root path.
2364
- */
2365
- interface IApplication extends Container {
2366
- getProjectRoot(): string;
2367
- }
2368
-
2369
- /**
2370
- * Interface for applications that support the boot() method.
2371
- * Implemented by BootMixin and BaseApplication.
2372
- */
2373
- interface IBootableApplication {
2374
- boot(): Promise<IBootReport>;
2375
- }
2376
- ```
2377
-
2378
- ### Exported Classes
2379
-
2380
- ```typescript
2381
- /**
2382
- * Abstract base class for all artifact booters.
2383
- * Implements the Template Method pattern for three-phase boot lifecycle.
2384
- *
2385
- * Protected properties:
2386
- * root: string -- Project root path
2387
- * artifactOptions: IArtifactOptions -- Merged discovery config
2388
- * discoveredFiles: string[] -- Files found during discover phase
2389
- * loadedClasses: TClass[] -- Classes extracted during load phase
2390
- *
2391
- * Abstract methods (must be implemented by subclasses):
2392
- * getDefaultDirs(): string[]
2393
- * getDefaultExtensions(): string[]
2394
- * bind(): Promise<void>
2395
- */
2396
- abstract class BaseArtifactBooter extends BaseHelper implements IBooter { }
2397
-
2398
- /**
2399
- * Orchestrates the boot process across all discovered booters.
2400
- *
2401
- * Private properties:
2402
- * booters: IBooter[]
2403
- * phaseStartTimings: Map<string, number>
2404
- * phaseEndTimings: Map<string, number>
2405
- *
2406
- * Constructor injection:
2407
- * @inject({ key: '@app/instance' }) application: IApplication
2408
- */
2409
- class Bootstrapper extends BaseHelper implements IBootstrapper { }
2410
-
2411
- /**
2412
- * Built-in booter for controller artifacts.
2413
- * Default dir: 'controllers', extension: '.controller.js', scope: transient
2414
- */
2415
- class ControllerBooter extends BaseArtifactBooter { }
2416
-
2417
- /**
2418
- * Built-in booter for service artifacts.
2419
- * Default dir: 'services', extension: '.service.js', scope: transient
2420
- */
2421
- class ServiceBooter extends BaseArtifactBooter { }
2422
-
2423
- /**
2424
- * Built-in booter for repository artifacts.
2425
- * Default dir: 'repositories', extension: '.repository.js', scope: transient
2426
- */
2427
- class RepositoryBooter extends BaseArtifactBooter { }
2428
-
2429
- /**
2430
- * Built-in booter for datasource artifacts.
2431
- * Default dir: 'datasources', extension: '.datasource.js', scope: SINGLETON
2432
- */
2433
- class DatasourceBooter extends BaseArtifactBooter { }
2434
- ```
2435
-
2436
- ### Exported Functions
2437
-
2438
- ```typescript
2439
- /**
2440
- * Mixin that enhances a Container subclass with boot capabilities.
2441
- * Registers all built-in booters and the Bootstrapper in the constructor.
2442
- * Adds bootOptions property and boot() method to the class.
2443
- */
2444
- const BootMixin: <T extends TMixinTarget<Container>>(baseClass: T) => T & IBootableApplication;
2445
-
2446
- /**
2447
- * Discover files matching a glob pattern relative to a root directory.
2448
- * Returns absolute file paths. Uses the 'glob' npm package internally.
2449
- */
2450
- const discoverFiles: (opts: { pattern: string; root: string }) => Promise<string[]>;
2451
-
2452
- /**
2453
- * Dynamically import files and extract class constructor exports.
2454
- * Non-class exports are filtered out using isClass().
2455
- */
2456
- const loadClasses: (opts: { files: string[]; root: string }) => Promise<AnyType[]>;
2457
-
2458
- /**
2459
- * Type guard: checks if a value is a class constructor
2460
- * (a function with a prototype property).
2461
- */
2462
- const isClass: <T>(target: AnyType) => target is TClass<T>;
2463
- ```
2464
-
2465
- ---
2466
-
2467
- ## Constants Reference
2468
-
2469
- ```typescript
2470
- /**
2471
- * Static class providing boot phase name constants.
2472
- * Values match the TBootPhase union type.
2473
- */
2474
- class BootPhases {
2475
- static readonly CONFIGURE = 'configure';
2476
- static readonly DISCOVER = 'discover';
2477
- static readonly LOAD = 'load';
2478
- }
2479
-
2480
- /**
2481
- * Ordered array of all boot phases.
2482
- * Used as the default when Bootstrapper.boot() is called without specific phases.
2483
- */
2484
- const BOOT_PHASES: TBootPhase[] = ['configure', 'discover', 'load'];
2485
- ```
2486
-
2487
- ### DI Binding Keys Used by the Boot System
2488
-
2489
- | Key | Bound By | Type | Description |
2490
- | --------------------------- | --------------------------- | ----------------------- | ------------------------------------------------ |
2491
- | `@app/project_root` | Application | `string` | Absolute path to project's build output root |
2492
- | `@app/instance` | Application | `IApplication` | The application container itself |
2493
- | `@app/boot-options` | BootMixin / BaseApplication | `IBootOptions` | User's boot configuration |
2494
- | `booter.DatasourceBooter` | BootMixin | `DatasourceBooter` class | Tagged `'booter'` |
2495
- | `booter.RepositoryBooter` | BootMixin | `RepositoryBooter` class | Tagged `'booter'` |
2496
- | `booter.ServiceBooter` | BootMixin | `ServiceBooter` class | Tagged `'booter'` |
2497
- | `booter.ControllerBooter` | BootMixin | `ControllerBooter` class | Tagged `'booter'` |
2498
- | `bootstrapper` | BootMixin / BaseApplication | `Bootstrapper` class | Singleton scope |
2499
-
2500
- Note: When used with `BaseApplication`, the booter keys follow the pattern `booters.{ClassName}` (using the `BindingNamespaces.BOOTERS` namespace) instead of `booter.{ClassName}`.
2501
-
2502
- ---
2503
-
2504
- ## Testing Patterns
2505
-
2506
- The boot package tests run on **built** output (`dist/cjs/`) since the boot system discovers compiled `.js` files, not `.ts` source files.
2507
-
2508
- ### Test fixture structure
2509
-
2510
- ```
2511
- src/__tests__/fixtures/
2512
- repositories/
2513
- model1.repository.ts (exports Model1Repository class)
2514
- model2.repository.ts (exports Model2Repository class)
2515
- sub-repositories/
2516
- model3.repository.ts (exports Model3Repository class)
2517
- non-repositories/
2518
- non.repository.ts (empty file -- no class exports)
2519
- ```
2520
-
2521
- The `non-repositories/` directory contains a file with no class exports, used to verify that `loadClasses()` correctly filters non-class modules and returns an empty array.
2522
-
2523
- ### Testing a custom booter
2524
-
2525
- Create a concrete test booter by extending `BaseArtifactBooter`:
2526
-
2527
- ```typescript
2528
- import { BaseArtifactBooter } from '@venizia/ignis-boot';
2529
- import { beforeAll, describe, expect, test } from 'bun:test';
2530
- import path from 'node:path';
2531
-
2532
- class TestBooter extends BaseArtifactBooter {
2533
- protected override getDefaultDirs(): string[] {
2534
- return ['repositories'];
2535
- }
2536
- protected override getDefaultExtensions(): string[] {
2537
- return ['.repository.js'];
2538
- }
2539
- protected override bind(): Promise<void> {
2540
- return Promise.resolve();
2541
- }
2542
- }
2543
-
2544
- describe('TestBooter', () => {
2545
- let booter: TestBooter;
2546
- const root = path.resolve(process.cwd(), 'dist/cjs/__tests__/fixtures');
2547
-
2548
- beforeAll(() => {
2549
- booter = new TestBooter({ root, artifactOptions: {}, scope: TestBooter.name });
2550
- });
2551
-
2552
- test('should use default options after configure', async () => {
2553
- await booter.configure();
2554
- expect(booter['artifactOptions'].dirs).toEqual(['repositories']);
2555
- expect(booter['artifactOptions'].extensions).toEqual(['.repository.js']);
2556
- expect(booter['artifactOptions'].isNested).toEqual(true);
2557
- });
2558
-
2559
- test('should discover files after discover', async () => {
2560
- await booter.configure();
2561
- await booter.discover();
2562
- expect(booter['discoveredFiles'].length).toBeGreaterThan(0);
2563
- });
2564
-
2565
- test('should load classes after load', async () => {
2566
- await booter.configure();
2567
- await booter.discover();
2568
- await booter.load();
2569
- expect(booter['loadedClasses'].length).toBeGreaterThan(0);
2570
- });
2571
- });
2572
- ```
2573
-
2574
- ### Testing pattern generation
2575
-
2576
- ```typescript
2577
- test('should generate pattern with defaults', async () => {
2578
- await booter.configure();
2579
- const pattern = booter['getPattern']();
2580
- expect(pattern).toBe('repositories/{**/*,*}.repository.js');
2581
- });
2582
-
2583
- test('should generate pattern with multiple dirs and extensions', async () => {
2584
- const multiBooter = new TestBooter({
2585
- scope: TestBooter.name,
2586
- root,
2587
- artifactOptions: {
2588
- dirs: ['dir1', 'dir2'],
2589
- extensions: ['.ext1.js', '.ext2.js'],
2590
- },
2591
- });
2592
- await multiBooter.configure();
2593
- const pattern = multiBooter['getPattern']();
2594
- expect(pattern).toBe('{dir1,dir2}/{**/*,*}.{ext1.js,ext2.js}');
2595
- });
2596
-
2597
- test('should use custom glob if provided', async () => {
2598
- const globBooter = new TestBooter({
2599
- scope: TestBooter.name,
2600
- root,
2601
- artifactOptions: {
2602
- glob: 'custom/glob/pattern/**/*.js',
2603
- },
2604
- });
2605
- await globBooter.configure();
2606
- const pattern = globBooter['getPattern']();
2607
- expect(pattern).toBe('custom/glob/pattern/**/*.js');
2608
- });
2609
- ```
2610
-
2611
- ### Testing utility functions
2612
-
2613
- ```typescript
2614
- import { discoverFiles, isClass, loadClasses } from '@venizia/ignis-boot';
2615
-
2616
- describe('isClass', () => {
2617
- test('should return true for class constructors', () => {
2618
- class TestClass {}
2619
- abstract class AbstractClass {}
2620
- function FunctionClass() {}
2621
-
2622
- expect(isClass(TestClass)).toBe(true);
2623
- expect(isClass(AbstractClass)).toBe(true);
2624
- expect(isClass(FunctionClass)).toBe(true);
2625
- });
2626
-
2627
- test('should return false for non-class types', () => {
2628
- const ArrowFunction = () => {};
2629
- expect(isClass(ArrowFunction)).toBe(false);
2630
- expect(isClass({})).toBe(false);
2631
- expect(isClass('string')).toBe(false);
2632
- expect(isClass(null)).toBe(false);
2633
- expect(isClass(undefined)).toBe(false);
2634
- });
2635
- });
2636
-
2637
- describe('discoverFiles', () => {
2638
- test('should return files matching the glob pattern', async () => {
2639
- const files = await discoverFiles({
2640
- pattern: '**/*.repository.js',
2641
- root: '/path/to/dist/__tests__/fixtures',
2642
- });
2643
- expect(files.length).toBeGreaterThan(0);
2644
- });
2645
-
2646
- test('should return empty array for no matches', async () => {
2647
- const files = await discoverFiles({
2648
- pattern: '**/*.nonexistent',
2649
- root: process.cwd(),
2650
- });
2651
- expect(files).toEqual([]);
2652
- });
2653
- });
2654
-
2655
- describe('loadClasses', () => {
2656
- test('should load classes from files', async () => {
2657
- const files = await discoverFiles({
2658
- pattern: 'repositories/*.repository.js',
2659
- root: '/path/to/dist/__tests__/fixtures',
2660
- });
2661
- const classes = await loadClasses({ files, root: '/path/to/dist/__tests__/fixtures' });
2662
- expect(classes.length).toBeGreaterThan(0);
2663
- });
2664
-
2665
- test('should return empty array when no classes exported', async () => {
2666
- const files = await discoverFiles({
2667
- pattern: 'non-repositories/*.repository.js',
2668
- root: '/path/to/dist/__tests__/fixtures',
2669
- });
2670
- const classes = await loadClasses({ files, root: '/path/to/dist/__tests__/fixtures' });
2671
- expect(classes).toEqual([]);
2672
- });
2673
- });
2674
- ```
2675
-
2676
- **Key testing conventions:**
2677
-
2678
- - Use bracket notation to access protected members: `booter['artifactOptions']`, `booter['discoveredFiles']`
2679
- - Test each phase independently and sequentially (configure before discover, discover before load)
2680
- - Use fixture directories with simple class exports for predictable discovery results
2681
- - Non-class fixtures (empty files, constant exports) verify that filtering works correctly
2682
- - Tests use `dist/cjs/__tests__/fixtures/` (built output) as the root, not the `src/` directory
2683
- - The `pretest` script (`bun run rebuild`) ensures fixtures are compiled before tests run
2684
-
2685
- ---
220
+ [Documentation](https://ignis.venizia.ai) &#8226;
221
+ [Quickstart](https://ignis.venizia.ai/guides/get-started/5-minute-quickstart) &#8226;
222
+ [Core API](https://ignis.venizia.ai/references/) &#8226;
223
+ [Best practices](https://ignis.venizia.ai/best-practices/) &#8226;
224
+ [Changelog](https://ignis.venizia.ai/changelogs/)
2686
225
 
2687
- ## License
226
+ Inspired by [LoopBack 4's boot system](https://loopback.io/doc/en/lb4/Booting-an-Application.html).
2688
227
 
2689
- MIT
228
+ MIT licensed - see [LICENSE.md](LICENSE.md).
229
+ Questions: [GitHub Issues](https://github.com/VENIZIA-AI/ignis/issues) &#8226; developer@venizia.ai
230
+ </content>