@toijs/modular 1.0.3 → 1.0.6

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 ADDED
@@ -0,0 +1,403 @@
1
+ # @toijs/modular
2
+
3
+ Compose TypeScript applications with an isomorphic module pattern, explicit DI, and Shell — on Node.js and in the browser.
4
+ [![npm version](https://img.shields.io/npm/v/@toijs/modular.svg)](https://www.npmjs.com/package/@toijs/modular)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6.svg)](https://www.typescriptlang.org/)
6
+ [![ESM](https://img.shields.io/badge/module-ESM-brightgreen.svg)](https://nodejs.org/api/esm.html)
7
+ [![GitHub](https://img.shields.io/badge/github-toijs%2Fframework-181717.svg)](https://github.com/toijs/framework)
8
+
9
+ ## Overview
10
+
11
+ `@toijs/modular` is an isomorphic **composition pattern** for TypeScript: modules, explicit dependency injection, a bootstrap lifecycle, and a small runtime (`Task`, `Config`, `Metadata`, `Shell`). Use it on **Node.js servers** and in **browser clients**. App hosts (Express, Vue, React, …) plug in through **Shell** kits — this package never embeds HTTP or UI.
12
+
13
+ It does **not** include HTTP, routing, validation, GraphQL, an ORM, or a renderer. Those belong in the application or in separate kits such as `@toijs/express-kit`, `@toijs/vue-kit`, and `@toijs/typeorm`.
14
+
15
+ ## Why @toijs/modular?
16
+
17
+ Most TypeScript backends either assemble everything by hand or adopt a full platform such as NestJS. Frontends often wire Pinia/Vuex/React context ad hoc. `@toijs/modular` sits between those options: one composition model for server and client, without inheriting an HTTP server, a CLI, or a decorator-heavy application model.
18
+
19
+ Constructor dependencies are **explicit**. `@Injectable([UserRepository])` does not read `design:paramtypes`. That is intentional — the container never infers tokens from TypeScript types.
20
+ ## Features
21
+
22
+ Implemented in this package:
23
+
24
+ - **Modules as functions** — a factory returns `{ name, dependencies?, prepare?, register?, ready? }`
25
+ - **Dependency injection** — class, value, factory, and existing providers
26
+ - **Explicit tokens** — `@Injectable([tokens])`, `@Inject(token)`, `@Optional()`
27
+ - **Scopes** — `singleton` (default) and `transient` on class and factory providers
28
+ - **Parent/child containers** — `container.createChild()`
29
+ - **Lifecycle** — resolve factories, then `await prepare` → `await register` → `ready` (not awaited)
30
+ - **Task bus** — `subscribe` / `invoke` (`sequential` or `parallel`)
31
+ - **Config** — nested values via dot paths on a metadata-backed store
32
+ - **Shell** — host-app bootstrap (`create` / `register` / `ready`) for Express, Vue, React, … kits
33
+ - **Isomorphic** — same pattern on Node.js server and browser client
34
+ Not in this package:
35
+
36
+ - HTTP server, router, controllers as a framework primitive
37
+ - Request-scoped providers
38
+ - NestJS-style `@Module({ imports, providers })`
39
+ - First-party Lambda, SQS, or worker adapters
40
+ - Tests, changelog, and a published license (see [Open-source status](#open-source-status))
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ npm install @toijs/modular
46
+ # or
47
+ pnpm add @toijs/modular
48
+ ```
49
+
50
+ The package is **ESM-only** (`"type": "module"`). Node.js 18+ is the intended runtime; `engines` is not declared yet.
51
+
52
+ Enable class decorators in the consuming project. `@Injectable` is a class decorator. `@Inject` and `@Optional` are parameter decorators and need `experimentalDecorators` in `tsconfig.json`.
53
+
54
+ ```json
55
+ {
56
+ "compilerOptions": {
57
+ "experimentalDecorators": true
58
+ }
59
+ }
60
+ ```
61
+
62
+ `emitDecoratorMetadata` is **not** required and is **not** read by this container.
63
+
64
+ ## Quick Start
65
+
66
+ ```ts
67
+ import { Injectable, Launcher, type Module } from "@toijs/modular";
68
+
69
+ @Injectable()
70
+ class UserService {
71
+ list() {
72
+ return [{ id: "1", name: "Ada" }];
73
+ }
74
+ }
75
+
76
+ function AppModule(launcher: Launcher): Module {
77
+ const prepare = () => {
78
+ launcher.container.register([UserService]);
79
+ };
80
+
81
+ const ready = () => {
82
+ const users = launcher.container.resolve(UserService);
83
+ console.log(users.list());
84
+ };
85
+
86
+ return { name: "app", prepare, ready };
87
+ }
88
+
89
+ await new Launcher().modules([AppModule]).start();
90
+ ```
91
+
92
+ `start()` throws if `.modules()` was never called with at least one factory.
93
+
94
+ ## Module example
95
+
96
+ A module is a **function**, not a class. There is no `@Module()` decorator.
97
+
98
+ ```ts
99
+ import { Launcher, type Module } from "@toijs/modular";
100
+ import { UserService } from "./user.service.js";
101
+ import { UserRepository } from "./user.repository.js";
102
+
103
+ export function UserModule(launcher: Launcher): Module {
104
+ const prepare = () => {
105
+ launcher.container.register([UserRepository, UserService]);
106
+ };
107
+
108
+ return { name: "feature.user", prepare };
109
+ }
110
+ ```
111
+
112
+ Optional fields:
113
+
114
+ | Field | Role |
115
+ | --- | --- |
116
+ | `name` | Required identity. Duplicate names are **skipped**. Empty names are skipped. |
117
+ | `dependencies` | Extra `ModuleFactory[]` resolved after this module is recorded |
118
+ | `prepare` | Register providers, subscribe to tasks, define routes in a kit — **awaited** |
119
+ | `register` | Create the host (`shell.create`) — **awaited**; kits must **return** the create promise |
120
+ | `ready` | After register — **not** awaited; often unused in features |
121
+
122
+ The factory body runs during resolve, **before** any lifecycle hook. Put `config.define` there so later factories can read config.
123
+
124
+ ## Dependency Injection example
125
+
126
+ Tokens must be listed. An empty `@Injectable()` does not inspect constructor types.
127
+
128
+ ```ts
129
+ import { Injectable } from "@toijs/modular";
130
+ import { UserRepository } from "./user.repository.js";
131
+
132
+ @Injectable([UserRepository])
133
+ export class UserService {
134
+ constructor(private readonly users: UserRepository) {}
135
+
136
+ find(id: string) {
137
+ return this.users.findById(id);
138
+ }
139
+ }
140
+ ```
141
+
142
+ Register the class as its own token (singleton by default):
143
+
144
+ ```ts
145
+ launcher.container.register([UserRepository, UserService]);
146
+ ```
147
+
148
+ ## Provider example
149
+
150
+ ```ts
151
+ import { ConnectionManager } from "@toijs/typeorm";
152
+
153
+ // Class as its own token
154
+ launcher.container.register(UserService);
155
+
156
+ // Value
157
+ launcher.container.register({
158
+ provide: ConnectionManager,
159
+ useValue: connectionManager,
160
+ });
161
+
162
+ // Factory
163
+ launcher.container.register({
164
+ provide: "clock",
165
+ useFactory: () => () => Date.now(),
166
+ });
167
+
168
+ // Existing (alias)
169
+ launcher.container.register({
170
+ provide: "Users",
171
+ useExisting: UserService,
172
+ });
173
+
174
+ // Class with explicit inject list and transient scope
175
+ launcher.container.register({
176
+ provide: UserService,
177
+ useClass: UserService,
178
+ inject: [UserRepository],
179
+ scope: "transient",
180
+ });
181
+ ```
182
+
183
+ `useValue` and `useExisting` are always stored as singletons. `scope` is honored only on `useClass` and `useFactory`.
184
+
185
+ ## Token injection
186
+
187
+ ```ts
188
+ import { Inject, Injectable, Optional } from "@toijs/modular";
189
+
190
+ const CONFIG = Symbol("config");
191
+
192
+ @Injectable()
193
+ class ReportService {
194
+ constructor(
195
+ @Inject(CONFIG) private readonly config: { env: string },
196
+ @Optional() @Inject("logger") private readonly logger?: { info(msg: string): void },
197
+ ) {}
198
+ }
199
+
200
+ launcher.container.register({ provide: CONFIG, useValue: { env: "test" } });
201
+ launcher.container.register(ReportService);
202
+ ```
203
+
204
+ `@Optional()` only applies to **class constructor** parameters. Factory `inject` arrays cannot mark a dependency optional; a missing token throws.
205
+
206
+ ## Lifecycle
207
+
208
+ `await Launcher.start()`:
209
+
210
+ ```text
211
+ resolve factories (and `dependencies`)
212
+ ↓
213
+ await prepare() × all modules
214
+ ↓
215
+ await register() × all modules
216
+ ↓
217
+ ready() × all modules (not awaited)
218
+ ```
219
+
220
+ Hooks are `() => void | Promise<void>`. `start()` **awaits** `prepare` and `register` sequentially. **`ready` is not awaited.** Kits that create a host must **return** `shell.create(...)` from `register` so bootstrap waits for the host and `task.root.register` / `task.root.ready`.
221
+
222
+ `Shell` is the second, host-specific flow used by app-shell kits (Express, Vue, React, …):
223
+
224
+ ```text
225
+ await shell.create(fn)
226
+ ↓
227
+ await invoke "task.root.register"
228
+ ↓
229
+ await invoke "task.root.ready"
230
+ ```
231
+
232
+ Feature modules subscribe with `shell.register` / `shell.ready`. They should not call `shell.create`. The same modular pattern runs on Node.js and in the browser; only the kit (shell) changes.
233
+ ## Application composition
234
+
235
+ ```ts
236
+ import { Launcher } from "@toijs/modular";
237
+ import { TypeORMModule } from "@toijs/typeorm";
238
+ import { ConfigModule } from "./config.module.js";
239
+ import { DatabaseModule } from "./database.module.js";
240
+ import { UserModule } from "./user.module.js";
241
+ import { OrderModule } from "./order.module.js";
242
+
243
+ await new Launcher()
244
+ .modules([
245
+ ConfigModule, // factory body: config.define
246
+ TypeORMModule, // separate package
247
+ DatabaseModule, // setEntities before TypeORM register
248
+ UserModule,
249
+ OrderModule,
250
+ ])
251
+ .start();
252
+ ```
253
+
254
+ `Launcher` constructs a `Container` and registers itself plus `Task`, `Metadata`, `Config`, and `Shell` as value providers.
255
+
256
+ `launcher.define("http", client)` attaches a property on the launcher **and** registers it in DI. It throws if the property already exists.
257
+
258
+ ## Testing
259
+
260
+ There is no test helper in this package. Replace a production provider with a test double by registering over the same token:
261
+
262
+ ```ts
263
+ import { Launcher } from "@toijs/modular";
264
+ import { UserModule } from "./user.module.js";
265
+ import { UserRepository } from "./user.repository.js";
266
+ import { InMemoryUserRepository } from "./user.repository.memory.js";
267
+
268
+ const launcher = new Launcher();
269
+ await launcher.modules([UserModule]).start();
270
+
271
+ launcher.container.register({
272
+ provide: UserRepository,
273
+ useClass: InMemoryUserRepository,
274
+ });
275
+
276
+ const users = launcher.container.resolve(UserRepository);
277
+ ```
278
+
279
+ `register` deletes any cached singleton for that token, so the next `resolve` builds a new instance.
280
+
281
+ `container.tryResolve(token)` returns `undefined` instead of throwing. `container.has(token)` walks parent containers.
282
+
283
+ ## Architecture
284
+
285
+ ```text
286
+ Launcher
287
+ ├── container DI (providers, resolve, child containers)
288
+ ├── task named handler bus
289
+ ├── metadata key/value + change events
290
+ ├── config metadata-backed settings (dot paths)
291
+ └── shell host instance (Express, Vue, React, …)
292
+
293
+ Module factory
294
+ ├── factory body runs at resolve time
295
+ ├── prepare awaited by start()
296
+ ├── register awaited by start()
297
+ └── ready not awaited
298
+ ```
299
+
300
+ Keep domain types and business logic as plain TypeScript. Depend on `@toijs/modular` only at the composition root (modules, `@Injectable`, `container.register`). Same pattern on server and browser; kits supply the app shell.
301
+ ## Use cases
302
+
303
+ Fits:
304
+
305
+ - API process composed with a kit such as `@toijs/express-kit`
306
+ - Vue / React SPA composed with a UI kit (`@toijs/vue-kit` or equivalent)
307
+ - CLI / batch scripts that need modules + DI
308
+ - Long-running workers, if **you** wire the consumer (no SQS adapter ships here)
309
+ - Lambda handlers, if **you** bootstrap `Launcher` per cold start (no Lambda adapter ships here)
310
+
311
+ Does not fit as a drop-in:
312
+
313
+ - Teams that want NestJS’s HTTP, OpenAPI, microservices, and ecosystem
314
+ - Request-scoped per-HTTP-request providers
315
+ - Effect-style typed functional programming
316
+ - CommonJS-only deployments
317
+
318
+ ## Comparison
319
+
320
+ | | `@toijs/modular` | NestJS |
321
+ | --- | --- | --- |
322
+ | DI | Explicit tokens, class/value/factory/existing | Reflection + tokens, large provider model |
323
+ | Module | Function factory | `@Module()` class |
324
+ | Lifecycle | `await prepare` → `await register` → `ready` (not awaited) | Nest lifecycle hooks |
325
+ | HTTP / UI | Not included — Shell + kits (Express, Vue, React, …) | First-class HTTP |
326
+ | Runtime | Node.js and browser | Primarily Node.js |
327
+ | CLI | Not included | `@nestjs/cli` |
328
+ | ORM | Separate `@toijs/typeorm` | Separate `@nestjs/typeorm` etc. |
329
+ | Lambda / worker | Usable; no official adapter | Usable; larger bootstrap |
330
+ | Ecosystem | Small (`typeorm`, private kits) | Large |
331
+ | Learning curve | Small API, unusual module shape | Large API, more tutorials |
332
+
333
+ Use NestJS when you want a platform. Use `@toijs/modular` when you want isomorphic composition and DI without that platform.
334
+ A longer comparison with Effect and plain Node.js is in [docs/guides/comparison.md](docs/guides/comparison.md).
335
+
336
+ ## API overview
337
+
338
+ | Export | Kind |
339
+ | --- | --- |
340
+ | `Launcher` | Bootstrap: `modules()`, `start()`, `define()`, `container`, `task`, `config`, `metadata`, `shell` |
341
+ | `Container` | `register`, `resolve`, `tryResolve`, `has`, `createChild`, `clear` |
342
+ | `Injectable`, `Inject`, `Optional` | Decorators |
343
+ | `Task` | `subscribe`, `unsubscribe`, `invoke(name, data?, type?)` |
344
+ | `Config` | `define`, `resolve(path?)` |
345
+ | `Metadata` | `define`, `resolve`, `subscribe`, `invoke` |
346
+ | `Shell` | `create`, `register`, `ready`, `getInstance` |
347
+ | `SCOPE_SINGLETON`, `SCOPE_TRANSIENT` | `"singleton"` / `"transient"` |
348
+ | `TASK_ROOT_REGISTER`, `TASK_ROOT_READY` | Shell task names |
349
+ | `METADATA_CONFIG`, `EVENT_METADATA_CHANGED` | Config / metadata keys |
350
+
351
+ ## Packages / integrations
352
+
353
+ | Package | Status | Role |
354
+ | --- | --- | --- |
355
+ | `@toijs/modular` | v1.0.4, `private: false` | Core (isomorphic composition) |
356
+ | `@toijs/typeorm` | v1.0.7, `private: false` | `TypeORMModule`, `ConnectionManager` |
357
+ | `@toijs/express-kit` | `private: true` | Express `shell.create` + router (server shell) |
358
+ | `@toijs/vue-kit` | `private: true` | Vue `shell.create` + router (browser shell) |
359
+ | `@toijs/graphql` | `private: true` | Stub — do not treat as shipped |
360
+
361
+ HTTP, Vue, React, i18n, layout, and device kits live outside this package. They are not required to use the container.
362
+ ## Documentation
363
+
364
+ - [Documentation index](docs/README.md)
365
+ - [Modules](docs/concepts/modules.md)
366
+ - [Dependency injection](docs/concepts/dependency-injection.md)
367
+ - [Containers](docs/concepts/containers.md)
368
+ - [Lifecycle](docs/concepts/lifecycle.md)
369
+ - [Architecture](docs/guides/architecture.md)
370
+ - [Examples](examples/README.md)
371
+
372
+ ## Contributing
373
+
374
+ There is no `CONTRIBUTING.md` yet. Source of truth is `src/` in this package. Do not add NestJS APIs “for familiarity” — this runtime is not NestJS.
375
+
376
+ ## Roadmap
377
+
378
+ Not implemented, not scheduled in-repo (ideas only):
379
+
380
+ - Await `ready` hooks (today only `prepare` / `register` are awaited)
381
+ - Request scope
382
+ - CJS build
383
+ - Official Lambda / SQS examples as packages
384
+ - First-party React kit
385
+ - Test utilities
386
+ - License, changelog, CI, coverage
387
+ ## Open-source status
388
+
389
+ | Item | Status |
390
+ | --- | --- |
391
+ | Version | `1.0.4` |
392
+ | License | **Missing** (GitHub `license: null`, no `license` field in `package.json`) |
393
+ | Tests | **None** in this package |
394
+ | CI | **None** in the repository |
395
+ | Changelog | **Missing** |
396
+ | Node `engines` | **Not declared** |
397
+ | ESM / CJS | ESM only |
398
+ | README (previous) | **Missing** — this file is new |
399
+
400
+ Version `1.0.4` means the package is versioned, not that it has a stability policy. Treat breaking changes as possible until a changelog and semver policy exist.
401
+ ## License
402
+
403
+ **NEEDS VERIFICATION.** No license file is present in the package or on [toijs/framework](https://github.com/toijs/framework). Do not assume MIT or any other license until one is published.
@@ -12,6 +12,8 @@ export declare class Launcher {
12
12
  config: Config;
13
13
  options: StartOptions;
14
14
  instances: Map<string, Module>;
15
+ /** Factories already invoked — keeps re-entrant `start()` from re-running side effects. */
16
+ private factories;
15
17
  constructor();
16
18
  /**
17
19
  * Attach a capability onto the launcher (and optionally register it in DI).
@@ -26,13 +28,16 @@ export declare class Launcher {
26
28
  */
27
29
  modules(modules: ModuleFactory[]): this;
28
30
  /**
29
- * Resolve the module instances
30
- * @param modules - The modules to resolve
31
- * @returns The module instances
31
+ * Resolve module factories into instances.
32
+ * Skips factories already invoked so a later `modules([...]).start()` (e.g. plugin
33
+ * attach) does not re-run host factory side effects. Only newly resolved modules
34
+ * are returned so prepare/register/ready run for them alone.
32
35
  */
33
36
  private resolveModuleInstances;
34
37
  /**
35
- * Start the application
38
+ * Start (or continue starting) the application.
39
+ * Safe to call again after `modules([...])` adds more factories — only new
40
+ * modules run prepare → register → ready.
36
41
  */
37
42
  start(): Promise<void>;
38
43
  }
@@ -1 +1 @@
1
- {"version":3,"file":"launcher.service.d.ts","sourceRoot":"","sources":["../../../src/launcher/services/launcher.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAExC,qBAAa,QAAQ;IACZ,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,EAAE,SAAS,CAAC;IACrB,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,YAAY,CAEV;IACX,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAa;;IA8BlD;;;;;OAKG;IACH,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EACxB,GAAG,EAAE,CAAC,EACN,KAAK,EAAE,CAAC,EACR,KAAK,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GACxB,IAAI,GAAG,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;IAoBtB;;;OAGG;IACH,OAAO,CAAC,OAAO,EAAE,aAAa,EAAE;IAKhC;;;;OAIG;YACW,sBAAsB;IAsBpC;;OAEG;IACG,KAAK;CAmBZ"}
1
+ {"version":3,"file":"launcher.service.d.ts","sourceRoot":"","sources":["../../../src/launcher/services/launcher.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAExC,qBAAa,QAAQ;IACZ,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,EAAE,SAAS,CAAC;IACrB,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,YAAY,CAEV;IACX,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAa;IAClD,2FAA2F;IAC3F,OAAO,CAAC,SAAS,CAA4B;;IA8B7C;;;;;OAKG;IACH,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EACxB,GAAG,EAAE,CAAC,EACN,KAAK,EAAE,CAAC,EACR,KAAK,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GACxB,IAAI,GAAG,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;IAoBtB;;;OAGG;IACH,OAAO,CAAC,OAAO,EAAE,aAAa,EAAE;IAKhC;;;;;OAKG;YACW,sBAAsB;IA+BpC;;;;OAIG;IACG,KAAK;CAqBZ"}
@@ -13,6 +13,8 @@ export class Launcher {
13
13
  modules: [],
14
14
  };
15
15
  instances = new Map();
16
+ /** Factories already invoked — keeps re-entrant `start()` from re-running side effects. */
17
+ factories = new Set();
16
18
  constructor() {
17
19
  this.container = new Container();
18
20
  this.task = new Task();
@@ -71,21 +73,24 @@ export class Launcher {
71
73
  return this;
72
74
  }
73
75
  /**
74
- * Resolve the module instances
75
- * @param modules - The modules to resolve
76
- * @returns The module instances
76
+ * Resolve module factories into instances.
77
+ * Skips factories already invoked so a later `modules([...]).start()` (e.g. plugin
78
+ * attach) does not re-run host factory side effects. Only newly resolved modules
79
+ * are returned so prepare/register/ready run for them alone.
77
80
  */
78
81
  async resolveModuleInstances(modules, instances = []) {
79
82
  for (const module of modules) {
83
+ if (this.factories.has(module)) {
84
+ continue;
85
+ }
80
86
  const moduleInstance = module(this);
81
- // If the module is already registered, skip it
87
+ this.factories.add(module);
88
+ // Duplicate or empty name: factory side effects already ran once; skip hooks.
82
89
  if (this.instances.has(moduleInstance.name) || !moduleInstance.name) {
83
90
  continue;
84
91
  }
85
- // Register the module instance
86
92
  this.instances.set(moduleInstance.name, moduleInstance);
87
93
  instances.push(moduleInstance);
88
- // Resolve the module instances
89
94
  if (moduleInstance.dependencies) {
90
95
  instances = await this.resolveModuleInstances(moduleInstance.dependencies, instances);
91
96
  }
@@ -93,18 +98,20 @@ export class Launcher {
93
98
  return instances;
94
99
  }
95
100
  /**
96
- * Start the application
101
+ * Start (or continue starting) the application.
102
+ * Safe to call again after `modules([...])` adds more factories — only new
103
+ * modules run prepare → register → ready.
97
104
  */
98
105
  async start() {
99
106
  if (this.options.modules.length === 0) {
100
107
  throw new Error("No modules registered");
101
108
  }
102
- let instances = await this.resolveModuleInstances(this.options.modules);
109
+ const instances = await this.resolveModuleInstances(this.options.modules);
103
110
  for (const instance of instances) {
104
- instance.prepare?.();
111
+ await instance.prepare?.();
105
112
  }
106
113
  for (const instance of instances) {
107
- instance.register?.();
114
+ await instance.register?.();
108
115
  }
109
116
  for (const instance of instances) {
110
117
  instance.ready?.();
@@ -1 +1 @@
1
- {"version":3,"file":"launcher.service.js","sourceRoot":"","sources":["../../../src/launcher/services/launcher.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"launcher.service.js","sourceRoot":"","sources":["../../../src/launcher/services/launcher.service.ts"],"names":[],"mappings":"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"}
@@ -9,9 +9,9 @@ import { Launcher } from "../services";
9
9
  export type Module = {
10
10
  name: string;
11
11
  dependencies?: ModuleFactory[];
12
- prepare?: () => void;
13
- register?: () => void;
14
- ready?: () => void;
12
+ prepare?: () => void | Promise<void>;
13
+ register?: () => void | Promise<void>;
14
+ ready?: () => void | Promise<void>;
15
15
  };
16
16
  /**
17
17
  * A factory function that creates a module
@@ -1 +1 @@
1
- {"version":3,"file":"module.type.d.ts","sourceRoot":"","sources":["../../../src/launcher/types/module.type.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,MAAM,MAAM,GAAG;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,aAAa,EAAE,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,QAAQ,EAAE,QAAQ,KAAK,MAAM,CAAC;AAE3D;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B,CAAC"}
1
+ {"version":3,"file":"module.type.d.ts","sourceRoot":"","sources":["../../../src/launcher/types/module.type.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,MAAM,MAAM,GAAG;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,aAAa,EAAE,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,KAAK,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,QAAQ,EAAE,QAAQ,KAAK,MAAM,CAAC;AAE3D;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B,CAAC"}
package/package.json CHANGED
@@ -1,9 +1,33 @@
1
1
  {
2
2
  "name": "@toijs/modular",
3
- "version": "1.0.3",
3
+ "version": "1.0.6",
4
4
  "private": false,
5
5
  "type": "module",
6
- "description": "Lamtoi modular core (launcher, metadata, config, task, router)",
6
+ "description": "Isomorphic TypeScript composition pattern: modules, explicit DI, and Shell for Node.js and browser apps (Express, Vue, React, …)",
7
+ "keywords": [
8
+ "isomorphic",
9
+ "browser",
10
+ "nodejs",
11
+ "typescript",
12
+ "dependency-injection",
13
+ "di",
14
+ "container",
15
+ "module",
16
+ "modular",
17
+ "ioc",
18
+ "composition",
19
+ "launcher",
20
+ "shell"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/toijs/framework.git",
25
+ "directory": "modular"
26
+ },
27
+ "homepage": "https://github.com/toijs/framework/tree/develop/modular",
28
+ "bugs": {
29
+ "url": "https://github.com/toijs/framework/issues"
30
+ },
7
31
  "main": "dist/index.js",
8
32
  "types": "dist/index.d.ts",
9
33
  "exports": {