@velajs/vela 0.2.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/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/application.d.ts +44 -0
- package/dist/application.js +112 -0
- package/dist/constants.d.ts +35 -0
- package/dist/constants.js +43 -0
- package/dist/container/container.d.ts +25 -0
- package/dist/container/container.js +195 -0
- package/dist/container/decorators.d.ts +8 -0
- package/dist/container/decorators.js +36 -0
- package/dist/container/index.d.ts +4 -0
- package/dist/container/index.js +3 -0
- package/dist/container/types.d.ts +37 -0
- package/dist/container/types.js +11 -0
- package/dist/errors/http-exception.d.ts +61 -0
- package/dist/errors/http-exception.js +128 -0
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/factory.d.ts +5 -0
- package/dist/factory.js +39 -0
- package/dist/http/decorators.d.ts +122 -0
- package/dist/http/decorators.js +276 -0
- package/dist/http/index.d.ts +3 -0
- package/dist/http/index.js +2 -0
- package/dist/http/route.manager.d.ts +34 -0
- package/dist/http/route.manager.js +373 -0
- package/dist/http/types.d.ts +29 -0
- package/dist/http/types.js +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +26 -0
- package/dist/lifecycle/index.d.ts +20 -0
- package/dist/lifecycle/index.js +15 -0
- package/dist/metadata.d.ts +10 -0
- package/dist/metadata.js +29 -0
- package/dist/module/decorators.d.ts +5 -0
- package/dist/module/decorators.js +32 -0
- package/dist/module/index.d.ts +3 -0
- package/dist/module/index.js +2 -0
- package/dist/module/module-loader.d.ts +20 -0
- package/dist/module/module-loader.js +159 -0
- package/dist/module/types.d.ts +19 -0
- package/dist/module/types.js +1 -0
- package/dist/pipeline/component.manager.d.ts +18 -0
- package/dist/pipeline/component.manager.js +105 -0
- package/dist/pipeline/decorators.d.ts +10 -0
- package/dist/pipeline/decorators.js +50 -0
- package/dist/pipeline/index.d.ts +7 -0
- package/dist/pipeline/index.js +5 -0
- package/dist/pipeline/pipes.d.ts +25 -0
- package/dist/pipeline/pipes.js +52 -0
- package/dist/pipeline/reflector.d.ts +102 -0
- package/dist/pipeline/reflector.js +166 -0
- package/dist/pipeline/tokens.d.ts +33 -0
- package/dist/pipeline/tokens.js +27 -0
- package/dist/pipeline/types.d.ts +31 -0
- package/dist/pipeline/types.js +1 -0
- package/dist/registry/index.d.ts +2 -0
- package/dist/registry/index.js +1 -0
- package/dist/registry/metadata.registry.d.ts +61 -0
- package/dist/registry/metadata.registry.js +276 -0
- package/dist/registry/types.d.ts +55 -0
- package/dist/registry/types.js +2 -0
- package/package.json +72 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-02-19)
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- Decorator-based controllers with full HTTP method support
|
|
8
|
+
- Dependency injection with singleton, transient, and request scopes
|
|
9
|
+
- Module system with imports, exports, and dynamic modules
|
|
10
|
+
- Guards, pipes, interceptors, exception filters, and middleware
|
|
11
|
+
- Built-in pipes: ParseIntPipe, ParseFloatPipe, ParseBoolPipe, DefaultValuePipe, RequiredPipe, ZodValidationPipe
|
|
12
|
+
- 18 built-in HTTP exceptions
|
|
13
|
+
- Custom metadata with SetMetadata + Reflector
|
|
14
|
+
- Custom parameter decorators via createParamDecorator
|
|
15
|
+
- Route versioning
|
|
16
|
+
- Global prefix support
|
|
17
|
+
- Lifecycle hooks
|
|
18
|
+
- Optional hono-crud integration via `vela/crud`
|
|
19
|
+
- Edge runtime compatible (Cloudflare Workers, Deno, Bun, Node.js 20+)
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ksh
|
|
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,84 @@
|
|
|
1
|
+
# @velajs/vela
|
|
2
|
+
|
|
3
|
+
NestJS-compatible framework for edge runtimes, powered by [Hono](https://hono.dev).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @velajs/vela
|
|
9
|
+
# or
|
|
10
|
+
bun add @velajs/vela
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { VelaFactory, Controller, Get, Module, Injectable } from '@velajs/vela';
|
|
17
|
+
|
|
18
|
+
@Injectable()
|
|
19
|
+
class AppService {
|
|
20
|
+
getHello() {
|
|
21
|
+
return { message: 'Hello from the edge!' };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Controller('/app')
|
|
26
|
+
class AppController {
|
|
27
|
+
constructor(private appService: AppService) {}
|
|
28
|
+
|
|
29
|
+
@Get('/')
|
|
30
|
+
hello() {
|
|
31
|
+
return this.appService.getHello();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Module({
|
|
36
|
+
controllers: [AppController],
|
|
37
|
+
providers: [AppService],
|
|
38
|
+
})
|
|
39
|
+
class AppModule {}
|
|
40
|
+
|
|
41
|
+
const app = await VelaFactory.create(AppModule);
|
|
42
|
+
export default app; // Works on Cloudflare Workers, Deno, Bun, etc.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- **Decorator-based controllers** — `@Controller`, `@Get`, `@Post`, `@Put`, `@Patch`, `@Delete`
|
|
48
|
+
- **Dependency injection** — `@Injectable`, `@Inject`, `InjectionToken`, singleton/transient/request scopes
|
|
49
|
+
- **Modules** — `@Module` with imports, exports, controllers, providers
|
|
50
|
+
- **Guards** — `@UseGuards` with `CanActivate` interface
|
|
51
|
+
- **Pipes** — `@UsePipes`, built-in `ParseIntPipe`, `ParseBoolPipe`, `ZodValidationPipe`, etc.
|
|
52
|
+
- **Interceptors** — `@UseInterceptors` with `NestInterceptor` interface
|
|
53
|
+
- **Exception filters** — `@UseFilters`, `@Catch`, built-in HTTP exceptions
|
|
54
|
+
- **Middleware** — `@UseMiddleware` for Hono-native middleware
|
|
55
|
+
- **Custom metadata** — `@SetMetadata` + `Reflector`
|
|
56
|
+
- **Custom param decorators** — `createParamDecorator`
|
|
57
|
+
- **Route versioning** — `@Controller({ version: '1' })` + `@Version('2')`
|
|
58
|
+
- **Global prefix** — `app.setGlobalPrefix('/api')`
|
|
59
|
+
- **Lifecycle hooks** — `OnModuleInit`, `OnApplicationBootstrap`, `OnModuleDestroy`
|
|
60
|
+
- **CRUD integration** — Optional [`@velajs/crud`](https://github.com/velajs/crud) package
|
|
61
|
+
|
|
62
|
+
## Edge Runtime Compatibility
|
|
63
|
+
|
|
64
|
+
Vela runs on any runtime that supports the Web Standards API:
|
|
65
|
+
|
|
66
|
+
- Cloudflare Workers
|
|
67
|
+
- Deno Deploy
|
|
68
|
+
- Bun
|
|
69
|
+
- Node.js 20+
|
|
70
|
+
- Vercel Edge Functions
|
|
71
|
+
|
|
72
|
+
No Node.js-specific APIs (`node:fs`, `Buffer`, `process`) are used.
|
|
73
|
+
|
|
74
|
+
## CRUD Module (Optional)
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
bun add @velajs/crud hono-crud @hono/zod-openapi zod
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
See [`@velajs/crud`](https://github.com/velajs/crud) for documentation.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import type { Container } from './container/container';
|
|
3
|
+
import type { Token } from './container/types';
|
|
4
|
+
import type { RouteManager } from './http/route.manager';
|
|
5
|
+
import type { FilterType, GuardType, InterceptorType, MiddlewareType, PipeType } from './registry/types';
|
|
6
|
+
export declare class VelaApplication {
|
|
7
|
+
private readonly container;
|
|
8
|
+
private readonly routeManager;
|
|
9
|
+
private instances;
|
|
10
|
+
private honoApp;
|
|
11
|
+
private routesBuilt;
|
|
12
|
+
constructor(container: Container, routeManager: RouteManager);
|
|
13
|
+
/** Pre-build routes (handles async CRUD imports). Called by VelaFactory. */
|
|
14
|
+
initRoutes(): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Rebuild routes. Call after registering global middleware/guards/etc.
|
|
17
|
+
* Not needed if you don't use useGlobalMiddleware() post-create.
|
|
18
|
+
*/
|
|
19
|
+
rebuild(): Promise<void>;
|
|
20
|
+
private getApp;
|
|
21
|
+
get fetch(): Hono['fetch'];
|
|
22
|
+
getInstances(): unknown[];
|
|
23
|
+
getContainer(): Container;
|
|
24
|
+
setInstances(instances: unknown[]): void;
|
|
25
|
+
/**
|
|
26
|
+
* Set a global prefix for all routes (e.g. '/api').
|
|
27
|
+
* Must be called before getHonoApp()/fetch, or call rebuild() after.
|
|
28
|
+
*/
|
|
29
|
+
setGlobalPrefix(prefix: string): this;
|
|
30
|
+
/**
|
|
31
|
+
* Register global middleware. Call rebuild() after if called post-create.
|
|
32
|
+
* Controller/method-level @UseMiddleware works without rebuild.
|
|
33
|
+
*/
|
|
34
|
+
useGlobalMiddleware(...middleware: MiddlewareType[]): this;
|
|
35
|
+
useGlobalPipes(...pipes: PipeType[]): this;
|
|
36
|
+
useGlobalGuards(...guards: GuardType[]): this;
|
|
37
|
+
useGlobalInterceptors(...interceptors: InterceptorType[]): this;
|
|
38
|
+
useGlobalFilters(...filters: FilterType[]): this;
|
|
39
|
+
get<T>(token: Token<T>): T;
|
|
40
|
+
getHonoApp(): Hono;
|
|
41
|
+
callOnModuleInit(): Promise<void>;
|
|
42
|
+
callOnApplicationBootstrap(): Promise<void>;
|
|
43
|
+
close(signal?: string): Promise<void>;
|
|
44
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { hasBeforeApplicationShutdown, hasOnApplicationBootstrap, hasOnApplicationShutdown, hasOnModuleDestroy, hasOnModuleInit } from "./lifecycle/index.js";
|
|
2
|
+
export class VelaApplication {
|
|
3
|
+
container;
|
|
4
|
+
routeManager;
|
|
5
|
+
instances = [];
|
|
6
|
+
honoApp = null;
|
|
7
|
+
routesBuilt = false;
|
|
8
|
+
constructor(container, routeManager){
|
|
9
|
+
this.container = container;
|
|
10
|
+
this.routeManager = routeManager;
|
|
11
|
+
}
|
|
12
|
+
/** Pre-build routes (handles async CRUD imports). Called by VelaFactory. */ async initRoutes() {
|
|
13
|
+
this.honoApp = await this.routeManager.build();
|
|
14
|
+
this.routesBuilt = true;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Rebuild routes. Call after registering global middleware/guards/etc.
|
|
18
|
+
* Not needed if you don't use useGlobalMiddleware() post-create.
|
|
19
|
+
*/ async rebuild() {
|
|
20
|
+
this.honoApp = await this.routeManager.build();
|
|
21
|
+
this.routesBuilt = true;
|
|
22
|
+
}
|
|
23
|
+
getApp() {
|
|
24
|
+
if (!this.honoApp) {
|
|
25
|
+
throw new Error('Routes not built. This should not happen — VelaFactory.create() builds routes automatically.');
|
|
26
|
+
}
|
|
27
|
+
return this.honoApp;
|
|
28
|
+
}
|
|
29
|
+
get fetch() {
|
|
30
|
+
return this.getApp().fetch;
|
|
31
|
+
}
|
|
32
|
+
getInstances() {
|
|
33
|
+
return this.instances;
|
|
34
|
+
}
|
|
35
|
+
getContainer() {
|
|
36
|
+
return this.container;
|
|
37
|
+
}
|
|
38
|
+
setInstances(instances) {
|
|
39
|
+
this.instances = instances;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Set a global prefix for all routes (e.g. '/api').
|
|
43
|
+
* Must be called before getHonoApp()/fetch, or call rebuild() after.
|
|
44
|
+
*/ setGlobalPrefix(prefix) {
|
|
45
|
+
this.routeManager.setGlobalPrefix(prefix);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Register global middleware. Call rebuild() after if called post-create.
|
|
50
|
+
* Controller/method-level @UseMiddleware works without rebuild.
|
|
51
|
+
*/ useGlobalMiddleware(...middleware) {
|
|
52
|
+
this.routeManager.useGlobalMiddleware(...middleware);
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
useGlobalPipes(...pipes) {
|
|
56
|
+
this.routeManager.useGlobalPipes(...pipes);
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
useGlobalGuards(...guards) {
|
|
60
|
+
this.routeManager.useGlobalGuards(...guards);
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
useGlobalInterceptors(...interceptors) {
|
|
64
|
+
this.routeManager.useGlobalInterceptors(...interceptors);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
useGlobalFilters(...filters) {
|
|
68
|
+
this.routeManager.useGlobalFilters(...filters);
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
get(token) {
|
|
72
|
+
return this.container.resolve(token);
|
|
73
|
+
}
|
|
74
|
+
getHonoApp() {
|
|
75
|
+
return this.getApp();
|
|
76
|
+
}
|
|
77
|
+
// Lifecycle hooks
|
|
78
|
+
async callOnModuleInit() {
|
|
79
|
+
for (const instance of this.instances){
|
|
80
|
+
if (hasOnModuleInit(instance)) {
|
|
81
|
+
await instance.onModuleInit();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async callOnApplicationBootstrap() {
|
|
86
|
+
for (const instance of this.instances){
|
|
87
|
+
if (hasOnApplicationBootstrap(instance)) {
|
|
88
|
+
await instance.onApplicationBootstrap();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async close(signal) {
|
|
93
|
+
const reversed = [
|
|
94
|
+
...this.instances
|
|
95
|
+
].reverse();
|
|
96
|
+
for (const instance of reversed){
|
|
97
|
+
if (hasBeforeApplicationShutdown(instance)) {
|
|
98
|
+
await instance.beforeApplicationShutdown(signal);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const instance of reversed){
|
|
102
|
+
if (hasOnModuleDestroy(instance)) {
|
|
103
|
+
await instance.onModuleDestroy();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const instance of reversed){
|
|
107
|
+
if (hasOnApplicationShutdown(instance)) {
|
|
108
|
+
await instance.onApplicationShutdown(signal);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare const METADATA_KEYS: {
|
|
2
|
+
readonly INJECTABLE: "vela:injectable";
|
|
3
|
+
readonly INJECT: "vela:inject";
|
|
4
|
+
readonly SCOPE: "vela:scope";
|
|
5
|
+
readonly MODULE: "vela:module";
|
|
6
|
+
readonly CONTROLLER: "vela:controller";
|
|
7
|
+
readonly ROUTES: "vela:routes";
|
|
8
|
+
readonly PARAMS: "vela:params";
|
|
9
|
+
readonly HTTP_CODE: "vela:http-code";
|
|
10
|
+
readonly RESPONSE_HEADERS: "vela:response-headers";
|
|
11
|
+
readonly REDIRECT: "vela:redirect";
|
|
12
|
+
readonly CATCH: "vela:catch";
|
|
13
|
+
readonly CRUD: "vela:crud";
|
|
14
|
+
};
|
|
15
|
+
export declare enum HttpMethod {
|
|
16
|
+
GET = "get",
|
|
17
|
+
POST = "post",
|
|
18
|
+
PUT = "put",
|
|
19
|
+
PATCH = "patch",
|
|
20
|
+
DELETE = "delete",
|
|
21
|
+
OPTIONS = "options",
|
|
22
|
+
HEAD = "head"
|
|
23
|
+
}
|
|
24
|
+
export declare enum ParamType {
|
|
25
|
+
BODY = "body",
|
|
26
|
+
QUERY = "query",
|
|
27
|
+
PARAM = "param",
|
|
28
|
+
HEADERS = "headers",
|
|
29
|
+
REQUEST = "request"
|
|
30
|
+
}
|
|
31
|
+
export declare enum Scope {
|
|
32
|
+
SINGLETON = "singleton",
|
|
33
|
+
TRANSIENT = "transient",
|
|
34
|
+
REQUEST = "request"
|
|
35
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const METADATA_KEYS = {
|
|
2
|
+
// DI
|
|
3
|
+
INJECTABLE: 'vela:injectable',
|
|
4
|
+
INJECT: 'vela:inject',
|
|
5
|
+
SCOPE: 'vela:scope',
|
|
6
|
+
// Module
|
|
7
|
+
MODULE: 'vela:module',
|
|
8
|
+
// HTTP
|
|
9
|
+
CONTROLLER: 'vela:controller',
|
|
10
|
+
ROUTES: 'vela:routes',
|
|
11
|
+
PARAMS: 'vela:params',
|
|
12
|
+
HTTP_CODE: 'vela:http-code',
|
|
13
|
+
RESPONSE_HEADERS: 'vela:response-headers',
|
|
14
|
+
REDIRECT: 'vela:redirect',
|
|
15
|
+
// Pipeline
|
|
16
|
+
CATCH: 'vela:catch',
|
|
17
|
+
// CRUD
|
|
18
|
+
CRUD: 'vela:crud'
|
|
19
|
+
};
|
|
20
|
+
export var HttpMethod = /*#__PURE__*/ function(HttpMethod) {
|
|
21
|
+
HttpMethod["GET"] = "get";
|
|
22
|
+
HttpMethod["POST"] = "post";
|
|
23
|
+
HttpMethod["PUT"] = "put";
|
|
24
|
+
HttpMethod["PATCH"] = "patch";
|
|
25
|
+
HttpMethod["DELETE"] = "delete";
|
|
26
|
+
HttpMethod["OPTIONS"] = "options";
|
|
27
|
+
HttpMethod["HEAD"] = "head";
|
|
28
|
+
return HttpMethod;
|
|
29
|
+
}({});
|
|
30
|
+
export var ParamType = /*#__PURE__*/ function(ParamType) {
|
|
31
|
+
ParamType["BODY"] = "body";
|
|
32
|
+
ParamType["QUERY"] = "query";
|
|
33
|
+
ParamType["PARAM"] = "param";
|
|
34
|
+
ParamType["HEADERS"] = "headers";
|
|
35
|
+
ParamType["REQUEST"] = "request";
|
|
36
|
+
return ParamType;
|
|
37
|
+
}({});
|
|
38
|
+
export var Scope = /*#__PURE__*/ function(Scope) {
|
|
39
|
+
Scope["SINGLETON"] = "singleton";
|
|
40
|
+
Scope["TRANSIENT"] = "transient";
|
|
41
|
+
Scope["REQUEST"] = "request";
|
|
42
|
+
return Scope;
|
|
43
|
+
}({});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ProviderOptions, Token, Type } from './types';
|
|
2
|
+
export declare class Container {
|
|
3
|
+
private providers;
|
|
4
|
+
private resolutionStack;
|
|
5
|
+
private parent;
|
|
6
|
+
private requestInstances;
|
|
7
|
+
register<T>(provider: Type<T> | ProviderOptions<T>): this;
|
|
8
|
+
private registerClass;
|
|
9
|
+
private registerOptions;
|
|
10
|
+
resolve<T>(token: Token<T>): T;
|
|
11
|
+
has(token: Token): boolean;
|
|
12
|
+
getTokens(): Token[];
|
|
13
|
+
/**
|
|
14
|
+
* Create a child container for request-scoped resolution.
|
|
15
|
+
* The child shares the parent's providers but caches REQUEST-scoped
|
|
16
|
+
* instances separately per child (per request).
|
|
17
|
+
*/
|
|
18
|
+
createChild(): Container;
|
|
19
|
+
clear(): void;
|
|
20
|
+
private resolveRegistration;
|
|
21
|
+
private resolveClass;
|
|
22
|
+
private resolveFactory;
|
|
23
|
+
resolveAsync<T>(token: Token<T>): Promise<T>;
|
|
24
|
+
private tokenToString;
|
|
25
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { Scope } from "../constants.js";
|
|
2
|
+
import { getConstructorDependencies, getInjectMetadata, getScope, isInjectable } from "./decorators.js";
|
|
3
|
+
import { InjectionToken } from "./types.js";
|
|
4
|
+
export class Container {
|
|
5
|
+
providers = new Map();
|
|
6
|
+
resolutionStack = new Set();
|
|
7
|
+
parent = null;
|
|
8
|
+
requestInstances = new Map();
|
|
9
|
+
register(provider) {
|
|
10
|
+
if (typeof provider === 'function') {
|
|
11
|
+
this.registerClass(provider);
|
|
12
|
+
} else {
|
|
13
|
+
this.registerOptions(provider);
|
|
14
|
+
}
|
|
15
|
+
return this;
|
|
16
|
+
}
|
|
17
|
+
registerClass(target) {
|
|
18
|
+
if (!isInjectable(target)) {
|
|
19
|
+
console.warn(`Warning: ${target.name} is not decorated with @Injectable(). ` + `It will be registered but dependency resolution may not work correctly.`);
|
|
20
|
+
}
|
|
21
|
+
const scope = getScope(target);
|
|
22
|
+
this.providers.set(target, {
|
|
23
|
+
token: target,
|
|
24
|
+
scope,
|
|
25
|
+
useClass: target
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
registerOptions(options) {
|
|
29
|
+
const token = options.token;
|
|
30
|
+
if (!token) {
|
|
31
|
+
throw new Error('Provider registration requires a token');
|
|
32
|
+
}
|
|
33
|
+
const registration = {
|
|
34
|
+
token,
|
|
35
|
+
scope: options.scope ?? Scope.SINGLETON
|
|
36
|
+
};
|
|
37
|
+
if (options.useValue !== undefined) {
|
|
38
|
+
registration.useValue = options.useValue;
|
|
39
|
+
registration.instance = options.useValue;
|
|
40
|
+
} else if (options.useFactory) {
|
|
41
|
+
registration.useFactory = options.useFactory;
|
|
42
|
+
registration.inject = options.inject;
|
|
43
|
+
} else if (options.useExisting) {
|
|
44
|
+
registration.useExisting = options.useExisting;
|
|
45
|
+
} else if (typeof token === 'function') {
|
|
46
|
+
registration.useClass = token;
|
|
47
|
+
}
|
|
48
|
+
this.providers.set(token, registration);
|
|
49
|
+
}
|
|
50
|
+
resolve(token) {
|
|
51
|
+
const registration = this.providers.get(token);
|
|
52
|
+
if (!registration) {
|
|
53
|
+
if (typeof token === 'function') {
|
|
54
|
+
this.register(token);
|
|
55
|
+
return this.resolve(token);
|
|
56
|
+
}
|
|
57
|
+
if (token instanceof InjectionToken && token.options?.factory) {
|
|
58
|
+
this.register({
|
|
59
|
+
token,
|
|
60
|
+
useFactory: token.options.factory
|
|
61
|
+
});
|
|
62
|
+
return this.resolve(token);
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`No provider found for token: ${this.tokenToString(token)}`);
|
|
65
|
+
}
|
|
66
|
+
return this.resolveRegistration(registration);
|
|
67
|
+
}
|
|
68
|
+
has(token) {
|
|
69
|
+
return this.providers.has(token);
|
|
70
|
+
}
|
|
71
|
+
getTokens() {
|
|
72
|
+
return Array.from(this.providers.keys());
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Create a child container for request-scoped resolution.
|
|
76
|
+
* The child shares the parent's providers but caches REQUEST-scoped
|
|
77
|
+
* instances separately per child (per request).
|
|
78
|
+
*/ createChild() {
|
|
79
|
+
const child = new Container();
|
|
80
|
+
child.parent = this;
|
|
81
|
+
child.providers = this.providers; // share provider registrations
|
|
82
|
+
return child;
|
|
83
|
+
}
|
|
84
|
+
clear() {
|
|
85
|
+
this.providers.clear();
|
|
86
|
+
this.resolutionStack.clear();
|
|
87
|
+
this.requestInstances.clear();
|
|
88
|
+
}
|
|
89
|
+
resolveRegistration(registration) {
|
|
90
|
+
if (registration.useValue !== undefined) {
|
|
91
|
+
return registration.useValue;
|
|
92
|
+
}
|
|
93
|
+
if (registration.useExisting) {
|
|
94
|
+
return this.resolve(registration.useExisting);
|
|
95
|
+
}
|
|
96
|
+
// Singleton: return cached from registration (shared across all containers)
|
|
97
|
+
if (registration.scope === Scope.SINGLETON && registration.instance !== undefined) {
|
|
98
|
+
return registration.instance;
|
|
99
|
+
}
|
|
100
|
+
// Request: return cached from this child's requestInstances
|
|
101
|
+
if (registration.scope === Scope.REQUEST) {
|
|
102
|
+
const cached = this.requestInstances.get(registration.token);
|
|
103
|
+
if (cached !== undefined) {
|
|
104
|
+
return cached;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (this.resolutionStack.has(registration.token)) {
|
|
108
|
+
const chain = [
|
|
109
|
+
...this.resolutionStack,
|
|
110
|
+
registration.token
|
|
111
|
+
].map((t)=>this.tokenToString(t)).join(' -> ');
|
|
112
|
+
throw new Error(`Circular dependency detected: ${chain}`);
|
|
113
|
+
}
|
|
114
|
+
this.resolutionStack.add(registration.token);
|
|
115
|
+
try {
|
|
116
|
+
let instance;
|
|
117
|
+
if (registration.useFactory) {
|
|
118
|
+
instance = this.resolveFactory(registration);
|
|
119
|
+
} else if (registration.useClass) {
|
|
120
|
+
instance = this.resolveClass(registration.useClass);
|
|
121
|
+
} else {
|
|
122
|
+
throw new Error(`Invalid provider registration for: ${this.tokenToString(registration.token)}`);
|
|
123
|
+
}
|
|
124
|
+
if (registration.scope === Scope.SINGLETON) {
|
|
125
|
+
registration.instance = instance;
|
|
126
|
+
} else if (registration.scope === Scope.REQUEST) {
|
|
127
|
+
this.requestInstances.set(registration.token, instance);
|
|
128
|
+
}
|
|
129
|
+
return instance;
|
|
130
|
+
} finally{
|
|
131
|
+
this.resolutionStack.delete(registration.token);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
resolveClass(target) {
|
|
135
|
+
const paramTypes = getConstructorDependencies(target);
|
|
136
|
+
const injectMetadata = getInjectMetadata(target);
|
|
137
|
+
const injectMap = new Map(injectMetadata.map((m)=>[
|
|
138
|
+
m.index,
|
|
139
|
+
m.token
|
|
140
|
+
]));
|
|
141
|
+
const dependencies = paramTypes.map((paramType, index)=>{
|
|
142
|
+
const token = injectMap.get(index) ?? paramType;
|
|
143
|
+
if (!token || token === Object) {
|
|
144
|
+
throw new Error(`Cannot resolve dependency at index ${index} for ${target.name}. ` + `Parameter type is undefined or Object. ` + `Use @Inject() to specify the token explicitly.`);
|
|
145
|
+
}
|
|
146
|
+
return this.resolve(token);
|
|
147
|
+
});
|
|
148
|
+
return new target(...dependencies);
|
|
149
|
+
}
|
|
150
|
+
resolveFactory(registration) {
|
|
151
|
+
if (!registration.useFactory) {
|
|
152
|
+
throw new Error('Factory function is missing');
|
|
153
|
+
}
|
|
154
|
+
const dependencies = (registration.inject || []).map((token)=>this.resolve(token));
|
|
155
|
+
const result = registration.useFactory(...dependencies);
|
|
156
|
+
if (result instanceof Promise) {
|
|
157
|
+
throw new Error(`Async factory for ${this.tokenToString(registration.token)} returned a Promise. ` + `Use resolveAsync() for async providers.`);
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
async resolveAsync(token) {
|
|
162
|
+
const registration = this.providers.get(token);
|
|
163
|
+
if (!registration) {
|
|
164
|
+
if (typeof token === 'function') {
|
|
165
|
+
this.register(token);
|
|
166
|
+
return this.resolveAsync(token);
|
|
167
|
+
}
|
|
168
|
+
throw new Error(`No provider found for token: ${this.tokenToString(token)}`);
|
|
169
|
+
}
|
|
170
|
+
if (registration.useFactory) {
|
|
171
|
+
if (registration.scope === Scope.SINGLETON && registration.instance !== undefined) {
|
|
172
|
+
return registration.instance;
|
|
173
|
+
}
|
|
174
|
+
const dependencies = await Promise.all((registration.inject || []).map((t)=>this.resolveAsync(t)));
|
|
175
|
+
const instance = await registration.useFactory(...dependencies);
|
|
176
|
+
if (registration.scope === Scope.SINGLETON) {
|
|
177
|
+
registration.instance = instance;
|
|
178
|
+
}
|
|
179
|
+
return instance;
|
|
180
|
+
}
|
|
181
|
+
return this.resolve(token);
|
|
182
|
+
}
|
|
183
|
+
tokenToString(token) {
|
|
184
|
+
if (token instanceof InjectionToken) {
|
|
185
|
+
return token.toString();
|
|
186
|
+
}
|
|
187
|
+
if (typeof token === 'function') {
|
|
188
|
+
return token.name;
|
|
189
|
+
}
|
|
190
|
+
if (typeof token === 'symbol') {
|
|
191
|
+
return token.toString();
|
|
192
|
+
}
|
|
193
|
+
return String(token);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Scope } from '../constants';
|
|
2
|
+
import type { InjectableOptions, InjectMetadata, Token } from './types';
|
|
3
|
+
export declare function Injectable(options?: InjectableOptions): ClassDecorator;
|
|
4
|
+
export declare function Inject(token: Token): ParameterDecorator;
|
|
5
|
+
export declare function isInjectable(target: object): boolean;
|
|
6
|
+
export declare function getScope(target: object): Scope;
|
|
7
|
+
export declare function getConstructorDependencies(target: object): unknown[];
|
|
8
|
+
export declare function getInjectMetadata(target: object): InjectMetadata[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { METADATA_KEYS, Scope } from "../constants.js";
|
|
2
|
+
import { defineMetadata, getMetadata } from "../metadata.js";
|
|
3
|
+
import { MetadataRegistry } from "../registry/metadata.registry.js";
|
|
4
|
+
export function Injectable(options = {}) {
|
|
5
|
+
return (target)=>{
|
|
6
|
+
const { scope = Scope.SINGLETON } = options;
|
|
7
|
+
MetadataRegistry.markInjectable(target);
|
|
8
|
+
MetadataRegistry.setScope(target, scope);
|
|
9
|
+
// Keep WeakMap write for external package compat
|
|
10
|
+
defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
|
|
11
|
+
defineMetadata(METADATA_KEYS.SCOPE, scope, target);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function Inject(token) {
|
|
15
|
+
return (target, _propertyKey, parameterIndex)=>{
|
|
16
|
+
const existing = MetadataRegistry.getInjectTokens(target) ?? (getMetadata(METADATA_KEYS.INJECT, target) || []);
|
|
17
|
+
existing.push({
|
|
18
|
+
index: parameterIndex,
|
|
19
|
+
token
|
|
20
|
+
});
|
|
21
|
+
MetadataRegistry.setInjectTokens(target, existing);
|
|
22
|
+
defineMetadata(METADATA_KEYS.INJECT, existing, target);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function isInjectable(target) {
|
|
26
|
+
return MetadataRegistry.hasInjectable(target) || getMetadata(METADATA_KEYS.INJECTABLE, target) === true;
|
|
27
|
+
}
|
|
28
|
+
export function getScope(target) {
|
|
29
|
+
return MetadataRegistry.getScope(target) ?? getMetadata(METADATA_KEYS.SCOPE, target) ?? Scope.SINGLETON;
|
|
30
|
+
}
|
|
31
|
+
export function getConstructorDependencies(target) {
|
|
32
|
+
return Reflect.getMetadata('design:paramtypes', target) || [];
|
|
33
|
+
}
|
|
34
|
+
export function getInjectMetadata(target) {
|
|
35
|
+
return MetadataRegistry.getInjectTokens(target) ?? (getMetadata(METADATA_KEYS.INJECT, target) || []);
|
|
36
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { Container } from './container';
|
|
2
|
+
export { Injectable, Inject, isInjectable, getScope } from './decorators';
|
|
3
|
+
export { InjectionToken } from './types';
|
|
4
|
+
export type { Type, Token, InjectableOptions, InjectMetadata, ProviderOptions, ProviderRegistration, InjectionTokenOptions, } from './types';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Scope } from '../constants';
|
|
2
|
+
export type Type<T = any> = new (...args: any[]) => T;
|
|
3
|
+
export interface InjectionTokenOptions<T> {
|
|
4
|
+
factory?: () => T;
|
|
5
|
+
}
|
|
6
|
+
export declare class InjectionToken<T = unknown> {
|
|
7
|
+
private readonly description;
|
|
8
|
+
readonly options?: InjectionTokenOptions<T> | undefined;
|
|
9
|
+
constructor(description: string, options?: InjectionTokenOptions<T> | undefined);
|
|
10
|
+
toString(): string;
|
|
11
|
+
}
|
|
12
|
+
export type Token<T = any> = Type<T> | InjectionToken<T> | string | symbol;
|
|
13
|
+
export interface InjectableOptions {
|
|
14
|
+
scope?: Scope;
|
|
15
|
+
}
|
|
16
|
+
export interface InjectMetadata {
|
|
17
|
+
index: number;
|
|
18
|
+
token: Token;
|
|
19
|
+
}
|
|
20
|
+
export interface ProviderOptions<T = unknown> {
|
|
21
|
+
token?: Token<T>;
|
|
22
|
+
scope?: Scope;
|
|
23
|
+
useValue?: T;
|
|
24
|
+
useFactory?: (...args: any[]) => T | Promise<T>;
|
|
25
|
+
inject?: Token[];
|
|
26
|
+
useExisting?: Token<T>;
|
|
27
|
+
}
|
|
28
|
+
export interface ProviderRegistration<T = unknown> {
|
|
29
|
+
token: Token<T>;
|
|
30
|
+
scope: Scope;
|
|
31
|
+
instance?: T;
|
|
32
|
+
useValue?: T;
|
|
33
|
+
useFactory?: (...args: any[]) => T | Promise<T>;
|
|
34
|
+
useClass?: Type<T>;
|
|
35
|
+
inject?: Token[];
|
|
36
|
+
useExisting?: Token<T>;
|
|
37
|
+
}
|