ioc-manifest 0.3.2 → 1.0.0
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 +389 -100
- package/dist/cli/ioc.js +30 -6
- package/dist/cli/ioc.js.map +1 -1
- package/dist/cli/parseIocCli.d.ts +10 -2
- package/dist/cli/parseIocCli.d.ts.map +1 -1
- package/dist/cli/parseIocCli.js +32 -5
- package/dist/cli/parseIocCli.js.map +1 -1
- package/dist/config/iocConfig.d.ts +25 -33
- package/dist/config/iocConfig.d.ts.map +1 -1
- package/dist/config/iocConfig.js.map +1 -1
- package/dist/config/iocMode.d.ts +9 -0
- package/dist/config/iocMode.d.ts.map +1 -0
- package/dist/config/iocMode.js +8 -0
- package/dist/config/iocMode.js.map +1 -0
- package/dist/config/loadIocConfig.d.ts.map +1 -1
- package/dist/config/loadIocConfig.js +162 -26
- package/dist/config/loadIocConfig.js.map +1 -1
- package/dist/config/packageIdentifier.d.ts +19 -0
- package/dist/config/packageIdentifier.d.ts.map +1 -0
- package/dist/config/packageIdentifier.js +87 -0
- package/dist/config/packageIdentifier.js.map +1 -0
- package/dist/config/parseDiscoveryScanDirs.d.ts.map +1 -1
- package/dist/config/parseDiscoveryScanDirs.js +13 -28
- package/dist/config/parseDiscoveryScanDirs.js.map +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/manifest.d.ts +16 -1
- package/dist/core/manifest.d.ts.map +1 -1
- package/dist/core/manifest.js +1 -1
- package/dist/core/manifest.js.map +1 -1
- package/dist/generator/analyzeDemandSupply/emitTypeReference.d.ts +15 -0
- package/dist/generator/analyzeDemandSupply/emitTypeReference.d.ts.map +1 -0
- package/dist/generator/analyzeDemandSupply/emitTypeReference.js +137 -0
- package/dist/generator/analyzeDemandSupply/emitTypeReference.js.map +1 -0
- package/dist/generator/analyzeDemandSupply/enforceNamedDepsType.d.ts +17 -0
- package/dist/generator/analyzeDemandSupply/enforceNamedDepsType.d.ts.map +1 -0
- package/dist/generator/analyzeDemandSupply/enforceNamedDepsType.js +108 -0
- package/dist/generator/analyzeDemandSupply/enforceNamedDepsType.js.map +1 -0
- package/dist/generator/analyzeDemandSupply/index.d.ts +17 -0
- package/dist/generator/analyzeDemandSupply/index.d.ts.map +1 -0
- package/dist/generator/analyzeDemandSupply/index.js +162 -0
- package/dist/generator/analyzeDemandSupply/index.js.map +1 -0
- package/dist/generator/analyzeDemandSupply/types.d.ts +24 -0
- package/dist/generator/analyzeDemandSupply/types.d.ts.map +1 -0
- package/dist/generator/analyzeDemandSupply/types.js +2 -0
- package/dist/generator/analyzeDemandSupply/types.js.map +1 -0
- package/dist/generator/buildComposedRegistrationOverrides.d.ts +7 -0
- package/dist/generator/buildComposedRegistrationOverrides.d.ts.map +1 -0
- package/dist/generator/buildComposedRegistrationOverrides.js +59 -0
- package/dist/generator/buildComposedRegistrationOverrides.js.map +1 -0
- package/dist/generator/contractTypeSourceFile.d.ts +2 -3
- package/dist/generator/contractTypeSourceFile.d.ts.map +1 -1
- package/dist/generator/contractTypeSourceFile.js +2 -85
- package/dist/generator/contractTypeSourceFile.js.map +1 -1
- package/dist/generator/discoverFactories/discoverFactories.d.ts.map +1 -1
- package/dist/generator/discoverFactories/discoverFactories.js +0 -1
- package/dist/generator/discoverFactories/discoverFactories.js.map +1 -1
- package/dist/generator/discoverFactories/scanFactoryFile.d.ts.map +1 -1
- package/dist/generator/discoverFactories/scanFactoryFile.js +0 -1
- package/dist/generator/discoverFactories/scanFactoryFile.js.map +1 -1
- package/dist/generator/generateManifest.d.ts.map +1 -1
- package/dist/generator/generateManifest.js +72 -7
- package/dist/generator/generateManifest.js.map +1 -1
- package/dist/generator/loadComposedManifestContracts.d.ts +9 -0
- package/dist/generator/loadComposedManifestContracts.d.ts.map +1 -0
- package/dist/generator/loadComposedManifestContracts.js +74 -0
- package/dist/generator/loadComposedManifestContracts.js.map +1 -0
- package/dist/generator/loadComposedManifestGroups.d.ts +15 -0
- package/dist/generator/loadComposedManifestGroups.d.ts.map +1 -0
- package/dist/generator/loadComposedManifestGroups.js +82 -0
- package/dist/generator/loadComposedManifestGroups.js.map +1 -0
- package/dist/generator/manifestOptions.d.ts.map +1 -1
- package/dist/generator/manifestOptions.js +1 -6
- package/dist/generator/manifestOptions.js.map +1 -1
- package/dist/generator/manifestPaths.d.ts +10 -35
- package/dist/generator/manifestPaths.d.ts.map +1 -1
- package/dist/generator/manifestPaths.js +25 -185
- package/dist/generator/manifestPaths.js.map +1 -1
- package/dist/generator/resolveComposedPackageExport.d.ts +11 -0
- package/dist/generator/resolveComposedPackageExport.d.ts.map +1 -0
- package/dist/generator/resolveComposedPackageExport.js +54 -0
- package/dist/generator/resolveComposedPackageExport.js.map +1 -0
- package/dist/generator/resolveRegistrationPlan.d.ts +4 -1
- package/dist/generator/resolveRegistrationPlan.d.ts.map +1 -1
- package/dist/generator/resolveRegistrationPlan.js +74 -7
- package/dist/generator/resolveRegistrationPlan.js.map +1 -1
- package/dist/generator/writeComposedManifest.d.ts +15 -0
- package/dist/generator/writeComposedManifest.d.ts.map +1 -0
- package/dist/generator/writeComposedManifest.js +183 -0
- package/dist/generator/writeComposedManifest.js.map +1 -0
- package/dist/generator/writeManifest.d.ts +16 -0
- package/dist/generator/writeManifest.d.ts.map +1 -1
- package/dist/generator/writeManifest.js +146 -74
- package/dist/generator/writeManifest.js.map +1 -1
- package/dist/groups/canonicalBaseTypeId.d.ts +27 -0
- package/dist/groups/canonicalBaseTypeId.d.ts.map +1 -0
- package/dist/groups/canonicalBaseTypeId.js +198 -0
- package/dist/groups/canonicalBaseTypeId.js.map +1 -0
- package/dist/groups/resolveGroupPlan.d.ts +4 -1
- package/dist/groups/resolveGroupPlan.d.ts.map +1 -1
- package/dist/groups/resolveGroupPlan.js +27 -6
- package/dist/groups/resolveGroupPlan.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/inspection/runDiscoveryAnalysis.js +2 -2
- package/dist/inspection/runDiscoveryAnalysis.js.map +1 -1
- package/dist/runtime/bootstrap.d.ts +2 -1
- package/dist/runtime/bootstrap.d.ts.map +1 -1
- package/dist/runtime/bootstrap.js +6 -4
- package/dist/runtime/bootstrap.js.map +1 -1
- package/dist/runtime/composeManifests.d.ts +52 -0
- package/dist/runtime/composeManifests.d.ts.map +1 -0
- package/dist/runtime/composeManifests.js +427 -0
- package/dist/runtime/composeManifests.js.map +1 -0
- package/dist/runtime/composedOverrides.d.ts +22 -0
- package/dist/runtime/composedOverrides.d.ts.map +1 -0
- package/dist/runtime/composedOverrides.js +2 -0
- package/dist/runtime/composedOverrides.js.map +1 -0
- package/dist/runtime/groupBaseTypeEquivalence.d.ts +9 -0
- package/dist/runtime/groupBaseTypeEquivalence.d.ts.map +1 -0
- package/dist/runtime/groupBaseTypeEquivalence.js +18 -0
- package/dist/runtime/groupBaseTypeEquivalence.js.map +1 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/schemaVersion.d.ts +7 -0
- package/dist/schemaVersion.d.ts.map +1 -0
- package/dist/schemaVersion.js +6 -0
- package/dist/schemaVersion.js.map +1 -0
- package/dist/test-support/manifestFixtures.d.ts +20 -0
- package/dist/test-support/manifestFixtures.d.ts.map +1 -0
- package/dist/test-support/manifestFixtures.js +49 -0
- package/dist/test-support/manifestFixtures.js.map +1 -0
- package/dist/validate/checks/appConfig.d.ts +4 -0
- package/dist/validate/checks/appConfig.d.ts.map +1 -0
- package/dist/validate/checks/appConfig.js +92 -0
- package/dist/validate/checks/appConfig.js.map +1 -0
- package/dist/validate/checks/defaultAmbiguity.d.ts +3 -0
- package/dist/validate/checks/defaultAmbiguity.d.ts.map +1 -0
- package/dist/validate/checks/defaultAmbiguity.js +81 -0
- package/dist/validate/checks/defaultAmbiguity.js.map +1 -0
- package/dist/validate/checks/externals.d.ts +3 -0
- package/dist/validate/checks/externals.d.ts.map +1 -0
- package/dist/validate/checks/externals.js +27 -0
- package/dist/validate/checks/externals.js.map +1 -0
- package/dist/validate/checks/groups.d.ts +3 -0
- package/dist/validate/checks/groups.d.ts.map +1 -0
- package/dist/validate/checks/groups.js +136 -0
- package/dist/validate/checks/groups.js.map +1 -0
- package/dist/validate/checks/sameKeyConflict.d.ts +3 -0
- package/dist/validate/checks/sameKeyConflict.d.ts.map +1 -0
- package/dist/validate/checks/sameKeyConflict.js +78 -0
- package/dist/validate/checks/sameKeyConflict.js.map +1 -0
- package/dist/validate/checks/schemaVersion.d.ts +3 -0
- package/dist/validate/checks/schemaVersion.d.ts.map +1 -0
- package/dist/validate/checks/schemaVersion.js +21 -0
- package/dist/validate/checks/schemaVersion.js.map +1 -0
- package/dist/validate/formatValidationReport.d.ts +11 -0
- package/dist/validate/formatValidationReport.d.ts.map +1 -0
- package/dist/validate/formatValidationReport.js +36 -0
- package/dist/validate/formatValidationReport.js.map +1 -0
- package/dist/validate/loadParsedManifests.d.ts +12 -0
- package/dist/validate/loadParsedManifests.d.ts.map +1 -0
- package/dist/validate/loadParsedManifests.js +109 -0
- package/dist/validate/loadParsedManifests.js.map +1 -0
- package/dist/validate/parseGeneratedSource.d.ts +9 -0
- package/dist/validate/parseGeneratedSource.d.ts.map +1 -0
- package/dist/validate/parseGeneratedSource.js +258 -0
- package/dist/validate/parseGeneratedSource.js.map +1 -0
- package/dist/validate/runValidate.d.ts +30 -0
- package/dist/validate/runValidate.d.ts.map +1 -0
- package/dist/validate/runValidate.js +61 -0
- package/dist/validate/runValidate.js.map +1 -0
- package/dist/validate/types.d.ts +63 -0
- package/dist/validate/types.d.ts.map +1 -0
- package/dist/validate/types.js +5 -0
- package/dist/validate/types.js.map +1 -0
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ioc-manifest
|
|
2
2
|
|
|
3
|
-
**Convention-based dependency discovery and codegen for [Awilix](https://github.com/jeffijoe/awilix).** Write factory functions, run the generator, get a fully typed IoC container — no manual registrations.
|
|
3
|
+
**Convention-based dependency discovery and codegen for [Awilix](https://github.com/jeffijoe/awilix).** Write factory functions, run the generator, get a fully typed IoC container — no manual registrations. Compose containers across packages in a monorepo with first-class support.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
npm install ioc-manifest
|
|
@@ -12,14 +12,22 @@ npm install ioc-manifest
|
|
|
12
12
|
|
|
13
13
|
In most Node.js DI setups, every new service means another `container.register(...)` call, another import, another string key to keep in sync. Scale that to 50+ services and registration code becomes a maintenance burden. Awilix's `loadModules` helps, but you lose type safety — `container.resolve("userService")` returns `any` unless you maintain a cradle type by hand.
|
|
14
14
|
|
|
15
|
+
And once you have more than one package in a monorepo, the registration story gets worse: either one app's bootstrap scans into every other package's source (fragile, fights TypeScript's module resolution), or you duplicate registration glue everywhere.
|
|
16
|
+
|
|
15
17
|
## What this does
|
|
16
18
|
|
|
17
|
-
`ioc-manifest` scans your TypeScript source at **build time**, discovers factory functions by naming convention, infers their contracts and dependencies from the type system, and generates
|
|
19
|
+
`ioc-manifest` scans your TypeScript source at **build time**, discovers factory functions by naming convention, infers their contracts and dependencies from the type system, and generates manifest and types files that hand directly to Awilix.
|
|
20
|
+
|
|
21
|
+
For a single-package project, that's two generated files:
|
|
18
22
|
|
|
19
23
|
1. **`ioc-manifest.ts`** — a registration manifest with every factory, its contract, lifetime, and module import
|
|
20
|
-
2. **`ioc-registry.types.ts`** — a fully typed `IocGeneratedCradle` interface for your container
|
|
24
|
+
2. **`ioc-registry.types.ts`** — a fully typed `IocGeneratedCradle` interface for your container, plus an `IocExternals` interface describing dependencies the package expects from outside
|
|
25
|
+
|
|
26
|
+
For a monorepo where one app composes manifests from multiple packages, a third file appears in the app:
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
3. **`ioc-composed.ts`** — the composition glue: imports each package's manifest, intersects their cradle types into a single `AppCradle`, and emits compile-time assertions that every package's externals are satisfied.
|
|
29
|
+
|
|
30
|
+
Every factory is registered with the correct key and lifetime, the container is fully typed end-to-end, and you never write a registration line again.
|
|
23
31
|
|
|
24
32
|
The approach is loosely inspired by [StructureMap](https://structuremap.github.io/)'s registry scanning conventions from the .NET world — convention over configuration, with a single config file as the policy surface when you need to override defaults.
|
|
25
33
|
|
|
@@ -29,15 +37,35 @@ The approach is loosely inspired by [StructureMap](https://structuremap.github.i
|
|
|
29
37
|
- **Typed container** — `container.resolve("userService")` returns `UserService`, not `any`
|
|
30
38
|
- **Plural collections** — two implementations of `MediaStorage` automatically get a `mediaStorages: ReadonlyArray<MediaStorage>` key
|
|
31
39
|
- **Default selection** — convention picks the default; override in config when you have multiple implementations
|
|
40
|
+
- **Externals are explicit** — every dependency a package expects from outside is tracked in a generated `IocExternals` interface
|
|
41
|
+
- **Cross-package composition** — apps in a monorepo can compose manifests from multiple packages with no scanning across boundaries
|
|
42
|
+
- **Compile-time satisfaction checks** — when composing, TypeScript fails compilation if any composed package's externals aren't satisfied
|
|
43
|
+
- **`ioc validate`** — a CI-friendly command that reports every cross-manifest problem at once
|
|
32
44
|
- **Works in dev and prod** — discovers from TypeScript source during development, and works just as well against a bundled single-file production build (see [Dev and production builds](#dev-and-production-builds))
|
|
33
45
|
|
|
34
46
|
---
|
|
35
47
|
|
|
48
|
+
## Library mode vs app mode
|
|
49
|
+
|
|
50
|
+
`ioc-manifest` has two modes. Which one applies depends on a single config field: `composedManifests`.
|
|
51
|
+
|
|
52
|
+
**Library mode** is the default. A package generates its own manifest and types. Factories in that package can declare dependencies on things the package itself supplies (other local factories) _and_ on things it expects from outside (externals). The generated `IocExternals` interface documents the external contract — what the package needs to be handed at composition time.
|
|
53
|
+
|
|
54
|
+
**App mode** is what you turn on when a package composes manifests from other packages. The app declares `composedManifests: ['@scope/pkg-a', '@scope/pkg-b']` in its config. Codegen produces the extra `ioc-composed.ts` file, intersects the participating cradle types, and emits the compile-time assertion that every composed package's externals are satisfied somewhere in the composition.
|
|
55
|
+
|
|
56
|
+
A single-package project stays in library mode and never thinks about composition. A monorepo with one or more apps that consume shared packages has library-mode packages and one or more app-mode apps.
|
|
57
|
+
|
|
58
|
+
The quick start below walks through library mode. App mode is covered in [Cross-package composition](#cross-package-composition).
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
36
62
|
## Quick start
|
|
37
63
|
|
|
64
|
+
This walks through a single-package setup in library mode.
|
|
65
|
+
|
|
38
66
|
### 1. Create factories
|
|
39
67
|
|
|
40
|
-
Write plain factory functions. The naming convention `build<Name>` is the only requirement.
|
|
68
|
+
Write plain factory functions. The naming convention `build<Name>` is the only requirement. Each factory's first parameter is a **named local deps type** describing what it consumes.
|
|
41
69
|
|
|
42
70
|
```ts
|
|
43
71
|
// src/services/buildUserRepository.ts
|
|
@@ -53,20 +81,23 @@ export const buildUserRepository = (): UserRepository => ({
|
|
|
53
81
|
```ts
|
|
54
82
|
// src/services/buildUserService.ts
|
|
55
83
|
import type { UserRepository } from "./buildUserRepository.js";
|
|
56
|
-
import type { IocGeneratedCradle } from "../generated/ioc-registry.types.js";
|
|
57
84
|
|
|
58
85
|
export type UserService = {
|
|
59
86
|
getUser: (id: string) => Promise<User | undefined>;
|
|
60
87
|
};
|
|
61
88
|
|
|
89
|
+
type UserServiceDeps = {
|
|
90
|
+
userRepository: UserRepository;
|
|
91
|
+
};
|
|
92
|
+
|
|
62
93
|
export const buildUserService = ({
|
|
63
94
|
userRepository,
|
|
64
|
-
}:
|
|
95
|
+
}: UserServiceDeps): UserService => ({
|
|
65
96
|
getUser: (id) => userRepository.findById(id),
|
|
66
97
|
});
|
|
67
98
|
```
|
|
68
99
|
|
|
69
|
-
|
|
100
|
+
The named deps type pattern is required — factories cannot destructure directly from `IocGeneratedCradle`. There's a real reason for this: the cradle is generated _from_ what your factories declare. Asking a factory to declare its inputs by referencing the cradle would be a chicken-and-egg loop. Naming the deps type at the factory site also makes each factory independently testable — see [Testing](#testing) below.
|
|
70
101
|
|
|
71
102
|
### 2. Configure
|
|
72
103
|
|
|
@@ -111,39 +142,46 @@ const container = createContainer<IocGeneratedCradle>({
|
|
|
111
142
|
injectionMode: InjectionMode.PROXY,
|
|
112
143
|
});
|
|
113
144
|
|
|
114
|
-
registerIocFromManifest(container, iocManifest);
|
|
145
|
+
registerIocFromManifest(container, [iocManifest]);
|
|
115
146
|
|
|
116
147
|
// Fully typed — no 'any', no string guessing
|
|
117
148
|
const userService = container.resolve("userService");
|
|
118
149
|
```
|
|
119
150
|
|
|
120
|
-
|
|
151
|
+
Note that `registerIocFromManifest` takes an **array** of manifests, even when there's only one. The array is set-like — ordering is irrelevant, and the same input always produces the same registrations.
|
|
152
|
+
|
|
153
|
+
That's all you need for most single-package applications. The sections below cover the conventions in more detail. For monorepo composition, see [Cross-package composition](#cross-package-composition).
|
|
121
154
|
|
|
122
155
|
---
|
|
123
156
|
|
|
124
157
|
## What gets generated
|
|
125
158
|
|
|
126
|
-
Here's what
|
|
159
|
+
Here's what library-mode output looks like for a small app. You never edit these files — they're regenerated from source.
|
|
127
160
|
|
|
128
|
-
**`ioc-registry.types.ts`** — the typed cradle:
|
|
161
|
+
**`ioc-registry.types.ts`** — the typed cradle and externals:
|
|
129
162
|
|
|
130
163
|
```ts
|
|
131
164
|
/* AUTO-GENERATED. DO NOT EDIT. */
|
|
132
165
|
import type { Logger } from "../services/buildConsoleLogger.js";
|
|
133
166
|
import type { MediaStorage } from "../services/buildLocalMediaStorage.js";
|
|
134
167
|
import type { UserService } from "../services/buildUserService.js";
|
|
168
|
+
import type { Database } from "../types/Database.js";
|
|
135
169
|
|
|
136
|
-
export interface
|
|
170
|
+
export interface IocGeneratedCradle {
|
|
137
171
|
logger: Logger;
|
|
138
172
|
mediaStorage: MediaStorage;
|
|
139
173
|
mediaStorages: ReadonlyArray<MediaStorage>;
|
|
140
174
|
userService: UserService;
|
|
141
175
|
}
|
|
142
176
|
|
|
143
|
-
export
|
|
177
|
+
export interface IocExternals {
|
|
178
|
+
database: Database;
|
|
179
|
+
}
|
|
144
180
|
```
|
|
145
181
|
|
|
146
|
-
|
|
182
|
+
`mediaStorages` (plural) appears automatically because there are multiple `MediaStorage` implementations.
|
|
183
|
+
|
|
184
|
+
`IocExternals` lists every dependency the package consumes from outside — keys destructured by factory deps types where no local factory supplies them. `IocGeneratedCradle` contains only what the package itself supplies. The two interfaces together describe the package's full contract: what it provides and what it needs.
|
|
147
185
|
|
|
148
186
|
**`ioc-manifest.ts`** — the registration data:
|
|
149
187
|
|
|
@@ -159,6 +197,7 @@ import * as ioc_services_buildLocalMediaStorage from "../services/buildLocalMedi
|
|
|
159
197
|
// ... more imports ...
|
|
160
198
|
|
|
161
199
|
export const iocManifest = {
|
|
200
|
+
manifestSchemaVersion: 2,
|
|
162
201
|
moduleImports: [
|
|
163
202
|
/* ... */
|
|
164
203
|
] as const satisfies readonly IocModuleNamespace[],
|
|
@@ -201,9 +240,10 @@ The contract type must be a named type (interface or type alias) that is importe
|
|
|
201
240
|
|
|
202
241
|
When a contract has only one implementation, it is the default. When there are multiple, the default is selected by this precedence:
|
|
203
242
|
|
|
204
|
-
1. **
|
|
205
|
-
2. **
|
|
206
|
-
3. **
|
|
243
|
+
1. **App override** — `default: true` in an app-mode `ioc.config` (highest precedence; only relevant when composing)
|
|
244
|
+
2. **Explicit** — `default: true` on exactly one implementation in the local `ioc.config`
|
|
245
|
+
3. **Convention** — the implementation whose registration key equals the camel-cased contract name (e.g. `mediaStorage` for `MediaStorage`)
|
|
246
|
+
4. **Single** — if only one implementation exists, it's the default
|
|
207
247
|
|
|
208
248
|
If the choice is ambiguous, generation fails with a clear error telling you what to do.
|
|
209
249
|
|
|
@@ -222,7 +262,9 @@ This is the same fundamental idea behind having multiple implementations of a si
|
|
|
222
262
|
|
|
223
263
|
### Dependency inference
|
|
224
264
|
|
|
225
|
-
The generator analyzes each factory's first parameter to determine which
|
|
265
|
+
The generator analyzes each factory's first parameter — the named deps type — to determine which keys the factory consumes. Every property in the deps type becomes a **demand**. If a demanded key has a corresponding `build*` factory in the same package, it's a local dependency. If not, it's an external (and appears in `IocExternals`).
|
|
266
|
+
|
|
267
|
+
Codegen validates type agreement across factories: if `buildA` declares `database: Knex` and `buildB` declares `database: PostgresClient`, codegen fails with both locations and the conflicting types named.
|
|
226
268
|
|
|
227
269
|
---
|
|
228
270
|
|
|
@@ -243,19 +285,22 @@ export default defineIocConfig({
|
|
|
243
285
|
groups: {
|
|
244
286
|
/* cross-contract grouping by base type (advanced) */
|
|
245
287
|
},
|
|
288
|
+
// app mode only:
|
|
289
|
+
composedManifests: [
|
|
290
|
+
/* package names to compose */
|
|
291
|
+
],
|
|
246
292
|
});
|
|
247
293
|
```
|
|
248
294
|
|
|
249
295
|
### `discovery`
|
|
250
296
|
|
|
251
|
-
| Field
|
|
252
|
-
|
|
|
253
|
-
| `scanDirs`
|
|
254
|
-
| `includes`
|
|
255
|
-
| `excludes`
|
|
256
|
-
| `factoryPrefix`
|
|
257
|
-
| `generatedDir`
|
|
258
|
-
| `workspacePackageImportBases` | Maps workspace roots to bare specifiers for generated imports (see [Monorepo support](#monorepo-support-importprefix)). | — |
|
|
297
|
+
| Field | Purpose | Default |
|
|
298
|
+
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
|
299
|
+
| `scanDirs` | **Required.** Directories to scan. String, string array, or array of `{ path, scope? }` objects. Paths must resolve within the package root. | — |
|
|
300
|
+
| `includes` | Glob patterns for files to include. | `["**/*.{ts,tsx,js,mjs,cjs}"]` |
|
|
301
|
+
| `excludes` | Glob patterns for files to exclude. | `["**/*.d.ts", "**/*.test.ts", ...]` |
|
|
302
|
+
| `factoryPrefix` | Export name prefix for factory discovery. | `"build"` |
|
|
303
|
+
| `generatedDir` | Output directory for generated files. | `"generated"` |
|
|
259
304
|
|
|
260
305
|
### `registrations`
|
|
261
306
|
|
|
@@ -276,16 +321,201 @@ registrations: {
|
|
|
276
321
|
|
|
277
322
|
Under each contract name, keys are implementation names from discovery (`buildFoo` → `foo`). The reserved `$contract` key holds contract-level options.
|
|
278
323
|
|
|
279
|
-
| Per-implementation field | Effect
|
|
280
|
-
| ------------------------ |
|
|
281
|
-
| `name` | Overrides the Awilix registration key
|
|
282
|
-
| `lifetime` | `"singleton"` \| `"scoped"` \| `"transient"`
|
|
283
|
-
| `default` | `true` to select this implementation as the contract default
|
|
324
|
+
| Per-implementation field | Effect |
|
|
325
|
+
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
326
|
+
| `name` | Overrides the Awilix registration key |
|
|
327
|
+
| `lifetime` | `"singleton"` \| `"scoped"` \| `"transient"` |
|
|
328
|
+
| `default` | `true` to select this implementation as the contract default |
|
|
329
|
+
| `source` | (app mode only) Resolve same-key conflicts across composed manifests. See [Cross-package composition](#cross-package-composition). |
|
|
284
330
|
|
|
285
331
|
| `$contract` field | Effect |
|
|
286
332
|
| ----------------- | ----------------------------------------------------------------------------------------------- |
|
|
287
333
|
| `accessKey` | Overrides the cradle property name for the default slot (e.g. `"database"` instead of `"knex"`) |
|
|
288
334
|
|
|
335
|
+
### App-mode fields
|
|
336
|
+
|
|
337
|
+
These only apply in app mode (a package that composes manifests from other packages):
|
|
338
|
+
|
|
339
|
+
| Field | Purpose |
|
|
340
|
+
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
341
|
+
| `composedManifests` | Array of package names whose manifests this app composes. Setting this turns on app mode. |
|
|
342
|
+
| `packageName` | The local package's npm name. Used for self-reference detection. Falls back to `package.json` `name`; required if neither is available. |
|
|
343
|
+
| `groupBaseTypeAliases` | Equivalence sets for canonical base type identifiers when hoisting produces mismatches. See [Cross-package composition](#cross-package-composition). |
|
|
344
|
+
|
|
345
|
+
| Library-mode-only field | Purpose |
|
|
346
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
347
|
+
| `manifestExportPath` | Informational. The path your `package.json` `exports` points at for the manifest. Default `./generated/ioc-manifest`. |
|
|
348
|
+
|
|
349
|
+
`composedManifests` and `manifestExportPath` are mutually exclusive — a config is either library or app mode.
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Cross-package composition
|
|
354
|
+
|
|
355
|
+
Once you have more than one package in a monorepo, you typically have one or more apps that compose manifests from shared libraries. This is what app mode is for.
|
|
356
|
+
|
|
357
|
+
### The model
|
|
358
|
+
|
|
359
|
+
Each package generates its own manifest in library mode, scanning only its own source. The app's config declares which packages it composes with via `composedManifests`. Codegen produces an extra file in the app — `ioc-composed.ts` — that imports each package's manifest, intersects their cradle types, and emits compile-time assertions that every composed package's externals are satisfied.
|
|
360
|
+
|
|
361
|
+
At runtime, the app passes the composed manifests array to `registerIocFromManifest`. Composition is set-like: ordering doesn't matter. Conflicts (two manifests supplying the same registration key) are hard errors by default, resolved via explicit `source` config.
|
|
362
|
+
|
|
363
|
+
### A monorepo example
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
packages/
|
|
367
|
+
lib-storage/ # library mode
|
|
368
|
+
src/
|
|
369
|
+
ioc.config.ts
|
|
370
|
+
factories/
|
|
371
|
+
types/
|
|
372
|
+
lib-services/ # library mode
|
|
373
|
+
src/
|
|
374
|
+
ioc.config.ts
|
|
375
|
+
factories/
|
|
376
|
+
types/
|
|
377
|
+
app/ # app mode
|
|
378
|
+
src/
|
|
379
|
+
ioc.config.ts
|
|
380
|
+
bootstrap.ts
|
|
381
|
+
factories/
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
`lib-storage` registers `Storage` implementations. `lib-services` registers services that consume `Storage` (declared in their deps types — so `storage` appears in `lib-services`'s `IocExternals`). The app composes both and supplies anything neither library supplies.
|
|
385
|
+
|
|
386
|
+
### App config
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
// packages/app/src/ioc.config.ts
|
|
390
|
+
import { defineIocConfig } from "ioc-manifest";
|
|
391
|
+
|
|
392
|
+
export default defineIocConfig({
|
|
393
|
+
discovery: {
|
|
394
|
+
scanDirs: "src",
|
|
395
|
+
generatedDir: "generated",
|
|
396
|
+
},
|
|
397
|
+
composedManifests: ["@example/lib-storage", "@example/lib-services"],
|
|
398
|
+
registrations: {
|
|
399
|
+
Storage: {
|
|
400
|
+
s3Storage: { default: true },
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Required package exports
|
|
407
|
+
|
|
408
|
+
Each composed package's `package.json` must expose two subpath exports:
|
|
409
|
+
|
|
410
|
+
```jsonc
|
|
411
|
+
{
|
|
412
|
+
"exports": {
|
|
413
|
+
".": "./src/index.ts",
|
|
414
|
+
"./iocManifest": "./src/generated/ioc-manifest.js",
|
|
415
|
+
"./iocTypes": "./src/generated/ioc-registry.types.js",
|
|
416
|
+
},
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
(Substitute `./dist/...` for published packages with a build step.)
|
|
421
|
+
|
|
422
|
+
### Generated `ioc-composed.ts`
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
/* AUTO-GENERATED. DO NOT EDIT. */
|
|
426
|
+
import { iocManifest as localManifest } from "./ioc-manifest.js";
|
|
427
|
+
import { iocManifest as libStorageManifest } from "@example/lib-storage/iocManifest";
|
|
428
|
+
import { iocManifest as libServicesManifest } from "@example/lib-services/iocManifest";
|
|
429
|
+
|
|
430
|
+
import type { IocGeneratedCradle as LocalCradle } from "./ioc-registry.types.js";
|
|
431
|
+
import type { IocGeneratedCradle as LibStorageCradle } from "@example/lib-storage/iocTypes";
|
|
432
|
+
import type { IocGeneratedCradle as LibServicesCradle } from "@example/lib-services/iocTypes";
|
|
433
|
+
import type { IocExternals as LibStorageExternals } from "@example/lib-storage/iocTypes";
|
|
434
|
+
import type { IocExternals as LibServicesExternals } from "@example/lib-services/iocTypes";
|
|
435
|
+
|
|
436
|
+
export const composedManifests = [
|
|
437
|
+
localManifest,
|
|
438
|
+
libStorageManifest,
|
|
439
|
+
libServicesManifest,
|
|
440
|
+
] as const;
|
|
441
|
+
|
|
442
|
+
export type AppCradle = LocalCradle & LibStorageCradle & LibServicesCradle;
|
|
443
|
+
|
|
444
|
+
// Compile-time externals satisfaction assertions
|
|
445
|
+
type _IocExpect<T extends true> = T;
|
|
446
|
+
type _LibStorageExternalsSatisfied =
|
|
447
|
+
LibStorageExternals extends Pick<AppCradle, keyof LibStorageExternals>
|
|
448
|
+
? true
|
|
449
|
+
: false;
|
|
450
|
+
type _LibStorageExternalsAssert = _IocExpect<_LibStorageExternalsSatisfied>;
|
|
451
|
+
type _LibServicesExternalsSatisfied =
|
|
452
|
+
LibServicesExternals extends Pick<AppCradle, keyof LibServicesExternals>
|
|
453
|
+
? true
|
|
454
|
+
: false;
|
|
455
|
+
type _LibServicesExternalsAssert = _IocExpect<_LibServicesExternalsSatisfied>;
|
|
456
|
+
|
|
457
|
+
export const composedRegistrationOverrides = {
|
|
458
|
+
/* ... */
|
|
459
|
+
};
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
If `lib-services` requires a `logger` and no manifest in the composition supplies it, `_LibServicesExternalsAssert` fails compilation with a TypeScript error pointing at the assertion line. You don't have to run anything to find out you forgot something.
|
|
463
|
+
|
|
464
|
+
### App bootstrap
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
import { createContainer } from "awilix";
|
|
468
|
+
import { registerIocFromManifest } from "ioc-manifest";
|
|
469
|
+
import {
|
|
470
|
+
composedManifests,
|
|
471
|
+
composedRegistrationOverrides,
|
|
472
|
+
type AppCradle,
|
|
473
|
+
} from "./generated/ioc-composed.js";
|
|
474
|
+
|
|
475
|
+
const container = createContainer<AppCradle>();
|
|
476
|
+
registerIocFromManifest(
|
|
477
|
+
container,
|
|
478
|
+
composedManifests,
|
|
479
|
+
composedRegistrationOverrides,
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
const uploadService = container.resolve("uploadService");
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Resolving same-key conflicts
|
|
486
|
+
|
|
487
|
+
If two composed manifests both supply the same Awilix registration key, composition fails with a hard error naming both manifests. Resolve via the `source` field:
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
registrations: {
|
|
491
|
+
AlbumRepository: {
|
|
492
|
+
albumRepository: { source: "local" }, // or "@example/lib-services"
|
|
493
|
+
},
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
`source: "local"` picks the app's own factory; a package name picks that package's registration. There's no last-write-wins or array-position semantics — you decide explicitly which manifest's registration wins.
|
|
498
|
+
|
|
499
|
+
### Groups across manifests
|
|
500
|
+
|
|
501
|
+
If multiple composed packages declare contributors to the same group (e.g. several packages register `DiscountStrategy` implementations and all declare a `discountStrategies` collection group), the group merges across manifests. `container.resolve("discountStrategies")` returns the union.
|
|
502
|
+
|
|
503
|
+
For this to work, all contributors must reference the same canonical base type — typically by importing it from a shared contracts package. If npm hoisting produces a single physical file for the base type, identity matching works automatically.
|
|
504
|
+
|
|
505
|
+
In rare cases (version skew, peer-dep conflicts, nested installs) two contributors may end up with different physical paths for what is structurally the same type. The library reports this with a clear error and a remediation hint, including the exact config block to paste:
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
// in the app's ioc.config.ts
|
|
509
|
+
groupBaseTypeAliases: {
|
|
510
|
+
discountStrategies: [
|
|
511
|
+
"/path/to/a.ts:DiscountStrategy",
|
|
512
|
+
"/path/to/b.ts:DiscountStrategy",
|
|
513
|
+
],
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
The library treats the listed identifiers as equivalent. This is an escape hatch, not a normal-path mechanism.
|
|
518
|
+
|
|
289
519
|
---
|
|
290
520
|
|
|
291
521
|
## Dev and production builds
|
|
@@ -304,21 +534,106 @@ This was a deliberate design choice (and a painful one to get right). There's no
|
|
|
304
534
|
|
|
305
535
|
```bash
|
|
306
536
|
npx ioc # prints help
|
|
307
|
-
npx ioc generate # discover factories, emit manifest + types
|
|
537
|
+
npx ioc generate # discover factories, emit manifest + types (and ioc-composed.ts in app mode)
|
|
308
538
|
npx ioc generate -c ./ioc.config.test.ts # generate with a specific config
|
|
309
539
|
npx ioc inspect # loads the generated manifest and prints a summary
|
|
310
540
|
npx ioc inspect --discovery # re-runs discovery without reading the manifest
|
|
311
|
-
npx ioc
|
|
541
|
+
npx ioc validate # app mode: cross-manifest checks against composedManifests
|
|
542
|
+
npx ioc validate --json # machine-readable issue list
|
|
312
543
|
```
|
|
313
544
|
|
|
314
545
|
| Flag | Purpose |
|
|
315
546
|
| -------------------------- | --------------------------------------------------------------------------------------- |
|
|
316
547
|
| `--discovery` | (inspect only) Re-run factory discovery and planning; don't read the generated manifest |
|
|
548
|
+
| `--json` | (validate only) Emit issues as JSON |
|
|
317
549
|
| `--config PATH`, `-c PATH` | Explicit path to `ioc.config.ts` |
|
|
318
550
|
| `--project PATH` | Project directory for config resolution (default: cwd) |
|
|
319
551
|
|
|
320
552
|
Set `IOC_DEBUG=1` for full stack traces on errors.
|
|
321
553
|
|
|
554
|
+
### `ioc validate`
|
|
555
|
+
|
|
556
|
+
A separate command from `generate` because they have different audiences. `generate` runs frequently during development and shouldn't fail on transient inconsistencies (a sibling package mid-refactor). `validate` is the pre-merge / pre-deploy gate.
|
|
557
|
+
|
|
558
|
+
`validate` loads every composed manifest, runs every cross-manifest check at once, and reports all issues — not just the first. It does not modify any files; pure inspection. Exit code is non-zero if any error-severity issue is reported.
|
|
559
|
+
|
|
560
|
+
Typical output for a failing run:
|
|
561
|
+
|
|
562
|
+
```
|
|
563
|
+
[app-config] registrations references unknown contract "Storge"
|
|
564
|
+
Known local contracts: Logger.
|
|
565
|
+
Known composed contracts: Logger, LoggingService, Storage, UploadService.
|
|
566
|
+
Did you mean: "Storage"?
|
|
567
|
+
Suggested fix: Fix the contract name in ioc.config.ts registrations, or add a factory for "Storge".
|
|
568
|
+
|
|
569
|
+
Validation failed: 1 error, 0 warnings.
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
Library-mode invocations print an informational message and exit 0 — there's nothing cross-manifest to validate.
|
|
573
|
+
|
|
574
|
+
Recommended workflow: `ioc generate` → `ioc validate` → `tsc --noEmit` → deploy.
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## Testing
|
|
579
|
+
|
|
580
|
+
The named-deps-type pattern at the factory site enables three levels of testing, each with the ergonomics that fit:
|
|
581
|
+
|
|
582
|
+
### Factory-level (no container)
|
|
583
|
+
|
|
584
|
+
Most unit tests don't need a container. Import the factory, import its deps type, hand-build a stub, call the factory:
|
|
585
|
+
|
|
586
|
+
```ts
|
|
587
|
+
import { buildValidateOperationService } from "../src/...";
|
|
588
|
+
import type { ValidateOperationServiceDeps } from "../src/...";
|
|
589
|
+
|
|
590
|
+
const deps: ValidateOperationServiceDeps = {
|
|
591
|
+
mediaItemReadRepository: {
|
|
592
|
+
/* stub */
|
|
593
|
+
},
|
|
594
|
+
grantReadRepository: {
|
|
595
|
+
/* stub */
|
|
596
|
+
},
|
|
597
|
+
albumMemberReadRepository: {
|
|
598
|
+
/* stub */
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
const svc = buildValidateOperationService(deps);
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
No container, no manifest, no awilix. TypeScript enforces what must be provided.
|
|
605
|
+
|
|
606
|
+
### Container-level with mocked externals
|
|
607
|
+
|
|
608
|
+
When you want the full container — testing wiring, lifetimes, multi-service interactions inside the package — register the package's manifest then fill `IocExternals` with `asValue` stubs:
|
|
609
|
+
|
|
610
|
+
```ts
|
|
611
|
+
import { createContainer, asValue } from "awilix";
|
|
612
|
+
import { registerIocFromManifest } from "ioc-manifest";
|
|
613
|
+
import { iocManifest } from "../src/generated/ioc-manifest.js";
|
|
614
|
+
import type {
|
|
615
|
+
IocGeneratedCradle,
|
|
616
|
+
IocExternals,
|
|
617
|
+
} from "../src/generated/ioc-registry.types.js";
|
|
618
|
+
|
|
619
|
+
const container = createContainer<IocGeneratedCradle>();
|
|
620
|
+
registerIocFromManifest(container, [iocManifest]);
|
|
621
|
+
|
|
622
|
+
const externals: IocExternals = {
|
|
623
|
+
database: mockKnex,
|
|
624
|
+
logger: silentLogger,
|
|
625
|
+
};
|
|
626
|
+
for (const [k, v] of Object.entries(externals)) {
|
|
627
|
+
container.register({ [k]: asValue(v) });
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
The `IocExternals` type makes the external surface a typed checklist: forget one and TypeScript errors; add a new external dep in the package and every test breaks until updated.
|
|
632
|
+
|
|
633
|
+
### Test-specific manifest
|
|
634
|
+
|
|
635
|
+
For shared stubs across many tests, write stub factories under `tests/stubs/` and a separate `ioc.config.test.ts` scanning both `src` and `tests/stubs`. Generate a test manifest. Use as above. Run with `npx ioc generate -c ./ioc.config.test.ts`.
|
|
636
|
+
|
|
322
637
|
---
|
|
323
638
|
|
|
324
639
|
## Error handling
|
|
@@ -327,7 +642,9 @@ Errors are designed to tell you exactly what went wrong and what to do about it.
|
|
|
327
642
|
|
|
328
643
|
**Config errors** are prefixed `[ioc-config]` — unknown contracts in `registrations`, duplicate defaults, key collisions. These fail at generation time before any files are written.
|
|
329
644
|
|
|
330
|
-
**Discovery errors** are prefixed `[ioc]` — duplicate registration keys, unresolvable contract types, overlapping scan directories with conflicting scopes.
|
|
645
|
+
**Discovery errors** are prefixed `[ioc]` — duplicate registration keys, unresolvable contract types, overlapping scan directories with conflicting scopes, factories destructuring directly from `IocGeneratedCradle` (use named deps types instead).
|
|
646
|
+
|
|
647
|
+
**Validation errors** are prefixed by category (`[externals]`, `[same-key-conflict]`, `[group-base-type]`, etc.) and emitted by `ioc validate`. Validate aggregates: a failing run reports every issue at once, not just the first.
|
|
331
648
|
|
|
332
649
|
**Runtime resolution errors** use `IocResolutionError` with structured dependency chains:
|
|
333
650
|
|
|
@@ -402,9 +719,13 @@ groups: {
|
|
|
402
719
|
Now `container.resolve("discountStrategies")` gives you `ReadonlyArray<DiscountStrategy>` — every implementation that's assignable to the base type, discovered automatically. Your strategy runner just iterates through the array:
|
|
403
720
|
|
|
404
721
|
```ts
|
|
722
|
+
type PricingEngineDeps = {
|
|
723
|
+
discountStrategies: ReadonlyArray<DiscountStrategy>;
|
|
724
|
+
};
|
|
725
|
+
|
|
405
726
|
export const buildPricingEngine = ({
|
|
406
727
|
discountStrategies,
|
|
407
|
-
}:
|
|
728
|
+
}: PricingEngineDeps): PricingEngine => ({
|
|
408
729
|
applyDiscounts: (order) => {
|
|
409
730
|
for (const strategy of discountStrategies) {
|
|
410
731
|
if (strategy.applies(order)) {
|
|
@@ -418,6 +739,8 @@ export const buildPricingEngine = ({
|
|
|
418
739
|
|
|
419
740
|
Add a sixth strategy? Just create the factory. It shows up in the group automatically — no registration changes.
|
|
420
741
|
|
|
742
|
+
If you need strategies to run in a specific order, put ordering metadata on the strategy interface itself (e.g. a `priority` field) and sort at use time. The library never tries to order group members.
|
|
743
|
+
|
|
421
744
|
#### Object groups: bundling related services
|
|
422
745
|
|
|
423
746
|
Object groups are for when you have several services that implement a common base type and you want to access them as a keyed bundle rather than an array. A real example: in a GraphQL API, you might have a set of user-scoped read services that all need to be available on the resolver context:
|
|
@@ -445,75 +768,29 @@ Now `container.resolve("readServices")` returns an object keyed by each contract
|
|
|
445
768
|
|
|
446
769
|
#### Group validation
|
|
447
770
|
|
|
448
|
-
The generator validates that group names don't collide with implementation keys, access keys, or collection keys. If a base type has no assignable implementations, generation fails with an actionable error.
|
|
771
|
+
The generator validates that group names don't collide with implementation keys, access keys, or collection keys. If a base type has no assignable implementations, generation fails with an actionable error. Cross-manifest group composition is covered in [Cross-package composition](#cross-package-composition).
|
|
449
772
|
|
|
450
|
-
###
|
|
451
|
-
|
|
452
|
-
In a monorepo, factories in one package often return types defined in another. Without configuration, the generated manifest would emit deep relative paths like `../../../packages/shared/src/types/UserService.js` — fragile and ugly.
|
|
453
|
-
|
|
454
|
-
`importPrefix` and `importMode` fix this. They tell the generator how to write import statements for factories discovered under a given scan root:
|
|
455
|
-
|
|
456
|
-
```ts
|
|
457
|
-
discovery: {
|
|
458
|
-
scanDirs: [
|
|
459
|
-
{
|
|
460
|
-
path: "packages/shared/src",
|
|
461
|
-
importPrefix: "@acme/shared",
|
|
462
|
-
importMode: "subpath",
|
|
463
|
-
},
|
|
464
|
-
{
|
|
465
|
-
path: "packages/api/src",
|
|
466
|
-
importPrefix: "@acme/api",
|
|
467
|
-
importMode: "subpath",
|
|
468
|
-
},
|
|
469
|
-
],
|
|
470
|
-
},
|
|
471
|
-
```
|
|
773
|
+
### Environment-specific configs
|
|
472
774
|
|
|
473
|
-
|
|
775
|
+
The separation between factory code and `ioc.config.ts` makes it straightforward to swap implementations by environment. Your factories don't change — the config (or the set of composed manifests) is the only thing that differs.
|
|
474
776
|
|
|
475
|
-
For
|
|
777
|
+
For a single-package app, point the generator at a different config:
|
|
476
778
|
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
scanDirs: "packages/api/src",
|
|
480
|
-
workspacePackageImportBases: [
|
|
481
|
-
{ root: "packages/shared", importBase: "@acme/shared" },
|
|
482
|
-
],
|
|
483
|
-
},
|
|
779
|
+
```bash
|
|
780
|
+
npx ioc generate --config ./ioc.config.test.ts
|
|
484
781
|
```
|
|
485
782
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
### Environment-specific configs
|
|
489
|
-
|
|
490
|
-
The separation between factory code and `ioc.config.ts` makes it straightforward to swap implementations by environment. Your factories don't change — the config is the only thing that differs:
|
|
783
|
+
For a monorepo app, you can swap `composedManifests` entries to compose with mock packages in tests:
|
|
491
784
|
|
|
492
785
|
```ts
|
|
493
|
-
// ioc.config.ts
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
Cache: {
|
|
499
|
-
redisCache: { default: true, lifetime: "singleton" },
|
|
500
|
-
},
|
|
501
|
-
},
|
|
786
|
+
// ioc.config.test.ts
|
|
787
|
+
composedManifests: [
|
|
788
|
+
"@example/lib-storage-mock", // a sibling test-only package
|
|
789
|
+
"@example/lib-services",
|
|
790
|
+
],
|
|
502
791
|
```
|
|
503
792
|
|
|
504
|
-
|
|
505
|
-
// ioc.config.test.ts (testing)
|
|
506
|
-
registrations: {
|
|
507
|
-
EmailService: {
|
|
508
|
-
mockEmailService: { default: true },
|
|
509
|
-
},
|
|
510
|
-
Cache: {
|
|
511
|
-
inMemoryCache: { default: true, lifetime: "transient" },
|
|
512
|
-
},
|
|
513
|
-
},
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
Point the generator at a different config with `npx ioc generate --config ./ioc.config.test.ts` and you get a completely different wiring — all mocks, all stubs, whatever you need — without touching a single factory file.
|
|
793
|
+
Either way, factory source code doesn't change.
|
|
517
794
|
|
|
518
795
|
---
|
|
519
796
|
|
|
@@ -523,13 +800,23 @@ Point the generator at a different config with `npx ioc generate --config ./ioc.
|
|
|
523
800
|
|
|
524
801
|
**Contract not discovered** — the factory's return type must resolve to a named type (interface or type alias). The contract symbol must be imported or declared in the same file as the factory. Anonymous `{ foo: string }` return types are silently skipped.
|
|
525
802
|
|
|
526
|
-
**
|
|
803
|
+
**Factory destructures `IocGeneratedCradle`** — not allowed. Use a named local deps type instead. The error message names the factory and shows the correct pattern.
|
|
804
|
+
|
|
805
|
+
**Duplicate registration keys within a manifest** — every implementation needs a globally unique Awilix key. If two factories produce the same key, rename the exports or use `registrations[Contract][impl].name` to override.
|
|
806
|
+
|
|
807
|
+
**Duplicate registration keys across composed manifests** — composition errors with both manifest sources named. Resolve via `registrations[Contract][impl].source` in the app's `ioc.config`.
|
|
527
808
|
|
|
528
809
|
**Overlapping scan directories with different scopes** — if a factory file matches multiple scan roots that specify different `scope` values, generation fails. Narrow the roots or set lifetimes per implementation in `registrations`.
|
|
529
810
|
|
|
530
|
-
**`registrations` for unknown contracts** — keys in `registrations` must match discovered contract type
|
|
811
|
+
**`registrations` for unknown contracts** — keys in `registrations` must match a discovered contract type name exactly. In app mode, that includes contracts from composed manifests. A typo fails with a list of what was actually discovered, locally and from composed packages.
|
|
812
|
+
|
|
813
|
+
**App mode codegen fails to resolve a composed package** — the package needs `./iocManifest` and `./iocTypes` subpath exports in its `package.json`. Until those are added, app codegen can't import the manifest.
|
|
814
|
+
|
|
815
|
+
**`_<Pkg>ExternalsAssert` fails to compile** — a composed package's externals are not satisfied by the composition. Add a factory in the app (or in another composed package) that supplies the missing key, or compose another manifest that does.
|
|
816
|
+
|
|
817
|
+
**Group base type mismatch across manifests** — caused by hoisting producing different physical paths for the same logical type. The error includes the remediation block to paste into `groupBaseTypeAliases`.
|
|
531
818
|
|
|
532
|
-
|
|
819
|
+
**Library-mode invocation of `ioc validate`** — prints an informational message and exits 0. Validate is a cross-manifest tool; a library has no cross-manifest concerns to validate.
|
|
533
820
|
|
|
534
821
|
---
|
|
535
822
|
|
|
@@ -537,10 +824,12 @@ Point the generator at a different config with `npx ioc generate --config ./ioc.
|
|
|
537
824
|
|
|
538
825
|
This package is **not** an IoC container. It is a codegen layer over Awilix that trades manual registration for convention.
|
|
539
826
|
|
|
540
|
-
- **Factories are plain functions.** No decorators, no base classes, no `RESOLVER` symbols. A factory is an exported function that takes a
|
|
827
|
+
- **Factories are plain functions.** No decorators, no base classes, no `RESOLVER` symbols. A factory is an exported function that takes a named deps type and returns a value.
|
|
541
828
|
- **Policy lives in one file.** Lifetimes, defaults, and key overrides are in `ioc.config.ts` — never scattered across factory sources. Looking at a factory tells you _what_ it builds; looking at the config tells you _how_ it's registered.
|
|
542
829
|
- **Types are inferred, not declared.** The generator reads the TypeScript program to discover contracts, dependencies, and assignability. You don't maintain a parallel type registry.
|
|
543
|
-
- **
|
|
830
|
+
- **Library packages own their boundary.** Each package generates its own manifest. What it supplies appears in `IocGeneratedCradle`; what it expects from outside appears in `IocExternals`. The contract is explicit and machine-readable.
|
|
831
|
+
- **App-mode composition is set-like.** `registerIocFromManifest(container, [a, b, c])` is order-independent. Conflicts are hard errors with explicit resolution, never silent override.
|
|
832
|
+
- **Errors fail fast and explain themselves.** Ambiguous defaults, key collisions, missing externals, and base-type mismatches are caught at generation, validation, or compile time — with messages that name the problem, suggest a fix, and where possible give you the exact config block to paste.
|
|
544
833
|
- **Static imports, not runtime scanning.** The generated manifest is a plain TypeScript module with static imports. It works in dev with loose source files and in production with a single bundled file — no filesystem walking at runtime.
|
|
545
834
|
|
|
546
835
|
---
|