@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 +403 -0
- package/dist/launcher/services/launcher.service.d.ts +9 -4
- package/dist/launcher/services/launcher.service.d.ts.map +1 -1
- package/dist/launcher/services/launcher.service.js +17 -10
- package/dist/launcher/services/launcher.service.js.map +1 -1
- package/dist/launcher/types/module.type.d.ts +3 -3
- package/dist/launcher/types/module.type.d.ts.map +1 -1
- package/package.json +26 -2
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
|
+
[](https://www.npmjs.com/package/@toijs/modular)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://nodejs.org/api/esm.html)
|
|
7
|
+
[](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
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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;;
|
|
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
|
|
75
|
-
*
|
|
76
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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":"AACA,OAAO,EAAE,SAAS,EAAuB,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,MAAM,OAAO,QAAQ;IACZ,IAAI,CAAO;IACX,QAAQ,CAAW;IACnB,SAAS,CAAY;IACrB,KAAK,CAAQ;IACb,MAAM,CAAS;IACf,OAAO,GAAiB;QAC7B,OAAO,EAAE,EAAqB;KACf,CAAC;IACX,SAAS,GAAwB,IAAI,GAAG,EAAE,CAAC;
|
|
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;
|
|
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
|
+
"version": "1.0.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "
|
|
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": {
|