katagami 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hiroiku
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # Katagami
2
+
3
+ Lightweight TypeScript DI container with full type inference.
4
+
5
+ ## Why Katagami
6
+
7
+ Most TypeScript DI containers rely on decorators, reflect-metadata, or string-based tokens — each bringing trade-offs in tooling compatibility, type safety, or bundle size. Katagami takes a different approach.
8
+
9
+ ### No decorators, no reflect-metadata
10
+
11
+ Decorator-based DI requires `experimentalDecorators` and `emitDecoratorMetadata` compiler options. Modern build tools such as esbuild, Vite, and SWC do not support `emitDecoratorMetadata`, and the TC39 standard decorators proposal does not include it either. Katagami depends on none of these — it works with any build tool out of the box.
12
+
13
+ ### Full type inference from class tokens
14
+
15
+ String-token DI forces you to maintain manual token-to-type mappings. Parameter-name matching breaks under minification. Katagami uses classes directly as tokens, so `resolve` automatically infers the correct return type — synchronous or `Promise` — with no extra annotations.
16
+
17
+ ### Method-chain type accumulation
18
+
19
+ Types accumulate with each `register` call. Inside a factory, the resolver only accepts tokens that have already been registered at that point in the chain. Resolving an unregistered token is a compile-time error, not a runtime surprise.
20
+
21
+ ### Hybrid token strategy
22
+
23
+ Class tokens give you strict, order-dependent type safety through method chaining. But sometimes you want to define a set of services upfront and register them in any order. Pass an interface to `createContainer<T>()` and use PropertyKey tokens — the type map is fixed at creation time, so registration order does not matter.
24
+
25
+ ### Zero dependencies
26
+
27
+ No runtime dependencies, no polyfills. No need to add reflect-metadata (20–50 KB) to your bundle.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ npm install katagami
33
+ ```
34
+
35
+ ## Usage (TypeScript)
36
+
37
+ Use class tokens for full type inference:
38
+
39
+ ```ts
40
+ import { createContainer } from 'katagami';
41
+
42
+ class Logger {
43
+ log(msg: string) {
44
+ console.log(msg);
45
+ }
46
+ }
47
+
48
+ class UserService {
49
+ constructor(private logger: Logger) {}
50
+ greet(name: string) {
51
+ this.logger.log(`Hello, ${name}`);
52
+ }
53
+ }
54
+
55
+ const container = createContainer()
56
+ .registerSingleton(Logger, () => new Logger())
57
+ .registerSingleton(UserService, r => new UserService(r.resolve(Logger)));
58
+
59
+ const userService = container.resolve(UserService);
60
+ // ^? UserService (fully inferred)
61
+ userService.greet('world');
62
+ ```
63
+
64
+ ## Usage (JavaScript)
65
+
66
+ Use string or Symbol tokens:
67
+
68
+ ```js
69
+ import { createContainer } from 'katagami';
70
+
71
+ const container = createContainer()
72
+ .registerSingleton('logger', () => ({
73
+ log: msg => console.log(msg),
74
+ }))
75
+ .registerSingleton('userService', r => ({
76
+ greet: name => r.resolve('logger').log(`Hello, ${name}`),
77
+ }));
78
+
79
+ const userService = container.resolve('userService');
80
+ userService.greet('world');
81
+ ```
82
+
83
+ ## Async Factories
84
+
85
+ Factories that return a `Promise` are automatically tracked by the type system. When you `resolve` an async token, the return type is `Promise<V>` instead of `V`:
86
+
87
+ ```ts
88
+ import { createContainer } from 'katagami';
89
+
90
+ class Database {
91
+ constructor(public connected: boolean) {}
92
+ }
93
+
94
+ class Logger {
95
+ log(msg: string) {
96
+ console.log(msg);
97
+ }
98
+ }
99
+
100
+ const container = createContainer()
101
+ .registerSingleton(Logger, () => new Logger())
102
+ .registerSingleton(Database, async () => {
103
+ await new Promise(r => setTimeout(r, 100)); // simulate async init
104
+ return new Database(true);
105
+ });
106
+
107
+ const logger = container.resolve(Logger);
108
+ // ^? Logger
109
+
110
+ const db = await container.resolve(Database);
111
+ // ^? Promise<Database> (awaited → Database)
112
+ db.connected; // true
113
+ ```
114
+
115
+ Async factories can depend on both sync and async registrations:
116
+
117
+ ```ts
118
+ const container = createContainer()
119
+ .registerSingleton(Logger, () => new Logger())
120
+ .registerSingleton(Database, async r => {
121
+ const logger = r.resolve(Logger); // sync → Logger
122
+ logger.log('Connecting...');
123
+ return new Database(true);
124
+ });
125
+ ```
126
+
127
+ ## Interface Type Map
128
+
129
+ When you pass an interface to `createContainer<T>()`, PropertyKey tokens are typed from the interface rather than accumulated through chaining. This means you can register and resolve tokens in any order:
130
+
131
+ ```ts
132
+ import { createContainer } from 'katagami';
133
+
134
+ class Logger {
135
+ log(msg: string) {
136
+ console.log(msg);
137
+ }
138
+ }
139
+
140
+ interface Services {
141
+ logger: Logger;
142
+ greeting: string;
143
+ }
144
+
145
+ const container = createContainer<Services>()
146
+ // 'greeting' can reference 'logger' even though it is registered later
147
+ .registerSingleton('greeting', r => {
148
+ r.resolve('logger').log('Building greeting...');
149
+ return 'Hello!';
150
+ })
151
+ .registerSingleton('logger', () => new Logger());
152
+
153
+ const greeting = container.resolve('greeting');
154
+ // ^? string
155
+ ```
156
+
157
+ You can mix both approaches — use class tokens for order-dependent type safety and PropertyKey tokens for order-independent flexibility:
158
+
159
+ ```ts
160
+ const container = createContainer<Services>()
161
+ .registerSingleton(Logger, () => new Logger())
162
+ .registerSingleton('logger', () => new Logger())
163
+ .registerSingleton('greeting', r => {
164
+ r.resolve(Logger).log('Building greeting...');
165
+ return 'Hello!';
166
+ });
167
+ ```
168
+
169
+ ## API
170
+
171
+ ### `createContainer<T>()`
172
+
173
+ Creates a new DI container. Pass an interface as `T` to define the type map for PropertyKey tokens.
174
+
175
+ ### `container.registerSingleton(token, factory)`
176
+
177
+ Registers a factory as a singleton. The instance is created on the first `resolve` and cached thereafter. Returns the container for method chaining.
178
+
179
+ ### `container.registerTransient(token, factory)`
180
+
181
+ Registers a factory as transient. A new instance is created on every `resolve`. Returns the container for method chaining.
182
+
183
+ ### `container.resolve(token)`
184
+
185
+ Resolves and returns the instance for the given token. Throws `ContainerError` if the token is not registered.
186
+
187
+ ### `ContainerError`
188
+
189
+ Error class thrown when resolving an unregistered token.
190
+
191
+ ## License
192
+
193
+ MIT
@@ -0,0 +1,102 @@
1
+ type AbstractConstructor<T = unknown> = abstract new (...args: never[]) => T;
2
+ /**
3
+ * Resolver passed to factory callbacks.
4
+ *
5
+ * @template T PropertyKey-based type map (defined via interface, order-independent)
6
+ * @template C Union of registered class constructors (order-dependent)
7
+ */
8
+ export interface Resolver<T, C extends AbstractConstructor = AbstractConstructor, AC extends AbstractConstructor = never> {
9
+ /**
10
+ * Resolve an instance for the given token.
11
+ *
12
+ * @param token A registered token
13
+ * @returns The instance associated with the token
14
+ */
15
+ resolve<V>(token: AbstractConstructor<V> & AC): Promise<V>;
16
+ resolve<V>(token: AbstractConstructor<V> & C): V;
17
+ resolve<K extends keyof T>(token: K): T[K];
18
+ }
19
+ /**
20
+ * Create a new DI container.
21
+ *
22
+ * Pass an interface as generic T to fix the PropertyKey token type map upfront (order-independent).
23
+ * Class tokens are accumulated via registerSingleton/registerTransient method chaining (order-dependent).
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * interface Services { SampleController: string }
28
+ * const c = createContainer<Services>()
29
+ * .registerSingleton(TextGenerationService, () => new MastraTextGenerationService())
30
+ * .registerTransient(GenerateTextUseCase, r => new GenerateTextUseCase(r.resolve(TextGenerationService)));
31
+ * ```
32
+ */
33
+ export declare function createContainer<T = Record<never, never>>(): Container<T>;
34
+ /**
35
+ * Lightweight DI container.
36
+ *
37
+ * Provides type inference through method chaining with registerSingleton/registerTransient/resolve.
38
+ *
39
+ * - Singleton: Creates the instance on the first resolve and returns the cached value thereafter.
40
+ * - Transient: Creates a new instance via the factory function on every resolve.
41
+ *
42
+ * @template T PropertyKey-based token type map (defined via interface, order-independent)
43
+ * @template C Union of registered class constructors (accumulated via chaining, order-dependent)
44
+ */
45
+ export declare class Container<T = Record<never, never>, C extends AbstractConstructor = never, AC extends AbstractConstructor = never> {
46
+ /**
47
+ * Convert a token to a human-readable string.
48
+ */
49
+ private static tokenToString;
50
+ private readonly registrations;
51
+ private readonly instances;
52
+ constructor();
53
+ /**
54
+ * Register a factory function as a singleton for the given token.
55
+ *
56
+ * Creates the instance on the first resolve and returns the cached value thereafter.
57
+ *
58
+ * @param token Any value to use as a token
59
+ * @param factory Factory function that receives a resolver and returns an instance
60
+ * @returns The container for method chaining
61
+ */
62
+ registerSingleton<V>(token: AbstractConstructor<V>, factory: (resolver: Resolver<T, C, AC>) => Promise<V>): Container<T, C, AC | AbstractConstructor<V>>;
63
+ registerSingleton<V>(token: AbstractConstructor<V>, factory: (resolver: Resolver<T, C, AC>) => V): Container<T, C | AbstractConstructor<V>, AC>;
64
+ registerSingleton<K extends PropertyKey, V>(token: K, factory: (resolver: Resolver<T, C, AC>) => V): Container<Record<K, V> & T, C, AC>;
65
+ registerSingleton<V>(token: unknown, factory: (resolver: Resolver<T, C, AC>) => V): Container<T, C, AC>;
66
+ /**
67
+ * Register a factory function as transient for the given token.
68
+ *
69
+ * Creates a new instance via the factory function on every resolve.
70
+ *
71
+ * @param token Any value to use as a token
72
+ * @param factory Factory function that receives a resolver and returns an instance
73
+ * @returns The container for method chaining
74
+ */
75
+ registerTransient<V>(token: AbstractConstructor<V>, factory: (resolver: Resolver<T, C, AC>) => Promise<V>): Container<T, C, AC | AbstractConstructor<V>>;
76
+ registerTransient<V>(token: AbstractConstructor<V>, factory: (resolver: Resolver<T, C, AC>) => V): Container<T, C | AbstractConstructor<V>, AC>;
77
+ registerTransient<K extends PropertyKey, V>(token: K, factory: (resolver: Resolver<T, C, AC>) => V): Container<Record<K, V> & T, C, AC>;
78
+ registerTransient<V>(token: unknown, factory: (resolver: Resolver<T, C, AC>) => V): Container<T, C, AC>;
79
+ /**
80
+ * Resolve an instance for the given token.
81
+ *
82
+ * For singleton registrations, creates the instance on the first call and caches it.
83
+ * For transient registrations, creates a new instance on every call.
84
+ *
85
+ * @param token A registered token
86
+ * @returns The instance associated with the token
87
+ * @throws ContainerError if the token is not registered
88
+ */
89
+ resolve<V>(token: AbstractConstructor<V> & AC): Promise<V>;
90
+ resolve<V>(token: AbstractConstructor<V> & C): V;
91
+ resolve<K extends keyof T>(token: K): T[K];
92
+ /**
93
+ * Add a registration entry.
94
+ *
95
+ * @param token Token
96
+ * @param factory Factory function
97
+ * @param singleton Whether to register as a singleton
98
+ * @returns The container for method chaining
99
+ */
100
+ private addRegistration;
101
+ }
102
+ export {};
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Error thrown by the DI container.
3
+ *
4
+ * Represents failures in container operations such as resolving an unregistered token.
5
+ */
6
+ export declare class ContainerError extends Error {
7
+ /**
8
+ * @param message Error message
9
+ */
10
+ constructor(message: string);
11
+ }
package/dist/index.cjs ADDED
@@ -0,0 +1,92 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
6
+ var __toCommonJS = (from) => {
7
+ var entry = __moduleCache.get(from), desc;
8
+ if (entry)
9
+ return entry;
10
+ entry = __defProp({}, "__esModule", { value: true });
11
+ if (from && typeof from === "object" || typeof from === "function")
12
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
13
+ get: () => from[key],
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ }));
16
+ __moduleCache.set(from, entry);
17
+ return entry;
18
+ };
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true,
24
+ configurable: true,
25
+ set: (newValue) => all[name] = () => newValue
26
+ });
27
+ };
28
+
29
+ // src/index.ts
30
+ var exports_src = {};
31
+ __export(exports_src, {
32
+ createContainer: () => createContainer,
33
+ ContainerError: () => ContainerError,
34
+ Container: () => Container
35
+ });
36
+ module.exports = __toCommonJS(exports_src);
37
+
38
+ // src/error.ts
39
+ class ContainerError extends Error {
40
+ constructor(message) {
41
+ super(message);
42
+ this.name = "ContainerError";
43
+ }
44
+ }
45
+
46
+ // src/container.ts
47
+ function createContainer() {
48
+ return new Container;
49
+ }
50
+
51
+ class Container {
52
+ static tokenToString(token) {
53
+ if (typeof token === "function") {
54
+ return token.name || "anonymous function";
55
+ }
56
+ if (typeof token === "symbol") {
57
+ return token.toString();
58
+ }
59
+ return String(token);
60
+ }
61
+ registrations;
62
+ instances;
63
+ constructor() {
64
+ this.registrations = new Map;
65
+ this.instances = new Map;
66
+ }
67
+ registerSingleton(token, factory) {
68
+ return this.addRegistration(token, factory, true);
69
+ }
70
+ registerTransient(token, factory) {
71
+ return this.addRegistration(token, factory, false);
72
+ }
73
+ resolve(token) {
74
+ const cached = this.instances.get(token);
75
+ if (cached !== undefined) {
76
+ return cached;
77
+ }
78
+ const factory = this.registrations.get(token);
79
+ if (factory === undefined) {
80
+ throw new ContainerError(`Token "${Container.tokenToString(token)}" is not registered.`);
81
+ }
82
+ const instance = factory.factory(this);
83
+ if (factory.singleton) {
84
+ this.instances.set(token, instance);
85
+ }
86
+ return instance;
87
+ }
88
+ addRegistration(token, factory, singleton) {
89
+ this.registrations.set(token, { factory, singleton });
90
+ return this;
91
+ }
92
+ }
@@ -0,0 +1,3 @@
1
+ export type { Resolver } from './container';
2
+ export { Container, createContainer } from './container';
3
+ export { ContainerError } from './error';
package/dist/index.js ADDED
@@ -0,0 +1,60 @@
1
+ // src/error.ts
2
+ class ContainerError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "ContainerError";
6
+ }
7
+ }
8
+
9
+ // src/container.ts
10
+ function createContainer() {
11
+ return new Container;
12
+ }
13
+
14
+ class Container {
15
+ static tokenToString(token) {
16
+ if (typeof token === "function") {
17
+ return token.name || "anonymous function";
18
+ }
19
+ if (typeof token === "symbol") {
20
+ return token.toString();
21
+ }
22
+ return String(token);
23
+ }
24
+ registrations;
25
+ instances;
26
+ constructor() {
27
+ this.registrations = new Map;
28
+ this.instances = new Map;
29
+ }
30
+ registerSingleton(token, factory) {
31
+ return this.addRegistration(token, factory, true);
32
+ }
33
+ registerTransient(token, factory) {
34
+ return this.addRegistration(token, factory, false);
35
+ }
36
+ resolve(token) {
37
+ const cached = this.instances.get(token);
38
+ if (cached !== undefined) {
39
+ return cached;
40
+ }
41
+ const factory = this.registrations.get(token);
42
+ if (factory === undefined) {
43
+ throw new ContainerError(`Token "${Container.tokenToString(token)}" is not registered.`);
44
+ }
45
+ const instance = factory.factory(this);
46
+ if (factory.singleton) {
47
+ this.instances.set(token, instance);
48
+ }
49
+ return instance;
50
+ }
51
+ addRegistration(token, factory, singleton) {
52
+ this.registrations.set(token, { factory, singleton });
53
+ return this;
54
+ }
55
+ }
56
+ export {
57
+ createContainer,
58
+ ContainerError,
59
+ Container
60
+ };
@@ -0,0 +1,18 @@
1
+ export type AbstractConstructor<T = unknown> = abstract new (...args: never[]) => T;
2
+ /**
3
+ * Resolver passed to factory callbacks.
4
+ *
5
+ * @template T PropertyKey-based type map (defined via interface, order-independent)
6
+ * @template C Union of registered class constructors (order-dependent)
7
+ */
8
+ export interface Resolver<T, C extends AbstractConstructor = AbstractConstructor, AC extends AbstractConstructor = never> {
9
+ /**
10
+ * Resolve an instance for the given token.
11
+ *
12
+ * @param token A registered token
13
+ * @returns The instance associated with the token
14
+ */
15
+ resolve<V>(token: AbstractConstructor<V> & AC): Promise<V>;
16
+ resolve<V>(token: AbstractConstructor<V> & C): V;
17
+ resolve<K extends keyof T>(token: K): T[K];
18
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "katagami",
3
+ "version": "1.0.0",
4
+ "description": "Lightweight TypeScript DI container with full type inference",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.cjs"
12
+ }
13
+ },
14
+ "main": "./dist/index.cjs",
15
+ "module": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "keywords": [
21
+ "dependency-injection",
22
+ "di",
23
+ "container",
24
+ "ioc",
25
+ "typescript"
26
+ ],
27
+ "engines": {
28
+ "bun": ">=1.0.0"
29
+ },
30
+ "scripts": {
31
+ "preinstall": "case \"$npm_config_user_agent\" in bun*) ;; *) echo 'Error: This project requires Bun. Install: https://bun.sh' >&2; exit 1;; esac",
32
+ "build": "bun run build:types && bun run build:esm && bun run build:cjs",
33
+ "build:types": "tsc",
34
+ "build:esm": "bun build ./src/index.ts --outdir dist --format esm",
35
+ "build:cjs": "bun build ./src/index.ts --outfile dist/index.cjs --format cjs",
36
+ "test": "bun test --coverage",
37
+ "prepublishOnly": "bun run build",
38
+ "check": "bun run typecheck && bun run format",
39
+ "lint": "biome lint .",
40
+ "format": "biome check --write .",
41
+ "typecheck": "tsc --noEmit"
42
+ },
43
+ "dependencies": {
44
+ "@types/bun": "^1.3.8"
45
+ },
46
+ "devDependencies": {
47
+ "@biomejs/biome": "^2.3.14",
48
+ "typescript": "^5.9.3"
49
+ }
50
+ }