@asenajs/asena 0.6.0-beta.0 → 0.6.2
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 +30 -214
- package/dist/lib/server/web/websocket/AsenaSocket.d.ts +1 -0
- package/dist/lib/server/web/websocket/AsenaSocket.js +2 -0
- package/dist/lib/server/web/websocket/AsenaSocket.js.map +1 -1
- package/dist/lib/test/factory/mockFactory.d.ts +31 -0
- package/dist/lib/test/factory/mockFactory.js +103 -0
- package/dist/lib/test/factory/mockFactory.js.map +1 -0
- package/dist/lib/test/index.d.ts +3 -0
- package/dist/lib/test/index.js +3 -0
- package/dist/lib/test/index.js.map +1 -0
- package/dist/lib/test/metadata/discovery.d.ts +52 -0
- package/dist/lib/test/metadata/discovery.js +93 -0
- package/dist/lib/test/metadata/discovery.js.map +1 -0
- package/dist/lib/test/mockComponent.d.ts +75 -0
- package/dist/lib/test/mockComponent.js +115 -0
- package/dist/lib/test/mockComponent.js.map +1 -0
- package/dist/lib/test/types.d.ts +87 -0
- package/dist/lib/test/types.js +2 -0
- package/dist/lib/test/types.js.map +1 -0
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -1,218 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
<p width="%100" align="center">
|
|
2
|
+
<img src="https://avatars.githubusercontent.com/u/179836938?s=200&v=4" width="150" align="center"/>
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
# Asena
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
[](https://asena.sh)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://bun.sh)
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
A high-performance IoC web framework for Bun runtime, bringing Spring Boot's automatic component discovery and field-based dependency injection to TypeScript.
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
## Philosophy
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
`Spring Boot` and `Quarkus` have established proven patterns for enterprise application development, but developers transitioning to TypeScript often find themselves reimplementing familiar concepts or adapting to unfamiliar architectures. `AsenaJS` addresses this gap by bringing automatic component discovery, field-based dependency injection to the `Bun` ecosystem.
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
- **Decorator-based**: TypeScript decorators for routing and DI (similar to NestJS)
|
|
15
|
-
- **High Performance**: Built on Bun runtime for optimal speed
|
|
16
|
-
- **WebSocket Support**: Native WebSocket handling
|
|
17
|
-
- **Flexible Middleware**: Multi-level middleware system (global, controller, route)
|
|
18
|
-
- **Adapter System**: Pluggable HTTP adapters (Hono default)
|
|
19
|
-
- **Type-Safe**: Full TypeScript support with strict mode
|
|
20
|
-
- **Zero Config**: Minimal setup required
|
|
17
|
+
The framework eliminates boilerplate through automatic scanning of decorator-annotated classes, removing the need for explicit module declarations or manual wiring. Components marked with `@Controller`, `@Service`, or `@Repository` (via asena-drizzle package) are discovered and registered automatically, allowing developers to focus on business logic rather than configuration. This approach, combined with Bun's native performance characteristics, delivers both the familiar developer experience of Spring Boot and the speed expected from modern JavaScript runtimes.
|
|
21
18
|
|
|
22
|
-
|
|
19
|
+
`AsenaJS` is designed to make developers familiar with `Spring Boot` and `Quarkus` feel at home in the TypeScript ecosystem. As the framework evolves, we remain committed to this philosophy—bringing more proven patterns from the Java world while maintaining the performance advantages that `Bun` provides and flexiblity of Typescript.
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
bun add -D @asenajs/asena-cli
|
|
28
|
-
asena create my-project
|
|
29
|
-
cd my-project
|
|
30
|
-
asena dev start
|
|
31
|
-
```
|
|
21
|
+
## Documentation
|
|
32
22
|
|
|
33
|
-
|
|
23
|
+
📚 **Full documentation:** [asena.sh](https://asena.sh)
|
|
34
24
|
|
|
35
|
-
|
|
25
|
+
For complete guides, API reference, and advanced topics, visit our documentation site.
|
|
36
26
|
|
|
37
|
-
|
|
38
|
-
bun add @asenajs/asena @asenajs/hono-adapter @asenajs/asena-logger
|
|
39
|
-
bun add -D @asenajs/asena-cli
|
|
40
|
-
```
|
|
27
|
+
**Example Project:** [AsenaExample](https://github.com/LibirSoft/AsenaExample) - See latest usage patterns and best practices.
|
|
41
28
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
```json
|
|
45
|
-
{
|
|
46
|
-
"compilerOptions": {
|
|
47
|
-
"target": "ESNext",
|
|
48
|
-
"module": "ESNext",
|
|
49
|
-
"moduleResolution": "bundler",
|
|
50
|
-
"experimentalDecorators": true,
|
|
51
|
-
"emitDecoratorMetadata": true,
|
|
52
|
-
"strict": true,
|
|
53
|
-
"skipLibCheck": true
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Initialize config:
|
|
29
|
+
## Quick Start
|
|
59
30
|
|
|
60
31
|
```bash
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
import { AsenaServerFactory } from '@asenajs/asena';
|
|
68
|
-
import { createHonoAdapter } from '@asenajs/hono-adapter';
|
|
69
|
-
import { logger } from './logger';
|
|
70
|
-
|
|
71
|
-
const [adapter, asenaLogger] = createHonoAdapter(logger);
|
|
72
|
-
|
|
73
|
-
const server = await AsenaServerFactory.create({
|
|
74
|
-
adapter,
|
|
75
|
-
logger: asenaLogger,
|
|
76
|
-
port: 3000
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
await server.start();
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Create a controller (`src/controllers/HelloController.ts`):
|
|
83
|
-
|
|
84
|
-
```typescript
|
|
85
|
-
import { Controller } from '@asenajs/asena/server';
|
|
86
|
-
import { Get } from '@asenajs/asena/web';
|
|
87
|
-
import type { Context } from '@asenajs/hono-adapter';
|
|
88
|
-
|
|
89
|
-
@Controller('/hello')
|
|
90
|
-
export class HelloController {
|
|
91
|
-
@Get('/world')
|
|
92
|
-
async getHello(context: Context) {
|
|
93
|
-
return context.send('Hello World');
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## Core Concepts
|
|
99
|
-
|
|
100
|
-
### Controllers
|
|
101
|
-
|
|
102
|
-
Handle HTTP requests with decorators:
|
|
103
|
-
|
|
104
|
-
```typescript
|
|
105
|
-
@Controller('/users')
|
|
106
|
-
export class UserController {
|
|
107
|
-
@Get('/:id')
|
|
108
|
-
async getUser(context: Context) {
|
|
109
|
-
const id = context.req.param('id');
|
|
110
|
-
return context.json({ id, name: 'John' });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
@Post('/')
|
|
114
|
-
async createUser(context: Context) {
|
|
115
|
-
const body = await context.req.json();
|
|
116
|
-
return context.json({ success: true });
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### Services
|
|
122
|
-
|
|
123
|
-
Business logic with dependency injection:
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
@Service()
|
|
127
|
-
export class UserService {
|
|
128
|
-
async findUser(id: string) {
|
|
129
|
-
// Your business logic
|
|
130
|
-
return { id, name: 'John' };
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
@Controller('/users')
|
|
135
|
-
export class UserController {
|
|
136
|
-
@Inject(UserService)
|
|
137
|
-
private userService: UserService;
|
|
138
|
-
|
|
139
|
-
@Get('/:id')
|
|
140
|
-
async getUser(context: Context) {
|
|
141
|
-
const user = await this.userService.findUser(context.req.param('id'));
|
|
142
|
-
return context.json(user);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### Middleware
|
|
148
|
-
|
|
149
|
-
Multi-level middleware support:
|
|
150
|
-
|
|
151
|
-
```typescript
|
|
152
|
-
@Middleware()
|
|
153
|
-
export class AuthMiddleware extends MiddlewareService {
|
|
154
|
-
async handle(context: Context, next: Function) {
|
|
155
|
-
// Check authentication
|
|
156
|
-
await next();
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Global middleware via Config
|
|
161
|
-
@Config()
|
|
162
|
-
export class AppConfig {
|
|
163
|
-
globalMiddlewares() {
|
|
164
|
-
return [
|
|
165
|
-
LoggerMiddleware, // Apply to all routes
|
|
166
|
-
|
|
167
|
-
// Pattern-based
|
|
168
|
-
{
|
|
169
|
-
middleware: AuthMiddleware,
|
|
170
|
-
routes: { include: ['/api/*', '/admin/*'] }
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
middleware: RateLimitMiddleware,
|
|
174
|
-
routes: { exclude: ['/health', '/metrics'] }
|
|
175
|
-
}
|
|
176
|
-
];
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Controller-level
|
|
181
|
-
@Controller({ path: '/admin', middlewares: [AuthMiddleware] })
|
|
182
|
-
export class AdminController { }
|
|
183
|
-
|
|
184
|
-
// Route-level
|
|
185
|
-
@Get({ path: '/users', middlewares: [AuthMiddleware] })
|
|
186
|
-
async getUsers(context: Context) { }
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
### WebSocket
|
|
190
|
-
|
|
191
|
-
Built-in WebSocket support:
|
|
192
|
-
|
|
193
|
-
```typescript
|
|
194
|
-
@WebSocket('/chat')
|
|
195
|
-
export class ChatSocket extends AsenaWebSocketService {
|
|
196
|
-
onOpen(ws: Socket) {
|
|
197
|
-
ws.send('Welcome!');
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
onMessage(ws: Socket, message: string) {
|
|
201
|
-
ws.send(`Echo: ${message}`);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
## CLI Commands
|
|
32
|
+
# Create new project
|
|
33
|
+
bun add -g @asenajs/asena-cli
|
|
34
|
+
asena create my-project --adapter=hono --logger --eslint --prettier
|
|
35
|
+
cd my-project
|
|
207
36
|
|
|
208
|
-
|
|
209
|
-
asena
|
|
210
|
-
asena init # Initialize config
|
|
211
|
-
asena dev start # Development mode
|
|
212
|
-
asena build # Production build
|
|
37
|
+
# Start development
|
|
38
|
+
asena dev start
|
|
213
39
|
```
|
|
214
40
|
|
|
215
|
-
|
|
41
|
+
Visit [asena.sh/docs/get-started](https://asena.sh/docs/get-started) for detailed setup instructions.
|
|
216
42
|
|
|
217
43
|
## Performance
|
|
218
44
|
|
|
@@ -220,27 +46,17 @@ Built on Bun runtime for exceptional performance:
|
|
|
220
46
|
|
|
221
47
|
| Framework | Requests/sec | Latency (avg) |
|
|
222
48
|
|--------------------------|-----------------------|---------------|
|
|
223
|
-
| **Asena + Ergenecore** | **
|
|
224
|
-
| Hono |
|
|
225
|
-
| **Asena + Hono-adapter** | **
|
|
226
|
-
| NestJS (Bun) |
|
|
227
|
-
| NestJS (Node) |
|
|
49
|
+
| **Asena + Ergenecore** | **294,962.61** | **1.34ms** |
|
|
50
|
+
| Hono | 266,476.83 | 1.49ms |
|
|
51
|
+
| **Asena + Hono-adapter** | **233,182.35** | **1.70ms** |
|
|
52
|
+
| NestJS (Bun) | 100,975.20 | 3.92ms |
|
|
53
|
+
| NestJS (Node) | 88,083.22 | 5.33ms |
|
|
228
54
|
|
|
229
55
|
> Benchmark: 12 threads, 400 connections, 120s duration, Hello World endpoint
|
|
230
56
|
|
|
231
|
-
## Architecture
|
|
232
|
-
|
|
233
|
-
```
|
|
234
|
-
lib/
|
|
235
|
-
├── ioc/ # IoC container & dependency injection
|
|
236
|
-
├── server/ # Core server & routing
|
|
237
|
-
├── adapter/ # HTTP adapter abstraction
|
|
238
|
-
└── utils/ # Utilities & helpers
|
|
239
|
-
```
|
|
240
|
-
|
|
241
57
|
## Contributing
|
|
242
58
|
|
|
243
|
-
Contributions welcome! Submit a Pull Request on [GitHub](https://github.com/AsenaJs/Asena).
|
|
59
|
+
Contributions are welcome! Submit a Pull Request on [GitHub](https://github.com/AsenaJs/Asena).
|
|
244
60
|
|
|
245
61
|
## License
|
|
246
62
|
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
16
|
export class AsenaSocket {
|
|
17
|
+
subscriptions;
|
|
17
18
|
/**
|
|
18
19
|
* The underlying Bun ServerWebSocket instance.
|
|
19
20
|
* @private
|
|
@@ -63,6 +64,7 @@ export class AsenaSocket {
|
|
|
63
64
|
this._binaryType = ws.binaryType;
|
|
64
65
|
this._readyState = ws.readyState;
|
|
65
66
|
this._namespace = namespace;
|
|
67
|
+
this.subscriptions = ws.subscriptions;
|
|
66
68
|
}
|
|
67
69
|
/**
|
|
68
70
|
* Sends data to the client. Automatically detects whether data is text or binary.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AsenaSocket.js","sourceRoot":"","sources":["../../../../../lib/server/web/websocket/AsenaSocket.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,WAAW;
|
|
1
|
+
{"version":3,"file":"AsenaSocket.js","sourceRoot":"","sources":["../../../../../lib/server/web/websocket/AsenaSocket.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a mock object from a class type
|
|
3
|
+
* Automatically generates Bun mock functions for all class methods
|
|
4
|
+
*
|
|
5
|
+
* @param classType - Class to create mock from
|
|
6
|
+
* @returns Mock object with all methods mocked
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const UserServiceMock = createMockFromClass(UserService);
|
|
11
|
+
* // Returns:
|
|
12
|
+
* // {
|
|
13
|
+
* // createUser: mock(async () => null),
|
|
14
|
+
* // findByEmail: mock(async () => null),
|
|
15
|
+
* // findById: mock(async () => null),
|
|
16
|
+
* // }
|
|
17
|
+
*
|
|
18
|
+
* // Can configure mocks:
|
|
19
|
+
* UserServiceMock.createUser.mockResolvedValue({ id: '123', name: 'John' });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function createMockFromClass(classType: any): any;
|
|
23
|
+
/**
|
|
24
|
+
* Creates a simple spy that tracks calls without mocking behavior
|
|
25
|
+
* Useful for partial mocking scenarios
|
|
26
|
+
*
|
|
27
|
+
* @returns Mock function that returns undefined
|
|
28
|
+
*
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
export declare function createSimpleSpy(): any;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { mock } from 'bun:test';
|
|
2
|
+
/**
|
|
3
|
+
* Detects all methods in a class (excluding constructor)
|
|
4
|
+
*
|
|
5
|
+
* @param classType - Class to inspect
|
|
6
|
+
* @returns Array of method names
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
function detectClassMethods(classType) {
|
|
11
|
+
if (!classType?.prototype) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
const methods = [];
|
|
15
|
+
const prototype = classType.prototype;
|
|
16
|
+
// Get all property names from prototype
|
|
17
|
+
const propertyNames = Object.getOwnPropertyNames(prototype);
|
|
18
|
+
for (const name of propertyNames) {
|
|
19
|
+
// Skip constructor
|
|
20
|
+
if (name === 'constructor') {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
// Check if it's a function
|
|
24
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
|
|
25
|
+
if (descriptor && typeof descriptor.value === 'function') {
|
|
26
|
+
methods.push(name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return methods;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Checks if a value is mockable (is a class with prototype)
|
|
33
|
+
*
|
|
34
|
+
* @param classType - Value to check
|
|
35
|
+
* @returns True if value is a class
|
|
36
|
+
*
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
function isMockable(classType) {
|
|
40
|
+
return (classType !== null &&
|
|
41
|
+
classType !== undefined &&
|
|
42
|
+
typeof classType === 'function' &&
|
|
43
|
+
classType.prototype !== undefined);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Creates a mock object from a class type
|
|
47
|
+
* Automatically generates Bun mock functions for all class methods
|
|
48
|
+
*
|
|
49
|
+
* @param classType - Class to create mock from
|
|
50
|
+
* @returns Mock object with all methods mocked
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const UserServiceMock = createMockFromClass(UserService);
|
|
55
|
+
* // Returns:
|
|
56
|
+
* // {
|
|
57
|
+
* // createUser: mock(async () => null),
|
|
58
|
+
* // findByEmail: mock(async () => null),
|
|
59
|
+
* // findById: mock(async () => null),
|
|
60
|
+
* // }
|
|
61
|
+
*
|
|
62
|
+
* // Can configure mocks:
|
|
63
|
+
* UserServiceMock.createUser.mockResolvedValue({ id: '123', name: 'John' });
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export function createMockFromClass(classType) {
|
|
67
|
+
// Handle non-mockable types - return empty object as placeholder
|
|
68
|
+
if (!isMockable(classType)) {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
// Detect all methods in the class
|
|
72
|
+
const methods = detectClassMethods(classType);
|
|
73
|
+
// Create mock object
|
|
74
|
+
const mockObject = {};
|
|
75
|
+
// Create Bun mock for each method
|
|
76
|
+
for (const methodName of methods) {
|
|
77
|
+
// Check if method is async by inspecting the original
|
|
78
|
+
const originalMethod = classType.prototype[methodName];
|
|
79
|
+
const isAsync = originalMethod?.constructor?.name === 'AsyncFunction';
|
|
80
|
+
// Create mock with appropriate default return value
|
|
81
|
+
if (isAsync) {
|
|
82
|
+
// Async methods return Promise.resolve(null) by default
|
|
83
|
+
mockObject[methodName] = mock(async () => null);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Sync methods return undefined by default
|
|
87
|
+
mockObject[methodName] = mock(() => undefined);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return mockObject;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Creates a simple spy that tracks calls without mocking behavior
|
|
94
|
+
* Useful for partial mocking scenarios
|
|
95
|
+
*
|
|
96
|
+
* @returns Mock function that returns undefined
|
|
97
|
+
*
|
|
98
|
+
* @internal
|
|
99
|
+
*/
|
|
100
|
+
export function createSimpleSpy() {
|
|
101
|
+
return mock(() => undefined);
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=mockFactory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mockFactory.js","sourceRoot":"","sources":["../../../../lib/test/factory/mockFactory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEhC;;;;;;;GAOG;AACH,SAAS,kBAAkB,CAAC,SAAc;IACxC,IAAI,CAAC,SAAS,EAAE,SAAS,EAAE,CAAC;QAC1B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS,CAAC;IAEtC,wCAAwC;IACxC,MAAM,aAAa,GAAG,MAAM,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC;IAE5D,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;QACjC,mBAAmB;QACnB,IAAI,IAAI,KAAK,aAAa,EAAE,CAAC;YAC3B,SAAS;QACX,CAAC;QAED,2BAA2B;QAC3B,MAAM,UAAU,GAAG,MAAM,CAAC,wBAAwB,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QACpE,IAAI,UAAU,IAAI,OAAO,UAAU,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;YACzD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,UAAU,CAAC,SAAc;IAChC,OAAO,CACL,SAAS,KAAK,IAAI;QAClB,SAAS,KAAK,SAAS;QACvB,OAAO,SAAS,KAAK,UAAU;QAC/B,SAAS,CAAC,SAAS,KAAK,SAAS,CAClC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,mBAAmB,CAAC,SAAc;IAChD,iEAAiE;IACjE,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC3B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,kCAAkC;IAClC,MAAM,OAAO,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;IAE9C,qBAAqB;IACrB,MAAM,UAAU,GAAwB,EAAE,CAAC;IAE3C,kCAAkC;IAClC,KAAK,MAAM,UAAU,IAAI,OAAO,EAAE,CAAC;QACjC,sDAAsD;QACtD,MAAM,cAAc,GAAG,SAAS,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACvD,MAAM,OAAO,GAAG,cAAc,EAAE,WAAW,EAAE,IAAI,KAAK,eAAe,CAAC;QAEtE,oDAAoD;QACpD,IAAI,OAAO,EAAE,CAAC;YACZ,wDAAwD;YACxD,UAAU,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC;QAClD,CAAC;aAAM,CAAC;YACN,2CAA2C;YAC3C,UAAU,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACjD,CAAC;IACH,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;AAC/B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/test/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAEpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { FieldMetadata } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Discovers all fields with @Inject decorator in a component
|
|
4
|
+
* Traverses prototype chain to support inheritance
|
|
5
|
+
*
|
|
6
|
+
* @param instance - Component instance to inspect
|
|
7
|
+
* @returns Array of field metadata containing field names, service names, and expressions
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const fields = discoverInjectedFields(new AuthService());
|
|
12
|
+
* // Returns: [
|
|
13
|
+
* // { fieldName: 'userService', serviceName: 'UserService', expression: undefined },
|
|
14
|
+
* // { fieldName: 'loginService', serviceName: 'LoginService', expression: (s) => s.login() }
|
|
15
|
+
* // ]
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function discoverInjectedFields(instance: any): FieldMetadata[];
|
|
19
|
+
/**
|
|
20
|
+
* Checks if a component has any injected dependencies
|
|
21
|
+
*
|
|
22
|
+
* @param ComponentClass - Component class to check
|
|
23
|
+
* @returns True if component has @Inject decorated fields
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* if (hasInjectedFields(AuthService)) {
|
|
28
|
+
* console.log('AuthService has dependencies');
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function hasInjectedFields(ComponentClass: new (...args: any[]) => any): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Gets the service name for a specific field
|
|
35
|
+
*
|
|
36
|
+
* @param ComponentClass - Component class
|
|
37
|
+
* @param fieldName - Name of the field
|
|
38
|
+
* @returns Service name if field is injected, undefined otherwise
|
|
39
|
+
*
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export declare function getFieldServiceName(ComponentClass: new (...args: any[]) => any, fieldName: string): string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Gets the expression function for a specific field
|
|
45
|
+
*
|
|
46
|
+
* @param ComponentClass - Component class
|
|
47
|
+
* @param fieldName - Name of the field
|
|
48
|
+
* @returns Expression function if defined, undefined otherwise
|
|
49
|
+
*
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
export declare function getFieldExpression(ComponentClass: new (...args: any[]) => any, fieldName: string): ((service: any) => any) | undefined;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ComponentConstants } from '../../ioc';
|
|
2
|
+
import { getTypedMetadata, getOwnTypedMetadata } from '../../utils';
|
|
3
|
+
/**
|
|
4
|
+
* Discovers all fields with @Inject decorator in a component
|
|
5
|
+
* Traverses prototype chain to support inheritance
|
|
6
|
+
*
|
|
7
|
+
* @param instance - Component instance to inspect
|
|
8
|
+
* @returns Array of field metadata containing field names, service names, and expressions
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const fields = discoverInjectedFields(new AuthService());
|
|
13
|
+
* // Returns: [
|
|
14
|
+
* // { fieldName: 'userService', serviceName: 'UserService', expression: undefined },
|
|
15
|
+
* // { fieldName: 'loginService', serviceName: 'LoginService', expression: (s) => s.login() }
|
|
16
|
+
* // ]
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function discoverInjectedFields(instance) {
|
|
20
|
+
const fields = [];
|
|
21
|
+
const processedFields = new Set();
|
|
22
|
+
// Traverse prototype chain (similar to how Container does it)
|
|
23
|
+
let currentPrototype = Object.getPrototypeOf(instance);
|
|
24
|
+
while (currentPrototype && currentPrototype !== Object.prototype) {
|
|
25
|
+
const constructor = currentPrototype.constructor;
|
|
26
|
+
// Get dependencies metadata for this level
|
|
27
|
+
const dependencies = getOwnTypedMetadata(ComponentConstants.DependencyKey, constructor);
|
|
28
|
+
// Get expressions metadata for this level
|
|
29
|
+
const expressions = getOwnTypedMetadata(ComponentConstants.ExpressionKey, constructor);
|
|
30
|
+
if (dependencies) {
|
|
31
|
+
// Process each dependency field
|
|
32
|
+
for (const [fieldName, serviceName] of Object.entries(dependencies)) {
|
|
33
|
+
// Skip if already processed (child class overrides)
|
|
34
|
+
if (processedFields.has(fieldName)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
processedFields.add(fieldName);
|
|
38
|
+
fields.push({
|
|
39
|
+
fieldName,
|
|
40
|
+
serviceName,
|
|
41
|
+
expression: expressions?.[fieldName],
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Move up the prototype chain
|
|
46
|
+
currentPrototype = Object.getPrototypeOf(currentPrototype);
|
|
47
|
+
}
|
|
48
|
+
return fields;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Checks if a component has any injected dependencies
|
|
52
|
+
*
|
|
53
|
+
* @param ComponentClass - Component class to check
|
|
54
|
+
* @returns True if component has @Inject decorated fields
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* if (hasInjectedFields(AuthService)) {
|
|
59
|
+
* console.log('AuthService has dependencies');
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function hasInjectedFields(ComponentClass) {
|
|
64
|
+
const dependencies = getTypedMetadata(ComponentConstants.DependencyKey, ComponentClass);
|
|
65
|
+
return dependencies !== undefined && Object.keys(dependencies).length > 0;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Gets the service name for a specific field
|
|
69
|
+
*
|
|
70
|
+
* @param ComponentClass - Component class
|
|
71
|
+
* @param fieldName - Name of the field
|
|
72
|
+
* @returns Service name if field is injected, undefined otherwise
|
|
73
|
+
*
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
76
|
+
export function getFieldServiceName(ComponentClass, fieldName) {
|
|
77
|
+
const dependencies = getTypedMetadata(ComponentConstants.DependencyKey, ComponentClass);
|
|
78
|
+
return dependencies?.[fieldName];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Gets the expression function for a specific field
|
|
82
|
+
*
|
|
83
|
+
* @param ComponentClass - Component class
|
|
84
|
+
* @param fieldName - Name of the field
|
|
85
|
+
* @returns Expression function if defined, undefined otherwise
|
|
86
|
+
*
|
|
87
|
+
* @internal
|
|
88
|
+
*/
|
|
89
|
+
export function getFieldExpression(ComponentClass, fieldName) {
|
|
90
|
+
const expressions = getTypedMetadata(ComponentConstants.ExpressionKey, ComponentClass);
|
|
91
|
+
return expressions?.[fieldName];
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../../../lib/test/metadata/discovery.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEpE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,sBAAsB,CAAC,QAAa;IAClD,MAAM,MAAM,GAAoB,EAAE,CAAC;IACnC,MAAM,eAAe,GAAG,IAAI,GAAG,EAAU,CAAC;IAE1C,8DAA8D;IAC9D,IAAI,gBAAgB,GAAG,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;IAEvD,OAAO,gBAAgB,IAAI,gBAAgB,KAAK,MAAM,CAAC,SAAS,EAAE,CAAC;QACjE,MAAM,WAAW,GAAG,gBAAgB,CAAC,WAAW,CAAC;QAEjD,2CAA2C;QAC3C,MAAM,YAAY,GAAG,mBAAmB,CAAe,kBAAkB,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;QAEtG,0CAA0C;QAC1C,MAAM,WAAW,GAAG,mBAAmB,CAAc,kBAAkB,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;QAEpG,IAAI,YAAY,EAAE,CAAC;YACjB,gCAAgC;YAChC,KAAK,MAAM,CAAC,SAAS,EAAE,WAAW,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;gBACpE,oDAAoD;gBACpD,IAAI,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;oBACnC,SAAS;gBACX,CAAC;gBAED,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBAE/B,MAAM,CAAC,IAAI,CAAC;oBACV,SAAS;oBACT,WAAW;oBACX,UAAU,EAAE,WAAW,EAAE,CAAC,SAAS,CAAC;iBACrC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,8BAA8B;QAC9B,gBAAgB,GAAG,MAAM,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;IAC7D,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,iBAAiB,CAAC,cAA2C;IAC3E,MAAM,YAAY,GAAG,gBAAgB,CAAe,kBAAkB,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC;IACtG,OAAO,YAAY,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;AAC5E,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CACjC,cAA2C,EAC3C,SAAiB;IAEjB,MAAM,YAAY,GAAG,gBAAgB,CAAe,kBAAkB,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC;IACtG,OAAO,YAAY,EAAE,CAAC,SAAS,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAChC,cAA2C,EAC3C,SAAiB;IAEjB,MAAM,WAAW,GAAG,gBAAgB,CAAc,kBAAkB,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC;IACpG,OAAO,WAAW,EAAE,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { MockComponentOptions, MockedComponent } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a test instance of a component with all @Inject dependencies automatically mocked
|
|
4
|
+
*
|
|
5
|
+
* This is the main API for testing Asena components. It discovers all @Inject decorated
|
|
6
|
+
* fields, generates mock objects for them, and injects them into a new instance.
|
|
7
|
+
*
|
|
8
|
+
* @template T - Type of the component being mocked
|
|
9
|
+
* @param ComponentClass - Component class to instantiate and mock
|
|
10
|
+
* @param options - Optional configuration for mocking behavior
|
|
11
|
+
* @returns Object containing the component instance and all mocked dependencies
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { mockComponent } from '@asenajs/asena';
|
|
16
|
+
* import { describe, test, expect, beforeEach } from 'bun:test';
|
|
17
|
+
*
|
|
18
|
+
* describe('AuthService', () => {
|
|
19
|
+
* let authService: AuthService;
|
|
20
|
+
* let mocks: Record<string, any>;
|
|
21
|
+
*
|
|
22
|
+
* beforeEach(() => {
|
|
23
|
+
* const result = mockComponent(AuthService);
|
|
24
|
+
* authService = result.instance;
|
|
25
|
+
* mocks = result.mocks;
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* test('register should create user', async () => {
|
|
29
|
+
* mocks.userService.createUser.mockResolvedValue({
|
|
30
|
+
* id: 'user-123',
|
|
31
|
+
* name: 'John Doe'
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* const result = await authService.register('John', 'john@example.com', 'pass');
|
|
35
|
+
*
|
|
36
|
+
* expect(result.user.id).toBe('user-123');
|
|
37
|
+
* expect(mocks.userService.createUser).toHaveBeenCalledTimes(1);
|
|
38
|
+
* });
|
|
39
|
+
* });
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* // With options - selective mocking
|
|
45
|
+
* const { instance, mocks } = mockComponent(PaymentService, {
|
|
46
|
+
* injections: ['stripeClient'],
|
|
47
|
+
* overrides: {
|
|
48
|
+
* stripeClient: myCustomStripeMock
|
|
49
|
+
* },
|
|
50
|
+
* postConstruct: (inst) => {
|
|
51
|
+
* console.log('Test instance ready');
|
|
52
|
+
* }
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare function mockComponent<T extends object>(ComponentClass: new (...args: any[]) => T, options?: MockComponentOptions): MockedComponent<T>;
|
|
57
|
+
/**
|
|
58
|
+
* Async version of mockComponent for components with async postConstruct
|
|
59
|
+
* Use this when your postConstruct hook is async
|
|
60
|
+
*
|
|
61
|
+
* @template T - Type of the component being mocked
|
|
62
|
+
* @param ComponentClass - Component class to instantiate and mock
|
|
63
|
+
* @param options - Optional configuration for mocking behavior
|
|
64
|
+
* @returns Promise resolving to object containing the component instance and mocked dependencies
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* const { instance, mocks } = await mockComponentAsync(DatabaseService, {
|
|
69
|
+
* postConstruct: async (inst) => {
|
|
70
|
+
* await inst.initialize();
|
|
71
|
+
* }
|
|
72
|
+
* });
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export declare function mockComponentAsync<T extends object>(ComponentClass: new (...args: any[]) => T, options?: MockComponentOptions): Promise<MockedComponent<T>>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { discoverInjectedFields } from './metadata/discovery';
|
|
2
|
+
import { createMockFromClass } from './factory/mockFactory';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a test instance of a component with all @Inject dependencies automatically mocked
|
|
5
|
+
*
|
|
6
|
+
* This is the main API for testing Asena components. It discovers all @Inject decorated
|
|
7
|
+
* fields, generates mock objects for them, and injects them into a new instance.
|
|
8
|
+
*
|
|
9
|
+
* @template T - Type of the component being mocked
|
|
10
|
+
* @param ComponentClass - Component class to instantiate and mock
|
|
11
|
+
* @param options - Optional configuration for mocking behavior
|
|
12
|
+
* @returns Object containing the component instance and all mocked dependencies
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { mockComponent } from '@asenajs/asena';
|
|
17
|
+
* import { describe, test, expect, beforeEach } from 'bun:test';
|
|
18
|
+
*
|
|
19
|
+
* describe('AuthService', () => {
|
|
20
|
+
* let authService: AuthService;
|
|
21
|
+
* let mocks: Record<string, any>;
|
|
22
|
+
*
|
|
23
|
+
* beforeEach(() => {
|
|
24
|
+
* const result = mockComponent(AuthService);
|
|
25
|
+
* authService = result.instance;
|
|
26
|
+
* mocks = result.mocks;
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* test('register should create user', async () => {
|
|
30
|
+
* mocks.userService.createUser.mockResolvedValue({
|
|
31
|
+
* id: 'user-123',
|
|
32
|
+
* name: 'John Doe'
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* const result = await authService.register('John', 'john@example.com', 'pass');
|
|
36
|
+
*
|
|
37
|
+
* expect(result.user.id).toBe('user-123');
|
|
38
|
+
* expect(mocks.userService.createUser).toHaveBeenCalledTimes(1);
|
|
39
|
+
* });
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* // With options - selective mocking
|
|
46
|
+
* const { instance, mocks } = mockComponent(PaymentService, {
|
|
47
|
+
* injections: ['stripeClient'],
|
|
48
|
+
* overrides: {
|
|
49
|
+
* stripeClient: myCustomStripeMock
|
|
50
|
+
* },
|
|
51
|
+
* postConstruct: (inst) => {
|
|
52
|
+
* console.log('Test instance ready');
|
|
53
|
+
* }
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function mockComponent(ComponentClass, options = {}) {
|
|
58
|
+
if (!ComponentClass || typeof ComponentClass !== 'function') {
|
|
59
|
+
throw new Error(`mockComponent expects a class constructor, but received: ${typeof ComponentClass}`);
|
|
60
|
+
}
|
|
61
|
+
const instance = new ComponentClass();
|
|
62
|
+
const allInjectedFields = discoverInjectedFields(instance);
|
|
63
|
+
const fieldsToMock = options.injections
|
|
64
|
+
? allInjectedFields.filter((field) => options.injections.includes(field.fieldName))
|
|
65
|
+
: allInjectedFields;
|
|
66
|
+
const mocks = {};
|
|
67
|
+
for (const field of fieldsToMock) {
|
|
68
|
+
const { fieldName, expression } = field;
|
|
69
|
+
let mockValue;
|
|
70
|
+
if (options.overrides?.[fieldName]) {
|
|
71
|
+
mockValue = options.overrides[fieldName];
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
mockValue = createMockFromClass(undefined);
|
|
75
|
+
}
|
|
76
|
+
if (expression) {
|
|
77
|
+
mockValue = expression(mockValue);
|
|
78
|
+
}
|
|
79
|
+
instance[fieldName] = mockValue;
|
|
80
|
+
mocks[fieldName] = mockValue;
|
|
81
|
+
}
|
|
82
|
+
if (options.postConstruct) {
|
|
83
|
+
const result = options.postConstruct(instance);
|
|
84
|
+
if (result instanceof Promise) {
|
|
85
|
+
return result.then(() => ({ instance, mocks }));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { instance, mocks };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Async version of mockComponent for components with async postConstruct
|
|
92
|
+
* Use this when your postConstruct hook is async
|
|
93
|
+
*
|
|
94
|
+
* @template T - Type of the component being mocked
|
|
95
|
+
* @param ComponentClass - Component class to instantiate and mock
|
|
96
|
+
* @param options - Optional configuration for mocking behavior
|
|
97
|
+
* @returns Promise resolving to object containing the component instance and mocked dependencies
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const { instance, mocks } = await mockComponentAsync(DatabaseService, {
|
|
102
|
+
* postConstruct: async (inst) => {
|
|
103
|
+
* await inst.initialize();
|
|
104
|
+
* }
|
|
105
|
+
* });
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export async function mockComponentAsync(ComponentClass, options = {}) {
|
|
109
|
+
const result = mockComponent(ComponentClass, options);
|
|
110
|
+
if (result instanceof Promise) {
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=mockComponent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mockComponent.js","sourceRoot":"","sources":["../../../lib/test/mockComponent.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,MAAM,UAAU,aAAa,CAC3B,cAAyC,EACzC,UAAgC,EAAE;IAElC,IAAI,CAAC,cAAc,IAAI,OAAO,cAAc,KAAK,UAAU,EAAE,CAAC;QAC5D,MAAM,IAAI,KAAK,CAAC,4DAA4D,OAAO,cAAc,EAAE,CAAC,CAAC;IACvG,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,cAAc,EAAE,CAAC;IACtC,MAAM,iBAAiB,GAAG,sBAAsB,CAAC,QAAQ,CAAC,CAAC;IAE3D,MAAM,YAAY,GAAG,OAAO,CAAC,UAAU;QACrC,CAAC,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACnF,CAAC,CAAC,iBAAiB,CAAC;IAEtB,MAAM,KAAK,GAAwB,EAAE,CAAC;IAEtC,KAAK,MAAM,KAAK,IAAI,YAAY,EAAE,CAAC;QACjC,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,KAAK,CAAC;QAExC,IAAI,SAAc,CAAC;QAEnB,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC;YACnC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QAC3C,CAAC;aAAM,CAAC;YACN,SAAS,GAAG,mBAAmB,CAAC,SAAS,CAAC,CAAC;QAC7C,CAAC;QAED,IAAI,UAAU,EAAE,CAAC;YACf,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;QACpC,CAAC;QAEA,QAAgB,CAAC,SAAS,CAAC,GAAG,SAAS,CAAC;QACzC,KAAK,CAAC,SAAS,CAAC,GAAG,SAAS,CAAC;IAC/B,CAAC;IAED,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAE/C,IAAI,MAAM,YAAY,OAAO,EAAE,CAAC;YAC9B,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAQ,CAAC;QACzD,CAAC;IACH,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,cAAyC,EACzC,UAAgC,EAAE;IAElC,MAAM,MAAM,GAAG,aAAa,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;IAEtD,IAAI,MAAM,YAAY,OAAO,EAAE,CAAC;QAC9B,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for configuring component mocking behavior
|
|
3
|
+
*/
|
|
4
|
+
export interface MockComponentOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Only mock specific injected fields (optional)
|
|
7
|
+
* If not provided, all @Inject decorated fields will be mocked
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* mockComponent(AuthService, {
|
|
12
|
+
* injections: ['userService'] // Only mock userService
|
|
13
|
+
* })
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
injections?: string[];
|
|
17
|
+
/**
|
|
18
|
+
* Provide custom mocks instead of auto-generated ones (optional)
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* mockComponent(AuthService, {
|
|
23
|
+
* overrides: {
|
|
24
|
+
* userService: myCustomMock
|
|
25
|
+
* }
|
|
26
|
+
* })
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
overrides?: Record<string, any>;
|
|
30
|
+
/**
|
|
31
|
+
* Lifecycle hook called after dependency injection (optional)
|
|
32
|
+
* Can be async
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* mockComponent(AuthService, {
|
|
37
|
+
* postConstruct: (instance) => {
|
|
38
|
+
* // Setup after injection
|
|
39
|
+
* }
|
|
40
|
+
* })
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
postConstruct?: (instance: any) => void | Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Result of mockComponent function
|
|
47
|
+
*/
|
|
48
|
+
export interface MockedComponent<T> {
|
|
49
|
+
/**
|
|
50
|
+
* Component instance with mocked dependencies
|
|
51
|
+
*/
|
|
52
|
+
instance: T;
|
|
53
|
+
/**
|
|
54
|
+
* Object containing all mocked dependencies
|
|
55
|
+
* Keys are field names, values are mock objects
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* const { instance, mocks } = mockComponent(AuthService);
|
|
60
|
+
* mocks.userService.createUser.mockResolvedValue({...});
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
mocks: Record<string, any>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Internal type for field metadata discovery
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
export interface FieldMetadata {
|
|
70
|
+
/**
|
|
71
|
+
* Name of the field
|
|
72
|
+
*/
|
|
73
|
+
fieldName: string;
|
|
74
|
+
/**
|
|
75
|
+
* Service name (from @Inject decorator)
|
|
76
|
+
*/
|
|
77
|
+
serviceName: string;
|
|
78
|
+
/**
|
|
79
|
+
* Optional expression function for transformation
|
|
80
|
+
*/
|
|
81
|
+
expression?: (service: any) => any;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Type alias for mock factory function
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
87
|
+
export type MockFactory = (classType: any) => any;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../lib/test/types.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asenajs/asena",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"author": "LibirSoft",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@changesets/cli": "^2.29.7",
|
|
16
16
|
"@types/bun": "latest",
|
|
17
|
-
"@typescript-eslint/eslint-plugin": "^8.46.
|
|
18
|
-
"@typescript-eslint/parser": "^8.46.
|
|
19
|
-
"eslint": "^9.39.
|
|
17
|
+
"@typescript-eslint/eslint-plugin": "^8.46.4",
|
|
18
|
+
"@typescript-eslint/parser": "^8.46.4",
|
|
19
|
+
"eslint": "^9.39.1",
|
|
20
20
|
"eslint-config-prettier": "^9.1.2",
|
|
21
21
|
"prettier": "^3.6.2",
|
|
22
22
|
"typescript": "^5.9.3",
|
|
23
|
-
"typescript-eslint": "^8.46.
|
|
23
|
+
"typescript-eslint": "^8.46.4"
|
|
24
24
|
},
|
|
25
25
|
"exports": {
|
|
26
26
|
".": {
|
|
@@ -86,6 +86,10 @@
|
|
|
86
86
|
"./utlis": {
|
|
87
87
|
"import": "./dist/lib/utils/index.js",
|
|
88
88
|
"types": "./dist/lib/utils/index.d.ts"
|
|
89
|
+
},
|
|
90
|
+
"./test": {
|
|
91
|
+
"import": "./dist/lib/test/index.js",
|
|
92
|
+
"types": "./dist/lib/test/index.d.ts"
|
|
89
93
|
}
|
|
90
94
|
},
|
|
91
95
|
"description": "Asena is a spring like framework for building web applications built with bun",
|