@haskou/value-objects 1.0.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/.editorconfig +15 -0
- package/.eslintignore +3 -0
- package/.eslintrc.json +218 -0
- package/.prettierrc.json +13 -0
- package/README.md +156 -0
- package/TECHNICAL_DOCUMENTATION.md +1091 -0
- package/jest.config.ts +30 -0
- package/package.json +49 -0
- package/src/errors/BaseError.ts +20 -0
- package/src/errors/DomainError.ts +12 -0
- package/src/errors/InvalidColorError.ts +7 -0
- package/src/errors/InvalidDayError.ts +7 -0
- package/src/errors/InvalidDayFormatError.ts +7 -0
- package/src/errors/InvalidEmailError.ts +7 -0
- package/src/errors/InvalidHourError.ts +7 -0
- package/src/errors/InvalidIntegerError.ts +7 -0
- package/src/errors/InvalidLatitudeError.ts +7 -0
- package/src/errors/InvalidLongitudeError.ts +7 -0
- package/src/errors/InvalidMinutesError.ts +7 -0
- package/src/errors/InvalidNumberError.ts +7 -0
- package/src/errors/InvalidPositiveNumberError.ts +7 -0
- package/src/errors/InvalidStringLengthError.ts +9 -0
- package/src/errors/InvalidTimestampIntervalError.ts +10 -0
- package/src/errors/NullObjectError.ts +8 -0
- package/src/errors/ValueNotInEnumError.ts +9 -0
- package/src/errors/index.ts +17 -0
- package/src/index.ts +5 -0
- package/src/interfaces/PrimitiveOf.ts +5 -0
- package/src/interfaces/index.ts +1 -0
- package/src/patterns/Assert.ts +15 -0
- package/src/patterns/NullObject.ts +60 -0
- package/src/patterns/ValueObject.ts +40 -0
- package/src/patterns/index.ts +3 -0
- package/src/types/Nullish.ts +1 -0
- package/src/types/Primitive.ts +1 -0
- package/src/types/index.ts +2 -0
- package/src/value-objects/Color.ts +39 -0
- package/src/value-objects/Email.ts +23 -0
- package/src/value-objects/Enum.ts +31 -0
- package/src/value-objects/Integer.ts +22 -0
- package/src/value-objects/NumberValueObject.ts +56 -0
- package/src/value-objects/PositiveNumber.ts +20 -0
- package/src/value-objects/StringValueObject.ts +27 -0
- package/src/value-objects/coordinates/Coordinates.ts +30 -0
- package/src/value-objects/coordinates/Latitude.ts +22 -0
- package/src/value-objects/coordinates/Longitude.ts +25 -0
- package/src/value-objects/coordinates/index.ts +3 -0
- package/src/value-objects/index.ts +9 -0
- package/src/value-objects/time/CalendarDay.ts +91 -0
- package/src/value-objects/time/Day.ts +17 -0
- package/src/value-objects/time/DayOfWeek.ts +60 -0
- package/src/value-objects/time/Duration.ts +142 -0
- package/src/value-objects/time/Hour.ts +105 -0
- package/src/value-objects/time/Month.ts +39 -0
- package/src/value-objects/time/MonthOfYear.ts +52 -0
- package/src/value-objects/time/Timestamp.ts +208 -0
- package/src/value-objects/time/TimestampInterval.ts +122 -0
- package/src/value-objects/time/Year.ts +27 -0
- package/src/value-objects/time/index.ts +10 -0
- package/tests/errors/BaseError.spec.ts +63 -0
- package/tests/errors/DomainError.spec.ts +52 -0
- package/tests/patterns/Assert.spec.ts +29 -0
- package/tests/patterns/NullObject.spec.ts +55 -0
- package/tests/setup.jest.ts +2 -0
- package/tests/value-objects/Color.spec.ts +214 -0
- package/tests/value-objects/Email.spec.ts +145 -0
- package/tests/value-objects/Enum.spec.ts +293 -0
- package/tests/value-objects/Integer.spec.ts +38 -0
- package/tests/value-objects/NumberValueObject.spec.ts +446 -0
- package/tests/value-objects/PositiveNumber.spec.ts +274 -0
- package/tests/value-objects/StringValueObject.spec.ts +135 -0
- package/tests/value-objects/coordinates/Coordinates.spec.ts +90 -0
- package/tests/value-objects/coordinates/Latitude.spec.ts +24 -0
- package/tests/value-objects/coordinates/Longitude.spec.ts +24 -0
- package/tests/value-objects/time/CalendarDay.spec.ts +182 -0
- package/tests/value-objects/time/Day.spec.ts +29 -0
- package/tests/value-objects/time/DayOfWeek.spec.ts +71 -0
- package/tests/value-objects/time/Duration.spec.ts +278 -0
- package/tests/value-objects/time/Hour.spec.ts +197 -0
- package/tests/value-objects/time/MonthOfYear.spec.ts +111 -0
- package/tests/value-objects/time/Timestamp.spec.ts +497 -0
- package/tests/value-objects/time/TimestampInterval.spec.ts +383 -0
- package/tests/value-objects/time/Year.spec.ts +48 -0
- package/tsconfig.jest.json +33 -0
- package/tsconfig.json +42 -0
package/.editorconfig
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
################################################
|
|
2
|
+
# ╔═╗╔╦╗╦╔╦╗╔═╗╦═╗┌─┐┌─┐┌┐┌┌─┐┬┌─┐
|
|
3
|
+
# ║╣ ║║║ ║ ║ ║╠╦╝│ │ ││││├┤ ││ ┬
|
|
4
|
+
# o╚═╝═╩╝╩ ╩ ╚═╝╩╚═└─┘└─┘┘└┘└ ┴└─┘
|
|
5
|
+
################################################
|
|
6
|
+
root = true
|
|
7
|
+
|
|
8
|
+
[*]
|
|
9
|
+
indent_style = space
|
|
10
|
+
indent_size = 2
|
|
11
|
+
end_of_line = lf
|
|
12
|
+
charset = utf-8
|
|
13
|
+
trim_trailing_whitespace = true
|
|
14
|
+
insert_final_newline = true
|
|
15
|
+
max_line_length = 80
|
package/.eslintignore
ADDED
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
{
|
|
2
|
+
"env": {
|
|
3
|
+
"es2022": true,
|
|
4
|
+
"node": true,
|
|
5
|
+
"jest": true
|
|
6
|
+
},
|
|
7
|
+
"parser": "@typescript-eslint/parser",
|
|
8
|
+
"plugins": [
|
|
9
|
+
"@typescript-eslint",
|
|
10
|
+
"prettier",
|
|
11
|
+
"unused-imports",
|
|
12
|
+
"sonarjs",
|
|
13
|
+
"perfectionist"
|
|
14
|
+
],
|
|
15
|
+
"extends": [
|
|
16
|
+
"eslint:recommended",
|
|
17
|
+
"plugin:@typescript-eslint/eslint-recommended",
|
|
18
|
+
"plugin:@typescript-eslint/recommended",
|
|
19
|
+
"plugin:prettier/recommended"
|
|
20
|
+
],
|
|
21
|
+
"parserOptions": {
|
|
22
|
+
"project": ["./tsconfig.json", "./tsconfig.jest.json"],
|
|
23
|
+
"sourceType": "module"
|
|
24
|
+
},
|
|
25
|
+
"rules": {
|
|
26
|
+
"lines-between-class-members": [
|
|
27
|
+
"error",
|
|
28
|
+
"always",
|
|
29
|
+
{
|
|
30
|
+
"exceptAfterSingleLine": true
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"padding-line-between-statements": [
|
|
34
|
+
"error",
|
|
35
|
+
{
|
|
36
|
+
"blankLine": "always",
|
|
37
|
+
"prev": "import",
|
|
38
|
+
"next": ["class", "export", "const", "let", "var"]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"blankLine": "always",
|
|
42
|
+
"prev": "*",
|
|
43
|
+
"next": ["return", "if"]
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"no-extra-boolean-cast": "error",
|
|
47
|
+
"@typescript-eslint/no-non-null-assertion": "off",
|
|
48
|
+
"@typescript-eslint/naming-convention": [
|
|
49
|
+
"error",
|
|
50
|
+
{
|
|
51
|
+
"selector": ["variable", "function"],
|
|
52
|
+
"format": ["camelCase", "PascalCase"],
|
|
53
|
+
"leadingUnderscore": "allow"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"selector": "interface",
|
|
57
|
+
"format": ["PascalCase"],
|
|
58
|
+
"custom": {
|
|
59
|
+
"regex": "^[A-Z]",
|
|
60
|
+
"match": true
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"no-unused-vars": "off",
|
|
65
|
+
"unused-imports/no-unused-imports": "error",
|
|
66
|
+
"unused-imports/no-unused-vars": [
|
|
67
|
+
"warn",
|
|
68
|
+
{
|
|
69
|
+
"vars": "all",
|
|
70
|
+
"varsIgnorePattern": "^_",
|
|
71
|
+
"args": "after-used",
|
|
72
|
+
"argsIgnorePattern": "^_"
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
"@typescript-eslint/no-use-before-define": "warn",
|
|
76
|
+
"@typescript-eslint/no-inferrable-types": "off",
|
|
77
|
+
"@typescript-eslint/no-this-alias": "warn",
|
|
78
|
+
"@typescript-eslint/no-require-imports": "error",
|
|
79
|
+
"@typescript-eslint/unbound-method": "off",
|
|
80
|
+
"require-await": "off",
|
|
81
|
+
"@typescript-eslint/require-await": "error",
|
|
82
|
+
"@typescript-eslint/no-misused-promises": "off",
|
|
83
|
+
"@typescript-eslint/no-floating-promises": "error",
|
|
84
|
+
"@typescript-eslint/prefer-for-of": "error",
|
|
85
|
+
"@typescript-eslint/member-delimiter-style": "off",
|
|
86
|
+
"@typescript-eslint/no-unsafe-return": "error",
|
|
87
|
+
"@typescript-eslint/no-unsafe-call": "warn",
|
|
88
|
+
"@typescript-eslint/no-unsafe-argument": "warn",
|
|
89
|
+
"max-classes-per-file": ["error", 1],
|
|
90
|
+
"no-multiple-empty-lines": [
|
|
91
|
+
1,
|
|
92
|
+
{
|
|
93
|
+
"max": 1,
|
|
94
|
+
"maxEOF": 0
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
"max-len": [
|
|
98
|
+
"error",
|
|
99
|
+
{
|
|
100
|
+
"code": 80,
|
|
101
|
+
"ignoreStrings": true,
|
|
102
|
+
"ignoreRegExpLiterals": true,
|
|
103
|
+
"ignoreTemplateLiterals": true
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"@typescript-eslint/explicit-member-accessibility": [
|
|
107
|
+
"error",
|
|
108
|
+
{
|
|
109
|
+
"ignoredMethodNames": ["constructor"],
|
|
110
|
+
"accessibility": "explicit"
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
"new-parens": "error",
|
|
114
|
+
"no-bitwise": "error",
|
|
115
|
+
"no-caller": "error",
|
|
116
|
+
"no-cond-assign": "error",
|
|
117
|
+
"no-console": "warn",
|
|
118
|
+
"no-debugger": "error",
|
|
119
|
+
"no-empty": "error",
|
|
120
|
+
"no-eval": "error",
|
|
121
|
+
"no-fallthrough": "off",
|
|
122
|
+
"no-invalid-this": "off",
|
|
123
|
+
"no-new-wrappers": "error",
|
|
124
|
+
"no-throw-literal": "error",
|
|
125
|
+
"no-trailing-spaces": "error",
|
|
126
|
+
"no-undef-init": "error",
|
|
127
|
+
"no-unsafe-finally": "error",
|
|
128
|
+
"no-unused-expressions": [
|
|
129
|
+
"error",
|
|
130
|
+
{
|
|
131
|
+
"allowShortCircuit": true
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
"no-unused-labels": "error",
|
|
135
|
+
"object-shorthand": "error",
|
|
136
|
+
"one-var": ["error", "never"],
|
|
137
|
+
"radix": "error",
|
|
138
|
+
"spaced-comment": "error",
|
|
139
|
+
"use-isnan": "error",
|
|
140
|
+
"valid-typeof": "off",
|
|
141
|
+
"complexity": ["error", 8],
|
|
142
|
+
"@typescript-eslint/member-ordering": [
|
|
143
|
+
"error",
|
|
144
|
+
{
|
|
145
|
+
"default": [
|
|
146
|
+
"field",
|
|
147
|
+
"static-field",
|
|
148
|
+
"private-static-field",
|
|
149
|
+
"public-static-field",
|
|
150
|
+
"private-instance-field",
|
|
151
|
+
"public-instance-field",
|
|
152
|
+
"private-static-method",
|
|
153
|
+
"public-static-method",
|
|
154
|
+
"constructor",
|
|
155
|
+
"private-method",
|
|
156
|
+
"public-method"
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
],
|
|
160
|
+
"max-params": ["warn", 7],
|
|
161
|
+
"max-nested-callbacks": ["warn", 2],
|
|
162
|
+
"max-depth": ["warn", 3],
|
|
163
|
+
"sonarjs/prefer-single-boolean-return": "error",
|
|
164
|
+
"sonarjs/no-collapsible-if": "error",
|
|
165
|
+
"sonarjs/no-duplicated-branches": "error",
|
|
166
|
+
"sonarjs/no-identical-expressions": "warn",
|
|
167
|
+
"sonarjs/no-nested-switch": "warn",
|
|
168
|
+
"perfectionist/sort-objects": [
|
|
169
|
+
"warn",
|
|
170
|
+
{
|
|
171
|
+
"type": "natural",
|
|
172
|
+
"order": "asc"
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
"perfectionist/sort-imports": [
|
|
176
|
+
"error",
|
|
177
|
+
{
|
|
178
|
+
"type": "natural",
|
|
179
|
+
"order": "asc"
|
|
180
|
+
}
|
|
181
|
+
],
|
|
182
|
+
"sonarjs/cognitive-complexity": ["error", 10],
|
|
183
|
+
"@typescript-eslint/explicit-module-boundary-types": [
|
|
184
|
+
"error",
|
|
185
|
+
{
|
|
186
|
+
"allowedNames": ["toPrimitives"]
|
|
187
|
+
}
|
|
188
|
+
],
|
|
189
|
+
"no-param-reassign": [
|
|
190
|
+
"warn",
|
|
191
|
+
{
|
|
192
|
+
"props": true
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
"no-restricted-imports": [
|
|
196
|
+
"error",
|
|
197
|
+
{
|
|
198
|
+
"paths": [
|
|
199
|
+
{
|
|
200
|
+
"name": "../infrastructure",
|
|
201
|
+
"message": "Domain layer cannot import infrastructure."
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
},
|
|
207
|
+
"overrides": [
|
|
208
|
+
{
|
|
209
|
+
"files": ["*.spec.ts"],
|
|
210
|
+
"rules": {
|
|
211
|
+
"no-unsafe-argument": "off",
|
|
212
|
+
"max-statements": "off",
|
|
213
|
+
"max-lines-per-function": "off",
|
|
214
|
+
"max-nested-callbacks": ["warn", 4]
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
}
|
package/.prettierrc.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tabWidth": 2,
|
|
3
|
+
"useTabs": false,
|
|
4
|
+
"printWidth": 80,
|
|
5
|
+
"singleQuote": true,
|
|
6
|
+
"trailingComma": "all",
|
|
7
|
+
"bracketSpacing": true,
|
|
8
|
+
"bracketSameLine": false,
|
|
9
|
+
"jsxBracketSameLine": false,
|
|
10
|
+
"semi": true,
|
|
11
|
+
"arrowParens": "always",
|
|
12
|
+
"endOfLine": "auto"
|
|
13
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
[](https://badge.fury.io/js/@haskou%2Fvalue-objects)
|
|
2
|
+
|
|
3
|
+
# Value Objects
|
|
4
|
+
|
|
5
|
+
A TypeScript dependency-less library for creating safe, immutable,
|
|
6
|
+
and validated **Value Objects**. Perfect for applications that require **Domain-Driven Design (DDD)** and **type safety**.
|
|
7
|
+
|
|
8
|
+
## 🚀 Quick Start
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @haskou/value-objects
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { Email, PositiveNumber, Color, Hour } from 'value-objects';
|
|
16
|
+
|
|
17
|
+
// ✅ Automatic validation on construction
|
|
18
|
+
const email = new Email('user@example.com'); // Valid
|
|
19
|
+
const price = new PositiveNumber(29.99); // Valid
|
|
20
|
+
const color = new Color('#FF0000'); // Valid
|
|
21
|
+
const hour = new Hour('09:30'); // Valid
|
|
22
|
+
|
|
23
|
+
// ❌ Immediate errors with invalid values
|
|
24
|
+
const badEmail = new Email('not-an-email'); // Error!
|
|
25
|
+
const badPrice = new PositiveNumber(-5); // Error!
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 🤔 Why use Value Objects?
|
|
29
|
+
|
|
30
|
+
**Without Value Objects:**
|
|
31
|
+
```typescript
|
|
32
|
+
function createUser(email: string, age: number): void {
|
|
33
|
+
// Are they valid? We don't know until runtime
|
|
34
|
+
if (!email.includes('@')) throw new Error('Invalid email');
|
|
35
|
+
if (age <= 0) throw new Error('Invalid age');
|
|
36
|
+
// Creation stuff
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**With Value Objects:**
|
|
41
|
+
```typescript
|
|
42
|
+
function createUser(email: Email, age: PositiveNumber): void {
|
|
43
|
+
// If we get here, values ARE valid
|
|
44
|
+
// Creation stuff
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This prevents defensive programming and if-else validations
|
|
49
|
+
since it implements Null Object pattern (see technical documentation).
|
|
50
|
+
Also value-objects can be modified easily keeping the same logic along
|
|
51
|
+
your application.
|
|
52
|
+
|
|
53
|
+
## 📦 Available Value Objects
|
|
54
|
+
|
|
55
|
+
### 🔤 Basic
|
|
56
|
+
- **`StringValueObject`** - Strings with length validation
|
|
57
|
+
- **`NumberValueObject`** - Numbers with math operations
|
|
58
|
+
- **`Integer`** - Whole numbers
|
|
59
|
+
- **`PositiveNumber`** - Positive numbers (> 0)
|
|
60
|
+
|
|
61
|
+
### ✨ Specialized
|
|
62
|
+
- **`Email`** - Email addresses with automatic validation
|
|
63
|
+
- **`Color`** - Hex colors (#FF0000)
|
|
64
|
+
|
|
65
|
+
### 🕒 Time
|
|
66
|
+
- **`Hour`** - Time in 24h format (09:30)
|
|
67
|
+
- **`Year`** - Years with leap year calculations
|
|
68
|
+
- **`CalendarDay`** - Calendar days
|
|
69
|
+
- **`Day`** - Days of month (1-31)
|
|
70
|
+
- **`DayOfWeek`** - Days of the week
|
|
71
|
+
- **`Month`** - Months (1-12)
|
|
72
|
+
- **`MonthOfYear`** - Month/year combinations
|
|
73
|
+
- **`Timestamp`** - Timestamps with operations
|
|
74
|
+
- **`TimestampInterval`** - Time intervals
|
|
75
|
+
- **`Duration`** - Duration in milliseconds
|
|
76
|
+
|
|
77
|
+
### 🌍 Coordinates
|
|
78
|
+
- **`Latitude`** - Latitude (-90 to 90)
|
|
79
|
+
- **`Longitude`** - Longitude (-180 to 180)
|
|
80
|
+
- **`Coordinates`** - Coordinate pairs
|
|
81
|
+
|
|
82
|
+
### 📝 Other
|
|
83
|
+
- **`Enum`** - Base class for typed enumerations
|
|
84
|
+
|
|
85
|
+
## 💡 Basic Examples
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// Validated strings
|
|
89
|
+
const name = new StringValueObject('John Doe');
|
|
90
|
+
const code = new StringValueObject('ABC', 3); // max 3 characters
|
|
91
|
+
|
|
92
|
+
// Numbers with operations
|
|
93
|
+
const price = new NumberValueObject(29.99);
|
|
94
|
+
const discount = new NumberValueObject(5.00);
|
|
95
|
+
const total = price.subtract(discount); // 24.99
|
|
96
|
+
|
|
97
|
+
// Emails
|
|
98
|
+
const email = new Email('user@example.com');
|
|
99
|
+
console.log(email.getDomain()); // 'example.com'
|
|
100
|
+
|
|
101
|
+
// Colors
|
|
102
|
+
const red = new Color('#FF0000');
|
|
103
|
+
const blue = Color.BLUE; // Predefined color
|
|
104
|
+
|
|
105
|
+
// Coordinates
|
|
106
|
+
const latitude = new Latitude(40.7128); // New York
|
|
107
|
+
const longitude = new Longitude(-74.0060);
|
|
108
|
+
const coords = new Coordinates(latitude, longitude);
|
|
109
|
+
|
|
110
|
+
// Time
|
|
111
|
+
const hour = new Hour('09:30');
|
|
112
|
+
const year = new Year(2024);
|
|
113
|
+
console.log(year.isLeapYear()); // true
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 📚 Technical Documentation
|
|
117
|
+
|
|
118
|
+
Need more details? Check the complete documentation:
|
|
119
|
+
|
|
120
|
+
**[📖 TECHNICAL_DOCUMENTATION.md](./TECHNICAL_DOCUMENTATION.md)**
|
|
121
|
+
|
|
122
|
+
Includes:
|
|
123
|
+
- ✅ Complete API for all Value Objects
|
|
124
|
+
- ✅ Advanced usage examples
|
|
125
|
+
- ✅ Error handling
|
|
126
|
+
- ✅ Design principles (immutability, null safety, etc.)
|
|
127
|
+
- ✅ Composition and extensibility patterns
|
|
128
|
+
|
|
129
|
+
## 🛠️ Development
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Install dependencies
|
|
133
|
+
npm install
|
|
134
|
+
|
|
135
|
+
# Run tests
|
|
136
|
+
npm test
|
|
137
|
+
|
|
138
|
+
# Build
|
|
139
|
+
npm run build
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## 🤝 Contributing
|
|
143
|
+
|
|
144
|
+
1. Fork the repository
|
|
145
|
+
2. Create a branch: `git checkout -b my-feature`
|
|
146
|
+
3. Make your changes and add tests
|
|
147
|
+
4. Run tests: `npm test`
|
|
148
|
+
5. Submit a pull request
|
|
149
|
+
|
|
150
|
+
## 📄 License
|
|
151
|
+
|
|
152
|
+
MIT License - see the [LICENSE](LICENSE) file for details.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
**Made with ❤️ and TypeScript**
|