@nwire/container 0.9.1 → 0.10.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 CHANGED
@@ -44,7 +44,7 @@ import { createContainer } from "@nwire/container";
44
44
 
45
45
  // Each plugin exports its cradle contribution as a type.
46
46
  import type { AuthCradle } from "@nwire/auth"; // { "auth.user": User; … }
47
- import type { DbCradle } from "@nwire/data-drizzle"; // { "db.pg": PgClient }
47
+ import type { DbCradle } from "@nwire/drizzle"; // { "db.pg": PgClient }
48
48
 
49
49
  // App composes the cradle (plus its own bindings).
50
50
  type AppCradle = AuthCradle &
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `@nwire/container/awilix` — Awilix-backed implementation of the
3
+ * `Container<TCradle>` contract from the top-level `@nwire/container`
4
+ * entry. Scope hierarchy, lazy proxy cradle, lifetimes
5
+ * (singleton/transient/scoped), classes via asClass, disposers,
6
+ * factory-with-deps. Source-location capture wraps `register()` so
7
+ * Studio's `.nwire/di.json` surfaces "where was this binding made?".
8
+ *
9
+ * Import this entry only when you need a real container; consumers
10
+ * that only need the type contract stay on `@nwire/container`.
11
+ */
12
+ import { type AwilixContainer } from "awilix";
13
+ import type { BindingEntry, Container, HealthResult, RegisterOptions } from "./index.js";
14
+ export type { BindingEntry, Container, HealthResult, RegisterOptions };
15
+ /**
16
+ * `AwilixBackedContainer<TCradle>` — narrowed `Container<TCradle>` that
17
+ * pins `raw` to the awilix container type so test code reaching through
18
+ * the escape hatch keeps its types.
19
+ */
20
+ export interface AwilixBackedContainer<TCradle extends object = object> extends Container<TCradle> {
21
+ readonly raw: AwilixContainer<TCradle>;
22
+ }
23
+ /**
24
+ * Build a container — generic over `TCradle`. The returned value
25
+ * satisfies the `Container<TCradle>` interface and is backed by Awilix
26
+ * (PROXY-mode cradle, scope hierarchy, lifetimes, classes, disposers).
27
+ *
28
+ * type AppCradle = { logger: Logger; pg: PgClient };
29
+ * const root = createContainer<AppCradle>();
30
+ * root.register("logger", () => makeLogger(), { dispose: l => l.flush() });
31
+ * root.cradle.logger.log("…");
32
+ */
33
+ export declare function createContainer<TCradle extends object = object>(): AwilixBackedContainer<TCradle>;
package/dist/awilix.js ADDED
@@ -0,0 +1,198 @@
1
+ /**
2
+ * `@nwire/container/awilix` — Awilix-backed implementation of the
3
+ * `Container<TCradle>` contract from the top-level `@nwire/container`
4
+ * entry. Scope hierarchy, lazy proxy cradle, lifetimes
5
+ * (singleton/transient/scoped), classes via asClass, disposers,
6
+ * factory-with-deps. Source-location capture wraps `register()` so
7
+ * Studio's `.nwire/di.json` surfaces "where was this binding made?".
8
+ *
9
+ * Import this entry only when you need a real container; consumers
10
+ * that only need the type contract stay on `@nwire/container`.
11
+ */
12
+ import { createContainer as createAwilixContainer, asValue, asFunction, asClass, InjectionMode, Lifetime, } from "awilix";
13
+ import { captureSourceLocation } from "@nwire/messages";
14
+ const LIFETIME_MAP = {
15
+ singleton: Lifetime.SINGLETON,
16
+ transient: Lifetime.TRANSIENT,
17
+ scoped: Lifetime.SCOPED,
18
+ };
19
+ /** ES classes have non-writable `prototype`. Arrow fns have none. Regular fns have writable. */
20
+ function isClass(fn) {
21
+ if (typeof fn !== "function")
22
+ return false;
23
+ const desc = Object.getOwnPropertyDescriptor(fn, "prototype");
24
+ return desc !== undefined && desc.writable === false;
25
+ }
26
+ const nowMs = typeof performance !== "undefined" && typeof performance.now === "function"
27
+ ? () => performance.now()
28
+ : () => Date.now();
29
+ // ─── Implementation ──────────────────────────────────────────────────
30
+ class NwireContainer {
31
+ raw;
32
+ meta = new Map();
33
+ order = 0;
34
+ disposed = false;
35
+ disposing;
36
+ constructor(awilix) {
37
+ this.raw =
38
+ awilix ??
39
+ createAwilixContainer({
40
+ injectionMode: InjectionMode.PROXY,
41
+ });
42
+ }
43
+ get cradle() {
44
+ return this.raw.cradle;
45
+ }
46
+ resolve(name) {
47
+ return this.raw.resolve(name);
48
+ }
49
+ register(name, factory, opts) {
50
+ if (this.disposed) {
51
+ throw new Error(`@nwire/container: cannot register "${name}" on a disposed container`);
52
+ }
53
+ // Skip two frames: this `register` itself plus the synthetic `Error`
54
+ // frame `captureSourceLocation` already accounts for. The regex inside
55
+ // `captureSourceLocation` walks past any remaining `packages/nwire-*`
56
+ // frames (forge's app capability registration, plugin .provide(), etc.)
57
+ // until it lands on user code.
58
+ const source = captureSourceLocation(2);
59
+ const requestedLifetime = opts?.lifetime ?? "singleton";
60
+ const lifetime = LIFETIME_MAP[requestedLifetime];
61
+ if (isClass(factory)) {
62
+ this.raw.register({
63
+ [name]: asClass(factory).setLifetime(lifetime),
64
+ });
65
+ }
66
+ else if (typeof factory === "function") {
67
+ this.raw.register({
68
+ [name]: asFunction(factory).setLifetime(lifetime),
69
+ });
70
+ }
71
+ else {
72
+ this.raw.register({
73
+ [name]: asValue(factory),
74
+ });
75
+ }
76
+ this.meta.set(name, {
77
+ name,
78
+ order: this.order++,
79
+ kind: requestedLifetime,
80
+ source,
81
+ dispose: opts?.dispose,
82
+ check: opts?.check,
83
+ });
84
+ }
85
+ createScope() {
86
+ if (this.disposed) {
87
+ throw new Error("@nwire/container: cannot create a scope on a disposed container");
88
+ }
89
+ return new NwireContainer(this.raw.createScope());
90
+ }
91
+ has(name) {
92
+ return this.raw.hasRegistration(name);
93
+ }
94
+ list() {
95
+ const localRegistered = this.raw.registrations;
96
+ return Object.keys(localRegistered).map((name) => {
97
+ const m = this.meta.get(name);
98
+ // Bindings made via `.raw.register({…})` directly won't appear in
99
+ // `meta` — we still surface them, just without dispose/check flags.
100
+ return {
101
+ name,
102
+ kind: m?.kind ?? classifyByLifetime(localRegistered[name]),
103
+ hasDispose: m?.dispose !== undefined,
104
+ hasCheck: m?.check !== undefined,
105
+ source: m?.source,
106
+ };
107
+ });
108
+ }
109
+ async dispose() {
110
+ if (this.disposing)
111
+ return this.disposing;
112
+ if (this.disposed)
113
+ return;
114
+ this.disposing = (async () => {
115
+ // LIFO — opened last, closed first.
116
+ const ordered = Array.from(this.meta.values())
117
+ .filter((m) => m.dispose !== undefined)
118
+ .sort((a, b) => b.order - a.order);
119
+ for (const m of ordered) {
120
+ let value;
121
+ try {
122
+ // Only resolve if instantiated — for SINGLETON Awilix returns the
123
+ // cached value; for TRANSIENT/SCOPED, resolution would create a
124
+ // fresh instance just to dispose it. For non-singletons we skip
125
+ // (the caller is responsible for tracking transients).
126
+ if (m.kind !== "singleton")
127
+ continue;
128
+ value = this.raw.resolve(m.name);
129
+ }
130
+ catch (err) {
131
+ // Resolution failure during dispose — the binding never
132
+ // materialised; nothing to clean up.
133
+ // eslint-disable-next-line no-console
134
+ console.error(`[@nwire/container] resolve-during-dispose failed for "${m.name}":`, err);
135
+ continue;
136
+ }
137
+ try {
138
+ await m.dispose(value);
139
+ }
140
+ catch (err) {
141
+ // eslint-disable-next-line no-console
142
+ console.error(`[@nwire/container] dispose failed for "${m.name}":`, err);
143
+ }
144
+ }
145
+ // Awilix's own dispose runs additional asValue.disposer hooks (if
146
+ // any were registered via .raw) and tears down the cradle proxy.
147
+ try {
148
+ await this.raw.dispose();
149
+ }
150
+ catch (err) {
151
+ // eslint-disable-next-line no-console
152
+ console.error("[@nwire/container] awilix dispose failed:", err);
153
+ }
154
+ this.disposed = true;
155
+ })();
156
+ return this.disposing;
157
+ }
158
+ async runChecks() {
159
+ if (this.disposed)
160
+ return [];
161
+ const checked = Array.from(this.meta.values()).filter((m) => m.check !== undefined);
162
+ if (checked.length === 0)
163
+ return [];
164
+ const results = await Promise.all(checked.map(async (m) => {
165
+ const startedAt = nowMs();
166
+ try {
167
+ const value = this.raw.resolve(m.name);
168
+ await m.check(value);
169
+ return { name: m.name, ok: true, durationMs: nowMs() - startedAt };
170
+ }
171
+ catch (err) {
172
+ return { name: m.name, ok: false, error: err, durationMs: nowMs() - startedAt };
173
+ }
174
+ }));
175
+ return results;
176
+ }
177
+ }
178
+ function classifyByLifetime(reg) {
179
+ const lifetime = reg?.lifetime;
180
+ if (lifetime === "TRANSIENT")
181
+ return "transient";
182
+ if (lifetime === "SCOPED")
183
+ return "scoped";
184
+ return "singleton";
185
+ }
186
+ /**
187
+ * Build a container — generic over `TCradle`. The returned value
188
+ * satisfies the `Container<TCradle>` interface and is backed by Awilix
189
+ * (PROXY-mode cradle, scope hierarchy, lifetimes, classes, disposers).
190
+ *
191
+ * type AppCradle = { logger: Logger; pg: PgClient };
192
+ * const root = createContainer<AppCradle>();
193
+ * root.register("logger", () => makeLogger(), { dispose: l => l.flush() });
194
+ * root.cradle.logger.log("…");
195
+ */
196
+ export function createContainer() {
197
+ return new NwireContainer();
198
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * `@nwire/container` — DI seam.
3
+ *
4
+ * The top-level entry exposes the `Container<TCradle>` contract +
5
+ * binding metadata types only — zero awilix dependency. Libraries
6
+ * that need the type contract but not the runtime import from here.
7
+ *
8
+ * `dummyContainer()` is a no-op container that throws a helpful
9
+ * error on `resolve()` — used as a fallback for standalone wire
10
+ * collections that aren't attached to a real app.
11
+ *
12
+ * The awilix-backed implementation lives at `@nwire/container/awilix`:
13
+ *
14
+ * import { createContainer } from "@nwire/container/awilix";
15
+ *
16
+ * The split keeps awilix out of the dependency tree of any consumer
17
+ * that just needs to type a container parameter.
18
+ */
19
+ import type { SourceLocation } from "@nwire/messages";
20
+ /**
21
+ * One row per active registration, surfaced by `container.list()`. Studio
22
+ * reads this through `.nwire/di.json` (emitted by `@nwire/scan`) to render
23
+ * "what's in the container, and which file registered it?".
24
+ */
25
+ export interface BindingEntry {
26
+ readonly name: string;
27
+ readonly kind: "singleton" | "transient" | "scoped";
28
+ readonly hasDispose: boolean;
29
+ readonly hasCheck: boolean;
30
+ readonly source?: SourceLocation;
31
+ }
32
+ /**
33
+ * Opt-in per-binding lifecycle + lifetime metadata. Plugins pass these
34
+ * through `bind(name, factory, opts)` in their setup closures; the
35
+ * container persists them so `dispose()` and `runChecks()` can drive them.
36
+ */
37
+ export interface RegisterOptions<T> {
38
+ readonly dispose?: (value: T) => void | Promise<void>;
39
+ readonly check?: (value: T) => void | Promise<void>;
40
+ readonly lifetime?: "singleton" | "transient" | "scoped";
41
+ }
42
+ /** One row of `runChecks()` output — pairs the binding name with its check result. */
43
+ export interface HealthResult {
44
+ readonly name: string;
45
+ readonly ok: boolean;
46
+ readonly error?: unknown;
47
+ readonly durationMs: number;
48
+ }
49
+ /** Anything that produces a T when called with no args (factory) or with cradle (deps-aware factory). */
50
+ type Factory<T> = ((...args: any[]) => T) | (new (...args: any[]) => T);
51
+ /** Accepted value shapes for `register()`. */
52
+ type Registrable<T> = T | Factory<T>;
53
+ export interface Container<TCradle extends object = object> {
54
+ resolve<T = unknown, K extends string = string>(name: K): K extends keyof TCradle ? TCradle[K] : T;
55
+ register<T = unknown, K extends string = string>(name: K, factory: K extends keyof TCradle ? Registrable<TCradle[K]> : Registrable<T>, opts?: RegisterOptions<K extends keyof TCradle ? TCradle[K] : T>): void;
56
+ readonly cradle: TCradle;
57
+ createScope(): Container<TCradle>;
58
+ has(name: keyof TCradle | (string & {})): boolean;
59
+ list(): ReadonlyArray<BindingEntry>;
60
+ dispose(): Promise<void>;
61
+ runChecks(): Promise<readonly HealthResult[]>;
62
+ /**
63
+ * Escape hatch — the underlying implementation handle (awilix container
64
+ * for the canonical impl). Typed as `unknown` here so the top-level
65
+ * contract carries no implementation dependency.
66
+ */
67
+ readonly raw: unknown;
68
+ }
69
+ /**
70
+ * `dummyContainer()` — a no-op container that throws on every `resolve`
71
+ * with a clear message naming the missing binding and pointing at the
72
+ * canonical `createContainer` import path.
73
+ *
74
+ * Used by `@nwire/endpoint` as the fallback `containerOf(wire)` result for
75
+ * standalone wire collections that aren't attached to a real app, and by
76
+ * libraries that just want a placeholder for tests.
77
+ */
78
+ export declare function dummyContainer<TCradle extends object = object>(): Container<TCradle>;
79
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `@nwire/container` — DI seam.
3
+ *
4
+ * The top-level entry exposes the `Container<TCradle>` contract +
5
+ * binding metadata types only — zero awilix dependency. Libraries
6
+ * that need the type contract but not the runtime import from here.
7
+ *
8
+ * `dummyContainer()` is a no-op container that throws a helpful
9
+ * error on `resolve()` — used as a fallback for standalone wire
10
+ * collections that aren't attached to a real app.
11
+ *
12
+ * The awilix-backed implementation lives at `@nwire/container/awilix`:
13
+ *
14
+ * import { createContainer } from "@nwire/container/awilix";
15
+ *
16
+ * The split keeps awilix out of the dependency tree of any consumer
17
+ * that just needs to type a container parameter.
18
+ */
19
+ /**
20
+ * `dummyContainer()` — a no-op container that throws on every `resolve`
21
+ * with a clear message naming the missing binding and pointing at the
22
+ * canonical `createContainer` import path.
23
+ *
24
+ * Used by `@nwire/endpoint` as the fallback `containerOf(wire)` result for
25
+ * standalone wire collections that aren't attached to a real app, and by
26
+ * libraries that just want a placeholder for tests.
27
+ */
28
+ export function dummyContainer() {
29
+ const cradle = new Proxy({}, {
30
+ get(_t, prop) {
31
+ throw new Error(`@nwire/container: no container provided — cradle access for "${String(prop)}". ` +
32
+ `Use createContainer() from "@nwire/container/awilix" if you need a real container.`);
33
+ },
34
+ });
35
+ return {
36
+ resolve(name) {
37
+ throw new Error(`@nwire/container: no container provided — resolve("${name}") failed. ` +
38
+ `Use createContainer() from "@nwire/container/awilix" if you need a real container.`);
39
+ },
40
+ register() {
41
+ throw new Error(`@nwire/container: no container provided — register() not supported on dummyContainer().`);
42
+ },
43
+ cradle,
44
+ createScope() {
45
+ return dummyContainer();
46
+ },
47
+ has() {
48
+ return false;
49
+ },
50
+ list() {
51
+ return [];
52
+ },
53
+ async dispose() {
54
+ // no-op
55
+ },
56
+ async runChecks() {
57
+ return [];
58
+ },
59
+ raw: undefined,
60
+ };
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nwire/container",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
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",
@@ -15,12 +15,16 @@
15
15
  "LICENSE"
16
16
  ],
17
17
  "type": "module",
18
- "main": "./dist/container.js",
19
- "types": "./dist/container.d.ts",
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
20
  "exports": {
21
21
  ".": {
22
- "import": "./dist/container.js",
23
- "types": "./dist/container.d.ts"
22
+ "import": "./dist/index.js",
23
+ "types": "./dist/index.d.ts"
24
+ },
25
+ "./awilix": {
26
+ "import": "./dist/awilix.js",
27
+ "types": "./dist/awilix.d.ts"
24
28
  }
25
29
  },
26
30
  "publishConfig": {
@@ -28,7 +32,7 @@
28
32
  },
29
33
  "dependencies": {
30
34
  "awilix": "^12.0.4",
31
- "@nwire/messages": "0.9.1"
35
+ "@nwire/messages": "0.10.0"
32
36
  },
33
37
  "devDependencies": {
34
38
  "@types/node": "^22.19.9",
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=container-list.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"container-list.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/container-list.test.ts"],"names":[],"mappings":""}
@@ -1,42 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { InMemoryContainer } from "../container.js";
3
- describe("InMemoryContainer.list", () => {
4
- it("returns every binding registered on this container with kind + source", () => {
5
- const c = new InMemoryContainer();
6
- c.register("config", { port: 3000 });
7
- c.register("factory", () => ({ greeting: "hi" }));
8
- const list = c.list();
9
- expect(list).toHaveLength(2);
10
- const byName = Object.fromEntries(list.map((b) => [b.name, b]));
11
- expect(byName.config?.kind).toBe("singleton");
12
- expect(byName.factory?.kind).toBe("transient");
13
- // captureSourceLocation should produce SOME source location. The exact
14
- // file the regex lands on depends on the caller — when register is
15
- // invoked from user code, it's the user's file; when invoked from
16
- // inside framework tests (this file lives under packages/nwire-*), the
17
- // regex skips ahead to the test runner. Either way, source must exist.
18
- expect(byName.config?.source).toBeDefined();
19
- expect(byName.factory?.source).toBeDefined();
20
- expect(typeof byName.config?.source?.line).toBe("number");
21
- });
22
- it("merges parent bindings with child overrides winning on name collision", () => {
23
- const parent = new InMemoryContainer();
24
- parent.register("a", 1);
25
- parent.register("shared", "from-parent");
26
- const child = parent.createScope();
27
- child.register("b", 2);
28
- child.register("shared", "from-child");
29
- const names = new Set(child.list().map((b) => b.name));
30
- expect(names).toEqual(new Set(["a", "b", "shared"]));
31
- // Child override of `shared` should win — resolve confirms it.
32
- expect(child.resolve("shared")).toBe("from-child");
33
- });
34
- it("does NOT break existing register/resolve/has API", () => {
35
- const c = new InMemoryContainer();
36
- c.register("answer", 42);
37
- expect(c.resolve("answer")).toBe(42);
38
- expect(c.has("answer")).toBe(true);
39
- expect(c.has("missing")).toBe(false);
40
- });
41
- });
42
- //# sourceMappingURL=container-list.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"container-list.test.js","sourceRoot":"","sources":["../../src/__tests__/container-list.test.ts"],"names":[],"mappings":"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"}
@@ -1,14 +0,0 @@
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
@@ -1 +0,0 @@
1
- {"version":3,"file":"cradle.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/cradle.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
@@ -1,83 +0,0 @@
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
@@ -1 +0,0 @@
1
- {"version":3,"file":"cradle.test.js","sourceRoot":"","sources":["../../src/__tests__/cradle.test.ts"],"names":[],"mappings":"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"}
@@ -1,101 +0,0 @@
1
- /**
2
- * `@nwire/container` — DI seam.
3
- *
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.
7
- *
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.
20
- */
21
- import { type AwilixContainer } from "awilix";
22
- import { type SourceLocation } from "@nwire/messages";
23
- /**
24
- * One row per active registration, surfaced by `container.list()`. Studio
25
- * reads this through `.nwire/di.json` (emitted by `@nwire/scan`) to render
26
- * "what's in the container, and which file registered it?".
27
- *
28
- * `kind` reflects how the value was stored: a function factory under
29
- * `register()` is `"transient"` (re-evaluated on every resolve); a plain
30
- * value is `"singleton"`. Awilix's full lifetime spectrum
31
- * (singleton/transient/scoped) is available via `.raw.register({…})`.
32
- */
33
- export interface BindingEntry {
34
- readonly name: string;
35
- readonly kind: "singleton" | "transient";
36
- readonly source?: SourceLocation;
37
- }
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;
64
- /**
65
- * Create a child scope that inherits parent registrations but lets the
66
- * caller override or register locally. Used for per-request scopes —
67
- * each HTTP request gets a scoped container so per-request values
68
- * (envelope, user, tenant) don't leak across requests.
69
- */
70
- createScope(): Container<TCradle>;
71
- /** True when a name is registered (either locally or on a parent). */
72
- has(name: keyof TCradle | (string & {})): boolean;
73
- /**
74
- * Snapshot every binding visible to this container. Purely
75
- * introspective — never throws, never resolves factories. Studio + CLI
76
- * `nwire cache` use this to surface the DI surface.
77
- *
78
- * Optional so external `Container` adapters stay valid without
79
- * re-implementing introspection.
80
- */
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>;
89
- }
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>;
101
- //# sourceMappingURL=container.d.ts.map
@@ -1 +0,0 @@
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 DELETED
@@ -1,97 +0,0 @@
1
- /**
2
- * `@nwire/container` — DI seam.
3
- *
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.
7
- *
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.
20
- */
21
- import { createContainer as createAwilixContainer, asValue, asFunction, InjectionMode, } from "awilix";
22
- import { captureSourceLocation } from "@nwire/messages";
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;
39
- }
40
- resolve(name) {
41
- return this.raw.resolve(name);
42
- }
43
- register(name, factory) {
44
- // Skip two frames: this `register` itself plus the synthetic `Error`
45
- // frame `captureSourceLocation` already accounts for. The regex inside
46
- // `captureSourceLocation` walks past any remaining `packages/nwire-*`
47
- // frames (forge's app capability registration, plugin .provide(), etc.)
48
- // until it lands on user code.
49
- const source = captureSourceLocation(2);
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
- }
63
- }
64
- createScope() {
65
- return new NwireContainer(this.raw.createScope());
66
- }
67
- has(name) {
68
- return this.raw.hasRegistration(name);
69
- }
70
- list() {
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
- });
82
- }
83
- }
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
- }
97
- //# sourceMappingURL=container.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"container.js","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"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"}