ts-class-to-openapi 1.0.2 → 1.0.4
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 +238 -21
- package/dist/__test__/entities/complex-generics.entity.d.ts +33 -0
- package/dist/__test__/entities/comprehensive-enum.entity.d.ts +23 -0
- package/dist/__test__/entities/enum.entity.d.ts +29 -0
- package/dist/__test__/entities/user-role-generic.entity.d.ts +13 -0
- package/dist/__test__/enum.test.d.ts +1 -0
- package/dist/__test__/generic-types.test.d.ts +1 -0
- package/dist/__test__/index.d.ts +2 -0
- package/dist/index.esm.js +529 -60
- package/dist/index.js +529 -60
- package/dist/run.js +529 -60
- package/dist/transformer.d.ts +147 -1
- package/dist/transformer.fixtures.d.ts +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
A powerful library that automatically converts your TypeScript classes into OpenAPI-compatible schemas. Works with **pure TypeScript classes** and **class-validator decorated classes**, with zero runtime dependencies.
|
|
6
6
|
|
|
7
|
-
> **🎯
|
|
7
|
+
> **🎯 Latest Features**:
|
|
8
|
+
>
|
|
9
|
+
> - **Pure TypeScript classes** without requiring any decorators or external dependencies!
|
|
10
|
+
> - **Full enum support** with `@IsEnum` decorator for string, numeric, and object enums
|
|
11
|
+
> - Perfect for transforming existing codebases instantly.
|
|
8
12
|
|
|
9
13
|
## 🚀 Key Features
|
|
10
14
|
|
|
11
15
|
- ✅ **Pure TypeScript Support** - Transform any TypeScript class without decorators
|
|
12
16
|
- ✅ **class-validator Compatible** - Enhanced schemas with validation decorators
|
|
17
|
+
- ✅ **Enum Support** - Full support for TypeScript enums and object enums with @IsEnum
|
|
13
18
|
- ✅ **CommonJS & ESM Compatible** - Works in any Node.js project
|
|
14
19
|
- ✅ **Zero Runtime Dependencies** - No `reflect-metadata` or `emitDecoratorMetadata` required
|
|
15
20
|
- ✅ **OpenAPI 3.1.0** - Industry-standard schema generation
|
|
@@ -81,7 +86,12 @@ This library is **100% compatible with both CommonJS and ESM**, allowing you to
|
|
|
81
86
|
```typescript
|
|
82
87
|
// ESM import
|
|
83
88
|
import { transform } from 'ts-class-to-openapi'
|
|
84
|
-
import { IsString, IsEmail, IsNotEmpty } from 'class-validator'
|
|
89
|
+
import { IsString, IsEmail, IsNotEmpty, IsEnum } from 'class-validator'
|
|
90
|
+
|
|
91
|
+
enum UserType {
|
|
92
|
+
ADMIN = 'admin',
|
|
93
|
+
USER = 'user',
|
|
94
|
+
}
|
|
85
95
|
|
|
86
96
|
class User {
|
|
87
97
|
@IsString()
|
|
@@ -90,6 +100,9 @@ class User {
|
|
|
90
100
|
|
|
91
101
|
@IsEmail()
|
|
92
102
|
email: string
|
|
103
|
+
|
|
104
|
+
@IsEnum(UserType)
|
|
105
|
+
type: UserType
|
|
93
106
|
}
|
|
94
107
|
|
|
95
108
|
const schema = transform(User)
|
|
@@ -101,7 +114,12 @@ console.log(JSON.stringify(schema, null, 2))
|
|
|
101
114
|
```typescript
|
|
102
115
|
// TypeScript with CommonJS configuration
|
|
103
116
|
import { transform } from 'ts-class-to-openapi'
|
|
104
|
-
import { IsString, IsEmail, IsNotEmpty } from 'class-validator'
|
|
117
|
+
import { IsString, IsEmail, IsNotEmpty, IsEnum } from 'class-validator'
|
|
118
|
+
|
|
119
|
+
enum UserType {
|
|
120
|
+
ADMIN = 'admin',
|
|
121
|
+
USER = 'user',
|
|
122
|
+
}
|
|
105
123
|
|
|
106
124
|
class User {
|
|
107
125
|
@IsString()
|
|
@@ -110,6 +128,9 @@ class User {
|
|
|
110
128
|
|
|
111
129
|
@IsEmail()
|
|
112
130
|
email: string
|
|
131
|
+
|
|
132
|
+
@IsEnum(UserType)
|
|
133
|
+
type: UserType
|
|
113
134
|
}
|
|
114
135
|
|
|
115
136
|
const schema = transform(User)
|
|
@@ -173,8 +194,22 @@ import {
|
|
|
173
194
|
IsPositive,
|
|
174
195
|
IsArray,
|
|
175
196
|
IsNotEmpty,
|
|
197
|
+
IsEnum,
|
|
176
198
|
} from 'class-validator'
|
|
177
199
|
|
|
200
|
+
// Define enums for better API contracts
|
|
201
|
+
enum ProductStatus {
|
|
202
|
+
DRAFT = 'draft',
|
|
203
|
+
PUBLISHED = 'published',
|
|
204
|
+
ARCHIVED = 'archived',
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
enum Priority {
|
|
208
|
+
LOW = 1,
|
|
209
|
+
MEDIUM = 2,
|
|
210
|
+
HIGH = 3,
|
|
211
|
+
}
|
|
212
|
+
|
|
178
213
|
// Enhanced with validation decorators
|
|
179
214
|
class Product {
|
|
180
215
|
@IsNumber()
|
|
@@ -189,6 +224,13 @@ class Product {
|
|
|
189
224
|
@IsPositive()
|
|
190
225
|
price: number
|
|
191
226
|
|
|
227
|
+
@IsEnum(ProductStatus)
|
|
228
|
+
@IsNotEmpty()
|
|
229
|
+
status: ProductStatus
|
|
230
|
+
|
|
231
|
+
@IsEnum(Priority)
|
|
232
|
+
priority?: Priority
|
|
233
|
+
|
|
192
234
|
@IsArray()
|
|
193
235
|
categories: string[]
|
|
194
236
|
}
|
|
@@ -204,6 +246,7 @@ const schema = transform(Product)
|
|
|
204
246
|
- ✅ String length constraints
|
|
205
247
|
- ✅ Number range validation
|
|
206
248
|
- ✅ Array size validation
|
|
249
|
+
- ✅ Enum value constraints with automatic type detection
|
|
207
250
|
|
|
208
251
|
## 🚀 Quick Start
|
|
209
252
|
|
|
@@ -264,7 +307,7 @@ console.log(JSON.stringify(result, null, 2))
|
|
|
264
307
|
"format": "date-time"
|
|
265
308
|
}
|
|
266
309
|
},
|
|
267
|
-
"required": []
|
|
310
|
+
"required": ["id", "name", "email", "age", "isActive", "tags", "createdAt"]
|
|
268
311
|
}
|
|
269
312
|
}
|
|
270
313
|
```
|
|
@@ -319,7 +362,7 @@ console.log(JSON.stringify(result, null, 2))
|
|
|
319
362
|
"maximum": 100
|
|
320
363
|
}
|
|
321
364
|
},
|
|
322
|
-
"required": ["name"]
|
|
365
|
+
"required": ["name", "email", "age"]
|
|
323
366
|
}
|
|
324
367
|
}
|
|
325
368
|
```
|
|
@@ -750,7 +793,7 @@ const schema = transform(User)
|
|
|
750
793
|
"format": "binary"
|
|
751
794
|
}
|
|
752
795
|
},
|
|
753
|
-
"required": ["id", "tags", "role", "avatar"]
|
|
796
|
+
"required": ["id", "name", "email", "tags", "createdAt", "role", "files", "avatar"]
|
|
754
797
|
}
|
|
755
798
|
}
|
|
756
799
|
```
|
|
@@ -765,7 +808,7 @@ const schema = transform(User)
|
|
|
765
808
|
| **Configuration** | None | `experimentalDecorators: true` |
|
|
766
809
|
| **Type Detection** | Automatic | Automatic + Decorators |
|
|
767
810
|
| **Validation Rules** | Basic types only | Rich validation constraints |
|
|
768
|
-
| **Required Fields** |
|
|
811
|
+
| **Required Fields** | Based on TypeScript optional operator (`?`) | TypeScript optional operator + decorators |
|
|
769
812
|
| **String Constraints** | None | Min/max length, patterns |
|
|
770
813
|
| **Number Constraints** | None | Min/max values, positive |
|
|
771
814
|
| **Array Constraints** | None | Min/max items, non-empty |
|
|
@@ -779,11 +822,11 @@ const schema = transform(User)
|
|
|
779
822
|
|
|
780
823
|
```typescript
|
|
781
824
|
class User {
|
|
782
|
-
name: string
|
|
783
|
-
email: string
|
|
784
|
-
age: number
|
|
825
|
+
name: string // Required (no ? operator)
|
|
826
|
+
email: string // Required (no ? operator)
|
|
827
|
+
age: number // Required (no ? operator)
|
|
785
828
|
}
|
|
786
|
-
// Generates: All properties
|
|
829
|
+
// Generates: All properties required (no ? operator), basic types
|
|
787
830
|
```
|
|
788
831
|
|
|
789
832
|
**Enhanced Class:**
|
|
@@ -801,21 +844,126 @@ class User {
|
|
|
801
844
|
@Min(18)
|
|
802
845
|
age: number
|
|
803
846
|
}
|
|
804
|
-
// Generates:
|
|
847
|
+
// Generates: All properties required, email format validation, age minimum 18
|
|
805
848
|
```
|
|
806
849
|
|
|
807
850
|
## 🎨 Supported Decorators Reference
|
|
808
851
|
|
|
809
852
|
### Type Validation Decorators
|
|
810
853
|
|
|
811
|
-
| Decorator
|
|
812
|
-
|
|
|
813
|
-
| `@IsString()`
|
|
814
|
-
| `@IsInt()`
|
|
815
|
-
| `@IsNumber()`
|
|
816
|
-
| `@IsBoolean()`
|
|
817
|
-
| `@IsEmail()`
|
|
818
|
-
| `@IsDate()`
|
|
854
|
+
| Decorator | Generated Schema Property | Description |
|
|
855
|
+
| --------------------- | ----------------------------------------------- | --------------------------- |
|
|
856
|
+
| `@IsString()` | `type: "string"` | String type validation |
|
|
857
|
+
| `@IsInt()` | `type: "integer", format: "int32"` | Integer type validation |
|
|
858
|
+
| `@IsNumber()` | `type: "number", format: "double"` | Number type validation |
|
|
859
|
+
| `@IsBoolean()` | `type: "boolean"` | Boolean type validation |
|
|
860
|
+
| `@IsEmail()` | `type: "string", format: "email"` | Email format validation |
|
|
861
|
+
| `@IsDate()` | `type: "string", format: "date-time"` | Date-time format validation |
|
|
862
|
+
| `@IsEnum(enumObject)` | `type: "string/number/boolean", enum: [values]` | Enum constraint validation |
|
|
863
|
+
|
|
864
|
+
### Enum Validation with @IsEnum
|
|
865
|
+
|
|
866
|
+
The `@IsEnum()` decorator provides powerful enum validation with automatic type detection and value extraction:
|
|
867
|
+
|
|
868
|
+
```typescript
|
|
869
|
+
import { transform } from 'ts-class-to-openapi'
|
|
870
|
+
import { IsEnum, IsNotEmpty, IsArray } from 'class-validator'
|
|
871
|
+
|
|
872
|
+
// String enum
|
|
873
|
+
enum UserRole {
|
|
874
|
+
ADMIN = 'admin',
|
|
875
|
+
USER = 'user',
|
|
876
|
+
MODERATOR = 'moderator',
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Numeric enum
|
|
880
|
+
enum Priority {
|
|
881
|
+
LOW = 1,
|
|
882
|
+
MEDIUM = 2,
|
|
883
|
+
HIGH = 3,
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Auto-incremented enum
|
|
887
|
+
enum Size {
|
|
888
|
+
SMALL, // 0
|
|
889
|
+
MEDIUM, // 1
|
|
890
|
+
LARGE, // 2
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Object enum with 'as const'
|
|
894
|
+
const Status = {
|
|
895
|
+
ACTIVE: 'active',
|
|
896
|
+
INACTIVE: 'inactive',
|
|
897
|
+
PENDING: 'pending',
|
|
898
|
+
} as const
|
|
899
|
+
|
|
900
|
+
class TaskEntity {
|
|
901
|
+
@IsEnum(UserRole)
|
|
902
|
+
@IsNotEmpty()
|
|
903
|
+
assignedRole: UserRole // Required enum
|
|
904
|
+
|
|
905
|
+
@IsEnum(Priority)
|
|
906
|
+
priority?: Priority // Optional enum
|
|
907
|
+
|
|
908
|
+
@IsEnum(Size)
|
|
909
|
+
size?: Size // Auto-incremented enum
|
|
910
|
+
|
|
911
|
+
@IsEnum(Status)
|
|
912
|
+
status?: keyof typeof Status // Object enum
|
|
913
|
+
|
|
914
|
+
@IsEnum(UserRole)
|
|
915
|
+
@IsArray()
|
|
916
|
+
allowedRoles?: UserRole[] // Array of enums
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const result = transform(TaskEntity)
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
**Generated Output:**
|
|
923
|
+
|
|
924
|
+
```json
|
|
925
|
+
{
|
|
926
|
+
"name": "TaskEntity",
|
|
927
|
+
"schema": {
|
|
928
|
+
"type": "object",
|
|
929
|
+
"properties": {
|
|
930
|
+
"assignedRole": {
|
|
931
|
+
"type": "string",
|
|
932
|
+
"enum": ["admin", "user", "moderator"]
|
|
933
|
+
},
|
|
934
|
+
"priority": {
|
|
935
|
+
"type": "number",
|
|
936
|
+
"enum": [1, 2, 3]
|
|
937
|
+
},
|
|
938
|
+
"size": {
|
|
939
|
+
"type": "number",
|
|
940
|
+
"enum": [0, 1, 2]
|
|
941
|
+
},
|
|
942
|
+
"status": {
|
|
943
|
+
"type": "string",
|
|
944
|
+
"enum": ["active", "inactive", "pending"]
|
|
945
|
+
},
|
|
946
|
+
"allowedRoles": {
|
|
947
|
+
"type": "array",
|
|
948
|
+
"items": {
|
|
949
|
+
"type": "string",
|
|
950
|
+
"enum": ["admin", "user", "moderator"]
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
},
|
|
954
|
+
"required": ["assignedRole"]
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
**Supported Enum Types:**
|
|
960
|
+
|
|
961
|
+
- ✅ **String enums**: `enum Color { RED = 'red' }`
|
|
962
|
+
- ✅ **Numeric enums**: `enum Status { ACTIVE = 1 }`
|
|
963
|
+
- ✅ **Auto-incremented enums**: `enum Size { SMALL, MEDIUM }`
|
|
964
|
+
- ✅ **Object enums**: `const Colors = { RED: 'red' } as const`
|
|
965
|
+
- ✅ **Mixed enums**: `enum Mixed { NUM = 1, STR = 'text' }`
|
|
966
|
+
- ✅ **Array of enums**: `roles: UserRole[]`
|
|
819
967
|
|
|
820
968
|
### String Validation Decorators
|
|
821
969
|
|
|
@@ -933,7 +1081,7 @@ console.log(JSON.stringify(schema, null, 2))
|
|
|
933
1081
|
"format": "binary"
|
|
934
1082
|
}
|
|
935
1083
|
},
|
|
936
|
-
"required": ["document"]
|
|
1084
|
+
"required": ["document", "attachments"]
|
|
937
1085
|
}
|
|
938
1086
|
}
|
|
939
1087
|
```
|
|
@@ -981,6 +1129,75 @@ console.log(result.name) // "User"
|
|
|
981
1129
|
console.log(result.schema) // OpenAPI schema object
|
|
982
1130
|
```
|
|
983
1131
|
|
|
1132
|
+
## 🔍 Required Properties Rules
|
|
1133
|
+
|
|
1134
|
+
Understanding how properties are marked as required is crucial for generating accurate schemas:
|
|
1135
|
+
|
|
1136
|
+
### TypeScript Optional Operator (`?`)
|
|
1137
|
+
|
|
1138
|
+
The presence or absence of the TypeScript optional operator (`?`) determines if a property is required:
|
|
1139
|
+
|
|
1140
|
+
```typescript
|
|
1141
|
+
class User {
|
|
1142
|
+
name: string // ✅ REQUIRED (no ? operator)
|
|
1143
|
+
email: string // ✅ REQUIRED (no ? operator)
|
|
1144
|
+
age?: number // ❌ OPTIONAL (has ? operator)
|
|
1145
|
+
bio?: string // ❌ OPTIONAL (has ? operator)
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// Generated schema:
|
|
1149
|
+
// "required": ["name", "email"]
|
|
1150
|
+
```
|
|
1151
|
+
|
|
1152
|
+
### class-validator Decorators Override
|
|
1153
|
+
|
|
1154
|
+
When using class-validator decorators, they can override the TypeScript optional behavior:
|
|
1155
|
+
|
|
1156
|
+
```typescript
|
|
1157
|
+
import { IsNotEmpty, IsOptional } from 'class-validator'
|
|
1158
|
+
|
|
1159
|
+
class User {
|
|
1160
|
+
@IsNotEmpty()
|
|
1161
|
+
requiredField?: string // ✅ REQUIRED (@IsNotEmpty overrides ?)
|
|
1162
|
+
|
|
1163
|
+
@IsOptional()
|
|
1164
|
+
optionalField: string // ❌ OPTIONAL (@IsOptional overrides no ?)
|
|
1165
|
+
|
|
1166
|
+
normalField: string // ✅ REQUIRED (no ? operator)
|
|
1167
|
+
normalOptional?: string // ❌ OPTIONAL (has ? operator)
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// Generated schema:
|
|
1171
|
+
// "required": ["requiredField", "normalField"]
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
### Array Properties
|
|
1175
|
+
|
|
1176
|
+
Array properties follow the same rules, with additional decorators:
|
|
1177
|
+
|
|
1178
|
+
```typescript
|
|
1179
|
+
import { ArrayNotEmpty } from 'class-validator'
|
|
1180
|
+
|
|
1181
|
+
class User {
|
|
1182
|
+
tags: string[] // ✅ REQUIRED (no ? operator)
|
|
1183
|
+
|
|
1184
|
+
@ArrayNotEmpty()
|
|
1185
|
+
categories?: string[] // ✅ REQUIRED (@ArrayNotEmpty overrides ?)
|
|
1186
|
+
|
|
1187
|
+
optionalTags?: string[] // ❌ OPTIONAL (has ? operator)
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Generated schema:
|
|
1191
|
+
// "required": ["tags", "categories"]
|
|
1192
|
+
```
|
|
1193
|
+
|
|
1194
|
+
### Summary of Rules
|
|
1195
|
+
|
|
1196
|
+
1. **No `?` operator** → Property is **REQUIRED**
|
|
1197
|
+
2. **Has `?` operator** → Property is **OPTIONAL**
|
|
1198
|
+
3. **`@IsNotEmpty()` or `@ArrayNotEmpty()`** → Forces **REQUIRED** (overrides `?`)
|
|
1199
|
+
4. **`@IsOptional()`** → Forces **OPTIONAL** (overrides no `?`)
|
|
1200
|
+
|
|
984
1201
|
## 🌟 Advanced Features
|
|
985
1202
|
|
|
986
1203
|
- ✅ **Pure TypeScript Support** - Works with any TypeScript class, no decorators required
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type ApiResponse<T> = {
|
|
2
|
+
data: T;
|
|
3
|
+
message: string;
|
|
4
|
+
success: boolean;
|
|
5
|
+
};
|
|
6
|
+
export type PaginatedResponse<T> = {
|
|
7
|
+
items: T[];
|
|
8
|
+
total: number;
|
|
9
|
+
page: number;
|
|
10
|
+
};
|
|
11
|
+
export declare class Product {
|
|
12
|
+
id: number;
|
|
13
|
+
name: string;
|
|
14
|
+
price: number;
|
|
15
|
+
}
|
|
16
|
+
export declare class UserEntity {
|
|
17
|
+
id: number;
|
|
18
|
+
username: string;
|
|
19
|
+
email: string;
|
|
20
|
+
}
|
|
21
|
+
export declare class ProductResponseDto {
|
|
22
|
+
response: ApiResponse<Product>;
|
|
23
|
+
}
|
|
24
|
+
export declare class UserListDto {
|
|
25
|
+
users: PaginatedResponse<UserEntity>;
|
|
26
|
+
}
|
|
27
|
+
export type KeyValuePair<K, V> = {
|
|
28
|
+
key: K;
|
|
29
|
+
value: V;
|
|
30
|
+
};
|
|
31
|
+
export declare class ConfigDto {
|
|
32
|
+
setting: KeyValuePair<string, number>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
declare enum Color {
|
|
2
|
+
RED = "red",
|
|
3
|
+
GREEN = "green",
|
|
4
|
+
BLUE = "blue"
|
|
5
|
+
}
|
|
6
|
+
declare enum Size {
|
|
7
|
+
SMALL = 0,
|
|
8
|
+
MEDIUM = 1,
|
|
9
|
+
LARGE = 2
|
|
10
|
+
}
|
|
11
|
+
declare enum HttpStatus {
|
|
12
|
+
OK = 200,
|
|
13
|
+
NOT_FOUND = 404,
|
|
14
|
+
ERROR = "server_error"
|
|
15
|
+
}
|
|
16
|
+
export declare class ComprehensiveEnumEntity {
|
|
17
|
+
primaryColor: Color;
|
|
18
|
+
size?: Size;
|
|
19
|
+
status?: HttpStatus;
|
|
20
|
+
availableColors?: Color[];
|
|
21
|
+
supportedSizes: Size[];
|
|
22
|
+
}
|
|
23
|
+
export { Color, Size, HttpStatus };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
declare enum UserRole {
|
|
2
|
+
ADMIN = "admin",
|
|
3
|
+
USER = "user",
|
|
4
|
+
MODERATOR = "moderator"
|
|
5
|
+
}
|
|
6
|
+
declare enum Priority {
|
|
7
|
+
LOW = 1,
|
|
8
|
+
MEDIUM = 2,
|
|
9
|
+
HIGH = 3
|
|
10
|
+
}
|
|
11
|
+
declare const Status: {
|
|
12
|
+
readonly ACTIVE: "active";
|
|
13
|
+
readonly INACTIVE: "inactive";
|
|
14
|
+
readonly PENDING: "pending";
|
|
15
|
+
};
|
|
16
|
+
declare enum BooleanEnum {
|
|
17
|
+
TRUE = "true",
|
|
18
|
+
FALSE = "false"
|
|
19
|
+
}
|
|
20
|
+
export declare class EnumTestEntity {
|
|
21
|
+
role: UserRole;
|
|
22
|
+
priority?: Priority;
|
|
23
|
+
status?: keyof typeof Status;
|
|
24
|
+
flag?: BooleanEnum;
|
|
25
|
+
roles?: UserRole[];
|
|
26
|
+
priorities?: Priority[];
|
|
27
|
+
optionalRole?: UserRole;
|
|
28
|
+
}
|
|
29
|
+
export { UserRole, Priority, Status, BooleanEnum };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|