@fougere/container 0.1.0-alpha.0 → 0.2.0-alpha.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
@@ -1,7 +1,12 @@
1
1
  # @fougere/container
2
- > The DI container interface
3
- The interface alone, with no implementation and no dependency enough to write code
4
- that resolves its dependencies without marrying one particular container.
2
+ > Fougere's DI container
3
+ Resolution **by type**, never by parameter name: the AST scan reads the constructor
4
+ signature and wires what it asks for. Nested scopes, one per frond. Zero dependencies.
5
+
6
+ It is small on purpose. The scan already knows the graph — every class and every
7
+ dependency is read from source before boot — so there is almost nothing left to bind
8
+ late: a Map with a parent chain, a scope, and a resolver of last resort for a frond
9
+ that lives in another process.
5
10
 
6
11
  ## Installation
7
12
  ```bash
@@ -1,30 +1,50 @@
1
1
  /**
2
- * Resolver function receives the container to resolve dependencies.
2
+ * A class constructor with any arguments.
3
3
  */
4
- export type ResolverFn<T = unknown> = (container: Container) => T;
4
+ export type Constructor<T = unknown> = new (...args: any[]) => T;
5
5
  /**
6
6
  * Registration options.
7
7
  */
8
8
  export interface RegisterOptions {
9
- /** Lifetime of the resolved value. Default: 'transient'. */
10
- lifetime?: 'singleton' | 'scoped' | 'transient';
9
+ /**
10
+ * Lifetime of the resolved value. Default: 'transient'.
11
+ *
12
+ * Two words, because two are used: `Config` is a singleton, every handler,
13
+ * presenter, collector and provider is transient — a fresh instance per
14
+ * resolution, which is what keeps one call's state out of the next. A third,
15
+ * `'scoped'`, was declared and never passed by any caller: {@link Container.createScope}
16
+ * already answers "one instance per frond, per surface", and two mechanisms for
17
+ * one need is one too many.
18
+ */
19
+ lifetime?: 'singleton' | 'transient';
11
20
  /** Dependency type names for type-based resolution (from AST scan). */
12
21
  deps?: string[];
13
22
  }
14
23
  /**
15
- * A class constructor with any arguments.
16
- */
17
- export type Constructor<T = unknown> = new (...args: any[]) => T;
18
- /**
19
- * What can be registered: a class or a resolver function.
24
+ * Anything holding a resource can say so, and disposing the container says it back.
25
+ *
26
+ * Not an interface a class implements a shape a class happens to have. A handler
27
+ * that opens nothing declares nothing.
20
28
  */
21
- export type Registration<T = unknown> = Constructor<T> | ResolverFn<T>;
29
+ export interface Disposable {
30
+ dispose(): void | Promise<void>;
31
+ }
22
32
  /**
23
33
  * DI container interface — the only thing application code sees.
34
+ *
35
+ * It is deliberately small, and the reason is that the scan already knows the graph:
36
+ * every class and every dependency is read from source before boot, so there is
37
+ * almost nothing left to bind late. What remains is a Map with a parent chain, plus
38
+ * the two gestures the scan cannot cover — {@link createScope} for isolation, and
39
+ * {@link setFallback} for a frond that lives in another process.
40
+ *
41
+ * Resolution is by name, and the names are TYPE names produced by the AST scan.
42
+ * The container never sees a type; "DI by type" is realized upstream, in the
43
+ * scanner, which is why this file mentions neither.
24
44
  */
25
45
  export interface Container {
26
- /** Register a value by name. */
27
- register<T>(name: string, registration: Registration<T>, options?: RegisterOptions): void;
46
+ /** Register a class by name. Its `deps` are resolved from this container. */
47
+ register<T>(name: string, ctor: Constructor<T>, options?: RegisterOptions): void;
28
48
  /** Register a pre-built value by name. */
29
49
  registerValue<T>(name: string, value: T): void;
30
50
  /** Resolve a dependency by name. */
@@ -44,7 +64,20 @@ export interface Container {
44
64
  setFallback?(resolve: (name: string) => unknown): void;
45
65
  /** Create a child scope. Inherits parent registrations. */
46
66
  createScope(): Container;
47
- /** Dispose the container and its scoped singletons. */
67
+ /**
68
+ * Dispose the container: every instance it KEPT — the singletons — that has a
69
+ * `dispose` method is told, most recent first, and awaited. A failure does not
70
+ * silence the rest; they travel out together in an `AggregateError`.
71
+ *
72
+ * This used to clear the registry and nothing else, while the doc above it promised
73
+ * disposal — so `await using app` (which routes here through `app[Symbol.asyncDispose]`)
74
+ * announced a cleanup that never happened. A container that holds instances is the
75
+ * only thing that knows they exist; if it stays silent, nobody else can speak.
76
+ *
77
+ * What it does NOT dispose: a transient, handed over and forgotten in the same
78
+ * breath, whose caller is the one who knows when it is done; and a value passed to
79
+ * {@link registerValue}, which the container did not build.
80
+ */
48
81
  dispose(): Promise<void>;
49
82
  }
50
83
  //# sourceMappingURL=container.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,SAAS,EAAE,SAAS,KAAK,CAAC,CAAC;AAElE;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,WAAW,GAAG,QAAQ,GAAG,WAAW,CAAC;IAChD,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;GAEG;AAEH,MAAM,MAAM,WAAW,CAAC,CAAC,GAAG,OAAO,IAAI,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAEjE;;GAEG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,OAAO,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;AAEvE;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,gCAAgC;IAChC,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IAE1F,0CAA0C;IAC1C,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IAE/C,oCAAoC;IACpC,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC;IAE5B,+DAA+D;IAC/D,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAE3B;;;;;;;;;OASG;IACH,WAAW,CAAC,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IAEvD,2DAA2D;IAC3D,WAAW,IAAI,SAAS,CAAC;IAEzB,uDAAuD;IACvD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B"}
1
+ {"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,MAAM,WAAW,CAAC,CAAC,GAAG,OAAO,IAAI,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAEjE;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,WAAW,GAAG,WAAW,CAAC;IACrC,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,SAAS;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IAEjF,0CAA0C;IAC1C,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IAE/C,oCAAoC;IACpC,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC;IAE5B,+DAA+D;IAC/D,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAE3B;;;;;;;;;OASG;IACH,WAAW,CAAC,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IAEvD,2DAA2D;IAC3D,WAAW,IAAI,SAAS,CAAC;IAEzB;;;;;;;;;;;;;OAaG;IACH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B"}
@@ -0,0 +1,3 @@
1
+ import type { Container } from './container.js';
2
+ export declare function createContainer(): Container;
3
+ //# sourceMappingURL=create.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../src/create.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAA4C,MAAM,gBAAgB,CAAC;AAsH1F,wBAAgB,eAAe,IAAI,SAAS,CAE3C"}
package/dist/create.js ADDED
@@ -0,0 +1,95 @@
1
+ const isDisposable = (value) => typeof value === 'object' && value !== null &&
2
+ typeof value.dispose === 'function';
3
+ function createScope(parent) {
4
+ const registry = new Map();
5
+ // Construction order, so disposal can run in reverse: a thing built later may
6
+ // hold something built earlier.
7
+ const built = [];
8
+ let fallback;
9
+ const remember = (value) => {
10
+ if (isDisposable(value))
11
+ built.push(value);
12
+ return value;
13
+ };
14
+ const container = {
15
+ register(name, ctor, options) {
16
+ const lifetime = options?.lifetime ?? 'transient';
17
+ const deps = options?.deps ?? [];
18
+ registry.set(name, {
19
+ factory: (c) => new ctor(...deps.map((d) => c.resolve(d))),
20
+ lifetime,
21
+ });
22
+ },
23
+ registerValue(name, value) {
24
+ // A value the container did not build is not the container's to dispose.
25
+ registry.set(name, { factory: () => value, lifetime: 'singleton', instance: value });
26
+ },
27
+ resolve(name) {
28
+ let entry = registry.get(name);
29
+ // Not found locally — the parent holds it, and holds its instance too.
30
+ if (!entry && parent && parent._getEntry(name)) {
31
+ return parent.resolve(name);
32
+ }
33
+ // Nobody holds it. Before failing, ask whoever set a last resort — a frond declared
34
+ // in `remotes` registers nothing here, so its façade is fabricated rather than found.
35
+ if (!entry) {
36
+ const made = container._getFallback()?.(name);
37
+ if (made !== undefined) {
38
+ registry.set(name, { factory: () => made, lifetime: 'singleton', instance: made });
39
+ return made;
40
+ }
41
+ throw new Error(`[container] '${name}' is not registered`);
42
+ }
43
+ if (entry.instance !== undefined)
44
+ return entry.instance;
45
+ const value = entry.factory(container);
46
+ // The container disposes what it KEEPS. A transient is handed over and
47
+ // forgotten in the same breath — remembering it would be a leak that grows
48
+ // once per call, and its caller is the one who knows when it is done.
49
+ if (entry.lifetime === 'singleton') {
50
+ entry.instance = value;
51
+ remember(value);
52
+ }
53
+ return value;
54
+ },
55
+ has(name) {
56
+ return registry.has(name) || (parent?.has(name) ?? false);
57
+ },
58
+ createScope() {
59
+ return createScope(container);
60
+ },
61
+ async dispose() {
62
+ // Reverse order, and one failure must not silence the rest: everything gets
63
+ // told, then the errors travel together.
64
+ const failures = [];
65
+ for (const value of built.reverse()) {
66
+ try {
67
+ await value.dispose();
68
+ }
69
+ catch (error) {
70
+ failures.push(error);
71
+ }
72
+ }
73
+ built.length = 0;
74
+ registry.clear();
75
+ if (failures.length > 0) {
76
+ throw new AggregateError(failures, '[container] one or more disposals failed');
77
+ }
78
+ },
79
+ setFallback(resolve) {
80
+ fallback = resolve;
81
+ },
82
+ _getEntry(name) {
83
+ return registry.get(name) ?? parent?._getEntry(name);
84
+ },
85
+ /** Set on the root, honoured from any scope — a scope inherits it by asking upward. */
86
+ _getFallback() {
87
+ return fallback ?? parent?._getFallback();
88
+ },
89
+ };
90
+ return container;
91
+ }
92
+ export function createContainer() {
93
+ return createScope();
94
+ }
95
+ //# sourceMappingURL=create.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create.js","sourceRoot":"","sources":["../src/create.ts"],"names":[],"mappings":"AAaA,MAAM,YAAY,GAAG,CAAC,KAAc,EAAuB,EAAE,CAC3D,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;IAC3C,OAAQ,KAAoB,CAAC,OAAO,KAAK,UAAU,CAAC;AAEtD,SAAS,WAAW,CAAC,MAAuB;IAC1C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAiB,CAAC;IAC1C,8EAA8E;IAC9E,gCAAgC;IAChC,MAAM,KAAK,GAAc,EAAE,CAAC;IAC5B,IAAI,QAAiD,CAAC;IAEtD,MAAM,QAAQ,GAAG,CAAI,KAAQ,EAAK,EAAE;QAClC,IAAI,YAAY,CAAC,KAAK,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3C,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;IAEF,MAAM,SAAS,GAAmB;QAChC,QAAQ,CAAI,IAAY,EAAE,IAAoB,EAAE,OAAyB;YACvE,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,WAAW,CAAC;YAClD,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,IAAI,EAAE,CAAC;YACjC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE;gBACjB,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC1D,QAAQ;aACT,CAAC,CAAC;QACL,CAAC;QAED,aAAa,CAAI,IAAY,EAAE,KAAQ;YACrC,yEAAyE;YACzE,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;QACvF,CAAC;QAED,OAAO,CAAI,IAAY;YACrB,IAAI,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAE/B,uEAAuE;YACvE,IAAI,CAAC,KAAK,IAAI,MAAM,IAAI,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC/C,OAAO,MAAM,CAAC,OAAO,CAAI,IAAI,CAAC,CAAC;YACjC,CAAC;YAED,oFAAoF;YACpF,sFAAsF;YACtF,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,MAAM,IAAI,GAAG,SAAS,CAAC,YAAY,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;gBAC9C,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;oBACvB,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;oBACnF,OAAO,IAAS,CAAC;gBACnB,CAAC;gBACD,MAAM,IAAI,KAAK,CAAC,gBAAgB,IAAI,qBAAqB,CAAC,CAAC;YAC7D,CAAC;YAED,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS;gBAAE,OAAO,KAAK,CAAC,QAAa,CAAC;YAC7D,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAM,CAAC;YAC5C,uEAAuE;YACvE,2EAA2E;YAC3E,sEAAsE;YACtE,IAAI,KAAK,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;gBACnC,KAAK,CAAC,QAAQ,GAAG,KAAK,CAAC;gBACvB,QAAQ,CAAC,KAAK,CAAC,CAAC;YAClB,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,GAAG,CAAC,IAAY;YACd,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,CAAC;QAC5D,CAAC;QAED,WAAW;YACT,OAAO,WAAW,CAAC,SAAS,CAAC,CAAC;QAChC,CAAC;QAED,KAAK,CAAC,OAAO;YACX,4EAA4E;YAC5E,yCAAyC;YACzC,MAAM,QAAQ,GAAc,EAAE,CAAC;YAC/B,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;gBACpC,IAAI,CAAC;oBACH,MAAO,KAAoB,CAAC,OAAO,EAAE,CAAC;gBACxC,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACvB,CAAC;YACH,CAAC;YACD,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;YACjB,QAAQ,CAAC,KAAK,EAAE,CAAC;YACjB,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxB,MAAM,IAAI,cAAc,CAAC,QAAQ,EAAE,0CAA0C,CAAC,CAAC;YACjF,CAAC;QACH,CAAC;QAED,WAAW,CAAC,OAAkC;YAC5C,QAAQ,GAAG,OAAO,CAAC;QACrB,CAAC;QAED,SAAS,CAAC,IAAY;YACpB,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;QACvD,CAAC;QAED,uFAAuF;QACvF,YAAY;YACV,OAAO,QAAQ,IAAI,MAAM,EAAE,YAAY,EAAE,CAAC;QAC5C,CAAC;KACF,CAAC;IAEF,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,WAAW,EAAE,CAAC;AACvB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- export { type Container, type Registration, type RegisterOptions, type ResolverFn, type Constructor, } from './container.js';
1
+ export { type Container, type RegisterOptions, type Constructor, type Disposable, } from './container.js';
2
+ export { createContainer } from './create.js';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,UAAU,EACf,KAAK,WAAW,GACjB,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,UAAU,GAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export {};
1
+ export { createContainer } from './create.js';
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,14 @@
1
1
  {
2
2
  "name": "@fougere/container",
3
- "version": "0.1.0-alpha.0",
4
- "description": "Fougere's DI container interface. Zero dependencies.",
3
+ "version": "0.2.0-alpha.0",
4
+ "description": "Fougere's DI container: resolution by type, wired from the AST scan. Zero dependencies.",
5
+ "keywords": [
6
+ "fougere",
7
+ "typescript",
8
+ "dependency-injection",
9
+ "ioc",
10
+ "container"
11
+ ],
5
12
  "license": "MIT",
6
13
  "repository": {
7
14
  "type": "git",
@@ -21,11 +28,16 @@
21
28
  "files": [
22
29
  "dist"
23
30
  ],
31
+ "devDependencies": {
32
+ "vitest": "^4.1.0"
33
+ },
24
34
  "publishConfig": {
25
35
  "access": "public"
26
36
  },
27
37
  "scripts": {
28
38
  "build": "rm -rf dist && tsc",
29
- "typecheck": "tsc --noEmit"
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "typecheck": "tsc --noEmit -p tsconfig.test.json"
30
42
  }
31
43
  }