@nwire/container 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -17
- package/dist/__tests__/cradle.test.d.ts +14 -0
- package/dist/__tests__/cradle.test.d.ts.map +1 -0
- package/dist/__tests__/cradle.test.js +83 -0
- package/dist/__tests__/cradle.test.js.map +1 -0
- package/dist/container.d.ts +68 -31
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +72 -38
- package/dist/container.js.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,33 +1,109 @@
|
|
|
1
1
|
# @nwire/container
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed DI container, generic over the app's `Cradle` shape. Awilix-backed under the hood — you get scope hierarchy, lazy proxy cradle, lifetimes (singleton/transient/scoped), disposers, and factory-with-deps for free.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add @nwire/container
|
|
7
|
+
```
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
## Why
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
Apps grow into needing:
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
- **Typed bindings.** `container.cradle.logger` should be `Logger`, not `unknown`.
|
|
14
|
+
- **Per-request scopes.** Each HTTP request gets its own scope so `user`, `tenant`, `requestId` don't leak across requests.
|
|
15
|
+
- **Lifetimes.** `singleton` for connections; `scoped` for per-request transactions; `transient` for fresh-each-time values.
|
|
16
|
+
- **Disposers.** Graceful shutdown calls `db.close()`, `redis.quit()` automatically when a scope ends.
|
|
17
|
+
- **Source location.** Studio shows "this binding was registered at `src/wires/api.ts:42`".
|
|
18
|
+
|
|
19
|
+
Awilix has done this for over a decade. We wrap it with a typed `Container<TCradle>` interface so app code stays decoupled from awilix-specific imports, and you opt into the full Awilix surface via `.raw` when you need it.
|
|
20
|
+
|
|
21
|
+
## Surface
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
interface Container<TCradle extends object = object> {
|
|
25
|
+
resolve<T, K extends string>(name: K): K extends keyof TCradle ? TCradle[K] : T;
|
|
26
|
+
register<T, K extends string>(
|
|
27
|
+
name: K,
|
|
28
|
+
factory: K extends keyof TCradle ? TCradle[K] | (() => TCradle[K]) : T | (() => T),
|
|
29
|
+
): void;
|
|
30
|
+
readonly cradle: TCradle;
|
|
31
|
+
createScope(): Container<TCradle>;
|
|
32
|
+
has(name: keyof TCradle | (string & {})): boolean;
|
|
33
|
+
list?(): ReadonlyArray<BindingEntry>;
|
|
34
|
+
readonly raw: AwilixContainer<TCradle>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createContainer<TCradle extends object = object>(): Container<TCradle>;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Consumer example
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { createContainer } from "@nwire/container";
|
|
44
|
+
|
|
45
|
+
// Each plugin exports its cradle contribution as a type.
|
|
46
|
+
import type { AuthCradle } from "@nwire/auth"; // { "auth.user": User; … }
|
|
47
|
+
import type { DbCradle } from "@nwire/data-drizzle"; // { "db.pg": PgClient }
|
|
48
|
+
|
|
49
|
+
// App composes the cradle (plus its own bindings).
|
|
50
|
+
type AppCradle = AuthCradle &
|
|
51
|
+
DbCradle & {
|
|
52
|
+
config: AppConfig;
|
|
53
|
+
logger: Logger;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const root = createContainer<AppCradle>();
|
|
57
|
+
root.register("config", { port: 3000, dbUrl: process.env.DATABASE_URL! });
|
|
58
|
+
root.register("logger", () => ({ log: (s) => process.stdout.write(s + "\n") }));
|
|
59
|
+
root.register("db.pg", () => new PgClient(root.cradle.config.dbUrl)); // ← typed
|
|
60
|
+
|
|
61
|
+
root.cradle.logger.log(`Listening on :${root.cradle.config.port}`); // ← typed, autocomplete
|
|
62
|
+
|
|
63
|
+
// Per-request scope — child inherits, owns request-scoped values
|
|
64
|
+
server.on("request", async (req, res) => {
|
|
65
|
+
const scope = root.createScope();
|
|
66
|
+
scope.register("requestId", crypto.randomUUID());
|
|
67
|
+
scope.register("user", await loadUser(req));
|
|
68
|
+
// hand `scope` to your handler / framework
|
|
69
|
+
});
|
|
13
70
|
```
|
|
14
71
|
|
|
15
|
-
##
|
|
72
|
+
## Lazy by default
|
|
16
73
|
|
|
17
|
-
|
|
74
|
+
`container.cradle` is an Awilix proxy. Untouched bindings never instantiate; touched ones are cached (singleton) or fresh-each-time (transient) per the registration.
|
|
18
75
|
|
|
19
76
|
```ts
|
|
20
|
-
|
|
21
|
-
|
|
77
|
+
const factory = vi.fn(() => new ExpensiveDb());
|
|
78
|
+
root.register("db", factory);
|
|
22
79
|
|
|
23
|
-
|
|
24
|
-
|
|
80
|
+
// factory not called yet
|
|
81
|
+
void root.cradle.db; // ← now called once
|
|
82
|
+
void root.cradle.db; // ← cached, not called again
|
|
83
|
+
```
|
|
25
84
|
|
|
26
|
-
|
|
85
|
+
## Full Awilix when you need it
|
|
86
|
+
|
|
87
|
+
`container.raw` is the underlying `AwilixContainer`. Use it for disposers, asClass, scoped lifetimes, async resolution, or `loadModules` — anything beyond the simple register/cradle/resolve surface:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { asFunction, asValue, Lifetime } from "awilix";
|
|
91
|
+
|
|
92
|
+
root.raw.register({
|
|
93
|
+
pgPool: asFunction(({ config }) => new PgPool(config.dbUrl))
|
|
94
|
+
.singleton()
|
|
95
|
+
.disposer((p) => p.end()), // graceful shutdown
|
|
96
|
+
pgTx: asFunction(({ pgPool }) => pgPool.transaction())
|
|
97
|
+
.setLifetime(Lifetime.SCOPED) // one transaction per request scope
|
|
98
|
+
.disposer((tx) => tx.rollback()), // auto-rollback if scope ends without commit
|
|
99
|
+
});
|
|
27
100
|
```
|
|
28
101
|
|
|
29
|
-
##
|
|
102
|
+
## Used by
|
|
103
|
+
|
|
104
|
+
Every Nwire transport (HTTP, queue, cron, MCP) creates its scope chain through this surface. Plugins register into it. Handlers read from a wire-composed ctx that pulls from the cradle.
|
|
105
|
+
|
|
106
|
+
## Notes
|
|
30
107
|
|
|
31
|
-
-
|
|
32
|
-
- `
|
|
33
|
-
- `rootContainer` — a pre-built blank `InMemoryContainer` framework layers compose with.
|
|
108
|
+
- Awilix uses PROXY injection mode by default — destructure parameters resolve from the cradle by name.
|
|
109
|
+
- `keyof TCradle` keys can include any string, including dotted names (`"auth.user"`, `"db.pg"`). Namespacing is the convention to avoid plugin name clashes.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Container<TCradle>` — Awilix-backed generic. App declares its cradle
|
|
3
|
+
* shape; plugins export type fragments the app intersects in. No global
|
|
4
|
+
* augmentation, no cross-app pollution.
|
|
5
|
+
*
|
|
6
|
+
* - declared keys resolve via both `.cradle.X` and `.resolve("X")`
|
|
7
|
+
* - wrong-shape factory for a declared key is a real type error
|
|
8
|
+
* - unknown names fall back to `<T>` generic
|
|
9
|
+
* - `.cradle` is a typed Awilix proxy: lazy resolution
|
|
10
|
+
* - child scopes inherit cradle typing
|
|
11
|
+
* - `.raw` exposes the underlying AwilixContainer for advanced cases
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=cradle.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cradle.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/cradle.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Container<TCradle>` — Awilix-backed generic. App declares its cradle
|
|
3
|
+
* shape; plugins export type fragments the app intersects in. No global
|
|
4
|
+
* augmentation, no cross-app pollution.
|
|
5
|
+
*
|
|
6
|
+
* - declared keys resolve via both `.cradle.X` and `.resolve("X")`
|
|
7
|
+
* - wrong-shape factory for a declared key is a real type error
|
|
8
|
+
* - unknown names fall back to `<T>` generic
|
|
9
|
+
* - `.cradle` is a typed Awilix proxy: lazy resolution
|
|
10
|
+
* - child scopes inherit cradle typing
|
|
11
|
+
* - `.raw` exposes the underlying AwilixContainer for advanced cases
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, it, expectTypeOf, vi } from "vitest";
|
|
14
|
+
import { asFunction, asValue, Lifetime } from "awilix";
|
|
15
|
+
import { createContainer } from "../container.js";
|
|
16
|
+
describe("createContainer<TCradle> — typed binding surface", () => {
|
|
17
|
+
it("resolves a declared key to its declared type via .cradle and .resolve", () => {
|
|
18
|
+
const c = createContainer();
|
|
19
|
+
c.register("config", { port: 3000, host: "0.0.0.0" });
|
|
20
|
+
c.register("logger", () => ({ log: () => { } }));
|
|
21
|
+
expectTypeOf(c.cradle.config).toEqualTypeOf();
|
|
22
|
+
expectTypeOf(c.cradle.logger).toEqualTypeOf();
|
|
23
|
+
expectTypeOf(c.resolve("config")).toEqualTypeOf();
|
|
24
|
+
expect(c.cradle.config.port).toBe(3000);
|
|
25
|
+
expect(c.resolve("logger")).toBe(c.cradle.logger); // factory.singleton() caches
|
|
26
|
+
c.cradle.logger.log("ok");
|
|
27
|
+
});
|
|
28
|
+
it("rejects a wrong-shaped registration at the type level", () => {
|
|
29
|
+
const c = createContainer();
|
|
30
|
+
// @ts-expect-error config must satisfy AppConfig, not a string
|
|
31
|
+
c.register("config", "not-config");
|
|
32
|
+
expect(c.has("config")).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
it("falls back to the <T> generic for unknown names (escape hatch)", () => {
|
|
35
|
+
const c = createContainer();
|
|
36
|
+
c.register("widget", { id: "w-1" });
|
|
37
|
+
const widget = c.resolve("widget");
|
|
38
|
+
expectTypeOf(widget).toEqualTypeOf();
|
|
39
|
+
expect(widget.id).toBe("w-1");
|
|
40
|
+
});
|
|
41
|
+
it("cradle is lazy — factories run only on first access", () => {
|
|
42
|
+
const factory = vi.fn(() => ({ log: () => { } }));
|
|
43
|
+
const c = createContainer();
|
|
44
|
+
c.register("logger", factory);
|
|
45
|
+
expect(factory).not.toHaveBeenCalled(); // ← never touched, never called
|
|
46
|
+
void c.cradle.logger;
|
|
47
|
+
expect(factory).toHaveBeenCalledTimes(1);
|
|
48
|
+
void c.cradle.logger;
|
|
49
|
+
expect(factory).toHaveBeenCalledTimes(1); // ← singleton: cached
|
|
50
|
+
});
|
|
51
|
+
it("child scope inherits TCradle typing from the parent", () => {
|
|
52
|
+
const parent = createContainer();
|
|
53
|
+
parent.register("config", { port: 8080, host: "localhost" });
|
|
54
|
+
const child = parent.createScope();
|
|
55
|
+
expectTypeOf(child.cradle.config).toEqualTypeOf();
|
|
56
|
+
expect(child.cradle.config.host).toBe("localhost");
|
|
57
|
+
});
|
|
58
|
+
it("child scope overrides parent for the scope only", () => {
|
|
59
|
+
const parent = createContainer();
|
|
60
|
+
parent.register("config", { port: 80, host: "prod" });
|
|
61
|
+
const child = parent.createScope();
|
|
62
|
+
child.register("config", { port: 99, host: "test" });
|
|
63
|
+
expect(child.cradle.config.host).toBe("test");
|
|
64
|
+
expect(parent.cradle.config.host).toBe("prod");
|
|
65
|
+
});
|
|
66
|
+
it(".raw exposes the underlying AwilixContainer for advanced cases", () => {
|
|
67
|
+
const c = createContainer();
|
|
68
|
+
// Use Awilix's full surface directly — lifetimes, disposers, factory-with-deps.
|
|
69
|
+
c.raw.register({
|
|
70
|
+
logger: asValue({ log: () => { } }),
|
|
71
|
+
config: asFunction(() => ({ port: 1, host: "x" })).setLifetime(Lifetime.SINGLETON),
|
|
72
|
+
});
|
|
73
|
+
expect(c.cradle.logger).toBeDefined();
|
|
74
|
+
expect(c.cradle.config.port).toBe(1);
|
|
75
|
+
});
|
|
76
|
+
it("untyped container works with default <TCradle = object>", () => {
|
|
77
|
+
const c = createContainer();
|
|
78
|
+
c.register("anything", 42);
|
|
79
|
+
expect(c.resolve("anything")).toBe(42);
|
|
80
|
+
expect(c.has("anything")).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
//# sourceMappingURL=cradle.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cradle.test.js","sourceRoot":"","sources":["../../src/__tests__/cradle.test.ts"],"names":[],"mappings":"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"}
|
package/dist/container.d.ts
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@nwire/container` — DI seam.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* in
|
|
7
|
-
* under any Nwire package without rewriting the framework.
|
|
4
|
+
* Generic over a `TCradle` shape (Awilix-style). Apps declare their cradle
|
|
5
|
+
* type at the boot site; plugins export type fragments the app intersects
|
|
6
|
+
* in. Nothing is globally augmented.
|
|
8
7
|
*
|
|
9
|
-
*
|
|
8
|
+
* type AppCradle = AuthCradle & DbCradle & { config: AppConfig };
|
|
9
|
+
* const c = createContainer<AppCradle>();
|
|
10
|
+
* c.register("auth.user", user); // typed
|
|
11
|
+
* c.cradle["auth.user"] // → User, autocomplete + lazy
|
|
12
|
+
* c.resolve("auth.user") // → User, escape hatch
|
|
13
|
+
*
|
|
14
|
+
* Implementation is Awilix-backed: scope hierarchy, lazy proxy cradle,
|
|
15
|
+
* lifetimes (singleton/transient/scoped), disposers, factory-with-deps.
|
|
16
|
+
* Source-location capture wraps `register()` so Studio's `.nwire/di.json`
|
|
17
|
+
* surfaces "where was this binding made?".
|
|
18
|
+
*
|
|
19
|
+
* Apps consume `createContainer<TCradle>()` as the canonical factory.
|
|
10
20
|
*/
|
|
21
|
+
import { type AwilixContainer } from "awilix";
|
|
11
22
|
import { type SourceLocation } from "@nwire/messages";
|
|
12
23
|
/**
|
|
13
24
|
* One row per active registration, surfaced by `container.list()`. Studio
|
|
@@ -16,49 +27,75 @@ import { type SourceLocation } from "@nwire/messages";
|
|
|
16
27
|
*
|
|
17
28
|
* `kind` reflects how the value was stored: a function factory under
|
|
18
29
|
* `register()` is `"transient"` (re-evaluated on every resolve); a plain
|
|
19
|
-
* value is `"singleton"`.
|
|
30
|
+
* value is `"singleton"`. Awilix's full lifetime spectrum
|
|
31
|
+
* (singleton/transient/scoped) is available via `.raw.register({…})`.
|
|
20
32
|
*/
|
|
21
33
|
export interface BindingEntry {
|
|
22
34
|
readonly name: string;
|
|
23
35
|
readonly kind: "singleton" | "transient";
|
|
24
36
|
readonly source?: SourceLocation;
|
|
25
37
|
}
|
|
26
|
-
export interface Container {
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
export interface Container<TCradle extends object = object> {
|
|
39
|
+
/**
|
|
40
|
+
* Imperative lookup by name. Throws when unregistered.
|
|
41
|
+
*
|
|
42
|
+
* Names declared on `TCradle` resolve to their declared type. Unknown
|
|
43
|
+
* names default to `unknown` — pass an explicit `<T>` to override.
|
|
44
|
+
*/
|
|
45
|
+
resolve<T = unknown, K extends string = string>(name: K): K extends keyof TCradle ? TCradle[K] : T;
|
|
46
|
+
/**
|
|
47
|
+
* Register a value or factory under a name. Last-write wins.
|
|
48
|
+
*
|
|
49
|
+
* - Plain value → Awilix `asValue(v)` (singleton-equivalent).
|
|
50
|
+
* - Function `() => T` → Awilix `asFunction(fn).singleton()` (cached after first resolve).
|
|
51
|
+
*
|
|
52
|
+
* Apps that need Awilix's full lifetime / disposer / factory-with-deps
|
|
53
|
+
* surface use `container.raw.register({…})` directly — the underlying
|
|
54
|
+
* `AwilixContainer` is exposed via `.raw`.
|
|
55
|
+
*/
|
|
56
|
+
register<T = unknown, K extends string = string>(name: K, factory: K extends keyof TCradle ? TCradle[K] | (() => TCradle[K]) : T | (() => T)): void;
|
|
57
|
+
/**
|
|
58
|
+
* Typed lazy proxy. `container.cradle["auth.user"]` resolves through the
|
|
59
|
+
* same machinery `container.resolve("auth.user")` uses — bindings are
|
|
60
|
+
* resolved only on access, so untouched ones never instantiate.
|
|
61
|
+
* Awilix's PROXY-mode cradle, surfaced under our typed interface.
|
|
62
|
+
*/
|
|
63
|
+
readonly cradle: TCradle;
|
|
31
64
|
/**
|
|
32
65
|
* Create a child scope that inherits parent registrations but lets the
|
|
33
66
|
* caller override or register locally. Used for per-request scopes —
|
|
34
67
|
* each HTTP request gets a scoped container so per-request values
|
|
35
68
|
* (envelope, user, tenant) don't leak across requests.
|
|
36
69
|
*/
|
|
37
|
-
createScope(): Container
|
|
70
|
+
createScope(): Container<TCradle>;
|
|
38
71
|
/** True when a name is registered (either locally or on a parent). */
|
|
39
|
-
has(name: string): boolean;
|
|
72
|
+
has(name: keyof TCradle | (string & {})): boolean;
|
|
40
73
|
/**
|
|
41
|
-
* Snapshot every binding visible to this container
|
|
42
|
-
* parent bindings; child overrides win on name collisions). Purely
|
|
74
|
+
* Snapshot every binding visible to this container. Purely
|
|
43
75
|
* introspective — never throws, never resolves factories. Studio + CLI
|
|
44
|
-
* `nwire cache` use this to surface the DI surface
|
|
45
|
-
* stick to `resolve` / `has`.
|
|
76
|
+
* `nwire cache` use this to surface the DI surface.
|
|
46
77
|
*
|
|
47
|
-
* Optional so external `Container` adapters
|
|
48
|
-
*
|
|
78
|
+
* Optional so external `Container` adapters stay valid without
|
|
79
|
+
* re-implementing introspection.
|
|
49
80
|
*/
|
|
50
81
|
list?(): ReadonlyArray<BindingEntry>;
|
|
82
|
+
/**
|
|
83
|
+
* Escape hatch — the underlying Awilix container. Use when you need
|
|
84
|
+
* Awilix-specific surface (`asClass`, scoped lifetimes, disposers,
|
|
85
|
+
* `loadModules`, …). Apps that stay on `register` + `cradle` + `resolve`
|
|
86
|
+
* never touch this.
|
|
87
|
+
*/
|
|
88
|
+
readonly raw: AwilixContainer<TCradle>;
|
|
51
89
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
export declare const rootContainer: Container;
|
|
90
|
+
/**
|
|
91
|
+
* Build a container — generic over `TCradle`. The returned value
|
|
92
|
+
* satisfies the `Container<TCradle>` interface and is backed by Awilix
|
|
93
|
+
* (PROXY-mode cradle, scope hierarchy, lifetimes, disposers).
|
|
94
|
+
*
|
|
95
|
+
* type AppCradle = { logger: Logger; pg: PgClient };
|
|
96
|
+
* const root = createContainer<AppCradle>();
|
|
97
|
+
* root.register("logger", asValue(logger));
|
|
98
|
+
* root.cradle.logger.log("…");
|
|
99
|
+
*/
|
|
100
|
+
export declare function createContainer<TCradle extends object = object>(): Container<TCradle>;
|
|
64
101
|
//# sourceMappingURL=container.d.ts.map
|
package/dist/container.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,QAAQ,CAAC;AAChB,OAAO,EAAyB,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAE7E;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,CAAC;IACzC,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC;CAClC;AAED,MAAM,WAAW,SAAS,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM;IACxD;;;;;OAKG;IACH,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,EAC5C,IAAI,EAAE,CAAC,GACN,CAAC,SAAS,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAE5C;;;;;;;;;OASG;IACH,QAAQ,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,EAC7C,IAAI,EAAE,CAAC,EACP,OAAO,EAAE,CAAC,SAAS,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GACjF,IAAI,CAAC;IAER;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAEzB;;;;;OAKG;IACH,WAAW,IAAI,SAAS,CAAC,OAAO,CAAC,CAAC;IAElC,sEAAsE;IACtE,GAAG,CAAC,IAAI,EAAE,MAAM,OAAO,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC;IAElD;;;;;;;OAOG;IACH,IAAI,CAAC,IAAI,aAAa,CAAC,YAAY,CAAC,CAAC;IAErC;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC;CACxC;AA8ED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,KAAK,SAAS,CAAC,OAAO,CAAC,CAErF"}
|
package/dist/container.js
CHANGED
|
@@ -1,29 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@nwire/container` — DI seam.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* in
|
|
7
|
-
* under any Nwire package without rewriting the framework.
|
|
4
|
+
* Generic over a `TCradle` shape (Awilix-style). Apps declare their cradle
|
|
5
|
+
* type at the boot site; plugins export type fragments the app intersects
|
|
6
|
+
* in. Nothing is globally augmented.
|
|
8
7
|
*
|
|
9
|
-
*
|
|
8
|
+
* type AppCradle = AuthCradle & DbCradle & { config: AppConfig };
|
|
9
|
+
* const c = createContainer<AppCradle>();
|
|
10
|
+
* c.register("auth.user", user); // typed
|
|
11
|
+
* c.cradle["auth.user"] // → User, autocomplete + lazy
|
|
12
|
+
* c.resolve("auth.user") // → User, escape hatch
|
|
13
|
+
*
|
|
14
|
+
* Implementation is Awilix-backed: scope hierarchy, lazy proxy cradle,
|
|
15
|
+
* lifetimes (singleton/transient/scoped), disposers, factory-with-deps.
|
|
16
|
+
* Source-location capture wraps `register()` so Studio's `.nwire/di.json`
|
|
17
|
+
* surfaces "where was this binding made?".
|
|
18
|
+
*
|
|
19
|
+
* Apps consume `createContainer<TCradle>()` as the canonical factory.
|
|
10
20
|
*/
|
|
21
|
+
import { createContainer as createAwilixContainer, asValue, asFunction, InjectionMode, } from "awilix";
|
|
11
22
|
import { captureSourceLocation } from "@nwire/messages";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
/** Per-binding source-location capture, keyed by the underlying Awilix container. */
|
|
24
|
+
const sourceByContainer = new WeakMap();
|
|
25
|
+
class NwireContainer {
|
|
26
|
+
raw;
|
|
27
|
+
constructor(awilix) {
|
|
28
|
+
this.raw =
|
|
29
|
+
awilix ??
|
|
30
|
+
createAwilixContainer({
|
|
31
|
+
injectionMode: InjectionMode.PROXY,
|
|
32
|
+
});
|
|
33
|
+
if (!sourceByContainer.has(this.raw)) {
|
|
34
|
+
sourceByContainer.set(this.raw, new Map());
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
get cradle() {
|
|
38
|
+
return this.raw.cradle;
|
|
17
39
|
}
|
|
18
40
|
resolve(name) {
|
|
19
|
-
|
|
20
|
-
if (own !== undefined) {
|
|
21
|
-
const v = own.value;
|
|
22
|
-
return typeof v === "function" ? v() : v;
|
|
23
|
-
}
|
|
24
|
-
if (this.parent)
|
|
25
|
-
return this.parent.resolve(name);
|
|
26
|
-
throw new Error(`Container: no binding for "${name}"`);
|
|
41
|
+
return this.raw.resolve(name);
|
|
27
42
|
}
|
|
28
43
|
register(name, factory) {
|
|
29
44
|
// Skip two frames: this `register` itself plus the synthetic `Error`
|
|
@@ -32,32 +47,51 @@ export class InMemoryContainer {
|
|
|
32
47
|
// frames (forge's app capability registration, plugin .provide(), etc.)
|
|
33
48
|
// until it lands on user code.
|
|
34
49
|
const source = captureSourceLocation(2);
|
|
35
|
-
|
|
50
|
+
const sources = sourceByContainer.get(this.raw);
|
|
51
|
+
if (source && sources)
|
|
52
|
+
sources.set(name, source);
|
|
53
|
+
if (typeof factory === "function") {
|
|
54
|
+
this.raw.register({
|
|
55
|
+
[name]: asFunction(factory).singleton(),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
this.raw.register({
|
|
60
|
+
[name]: asValue(factory),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
36
63
|
}
|
|
37
64
|
createScope() {
|
|
38
|
-
return new
|
|
65
|
+
return new NwireContainer(this.raw.createScope());
|
|
39
66
|
}
|
|
40
67
|
has(name) {
|
|
41
|
-
|
|
42
|
-
return true;
|
|
43
|
-
return this.parent?.has(name) ?? false;
|
|
68
|
+
return this.raw.hasRegistration(name);
|
|
44
69
|
}
|
|
45
70
|
list() {
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
return Array.from(merged.values());
|
|
71
|
+
const sources = sourceByContainer.get(this.raw) ?? new Map();
|
|
72
|
+
const registrations = this.raw.registrations;
|
|
73
|
+
return Object.keys(registrations).map((name) => {
|
|
74
|
+
// Awilix exposes `lifetime` on each registration; classify by it.
|
|
75
|
+
// `SINGLETON` (which we use for asFunction(...).singleton()) and
|
|
76
|
+
// `TRANSIENT` are the two we surface; SCOPED collapses into "transient"
|
|
77
|
+
// for the Studio view.
|
|
78
|
+
const reg = registrations[name].lifetime;
|
|
79
|
+
const kind = reg === "TRANSIENT" ? "transient" : "singleton";
|
|
80
|
+
return { name, kind, source: sources.get(name) };
|
|
81
|
+
});
|
|
59
82
|
}
|
|
60
83
|
}
|
|
61
|
-
/**
|
|
62
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Build a container — generic over `TCradle`. The returned value
|
|
86
|
+
* satisfies the `Container<TCradle>` interface and is backed by Awilix
|
|
87
|
+
* (PROXY-mode cradle, scope hierarchy, lifetimes, disposers).
|
|
88
|
+
*
|
|
89
|
+
* type AppCradle = { logger: Logger; pg: PgClient };
|
|
90
|
+
* const root = createContainer<AppCradle>();
|
|
91
|
+
* root.register("logger", asValue(logger));
|
|
92
|
+
* root.cradle.logger.log("…");
|
|
93
|
+
*/
|
|
94
|
+
export function createContainer() {
|
|
95
|
+
return new NwireContainer();
|
|
96
|
+
}
|
|
63
97
|
//# sourceMappingURL=container.js.map
|
package/dist/container.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"container.js","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"container.js","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nwire/container",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Nwire — DI container contract +
|
|
3
|
+
"version": "0.9.1",
|
|
4
|
+
"description": "Nwire — DI container contract + Awilix-backed default. Generic over TCradle; ships scope hierarchy, lazy cradle proxy, lifetimes, disposers via Awilix.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"container",
|
|
7
7
|
"di",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"
|
|
30
|
+
"awilix": "^12.0.4",
|
|
31
|
+
"@nwire/messages": "0.9.1"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@types/node": "^22.19.9",
|