partial-uploader 0.0.6 → 0.0.7

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/dist/index.d.ts CHANGED
@@ -1,4 +1,9 @@
1
- declare const uploadWithPartialFile: (url: string, file: any, headers?: any, chunkSize?: number) => Promise<{
1
+ type RefreshResult = {
2
+ headers?: any;
3
+ url?: string;
4
+ };
5
+ type UnauthorizedCallback = (error: Response) => Promise<RefreshResult | null>;
6
+ declare const uploadWithPartialFile: (url: string, file: any, headers?: any, chunkSize?: number, delay_number?: number, concurrency?: number, onUnauthorized?: UnauthorizedCallback) => Promise<{
2
7
  success: boolean;
3
8
  id: string;
4
9
  message: string;
package/dist/index.js CHANGED
@@ -1,4 +1,15 @@
1
1
  "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
2
13
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
14
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
15
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -8,85 +19,172 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
19
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
20
  });
10
21
  };
22
+ var __generator = (this && this.__generator) || function (thisArg, body) {
23
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
24
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
25
+ function verb(n) { return function (v) { return step([n, v]); }; }
26
+ function step(op) {
27
+ if (f) throw new TypeError("Generator is already executing.");
28
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
29
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
30
+ if (y = 0, t) op = [op[0] & 2, t.value];
31
+ switch (op[0]) {
32
+ case 0: case 1: t = op; break;
33
+ case 4: _.label++; return { value: op[1], done: false };
34
+ case 5: _.label++; y = op[1]; op = [0]; continue;
35
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
36
+ default:
37
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
38
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
39
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
40
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
41
+ if (t[2]) _.ops.pop();
42
+ _.trys.pop(); continue;
43
+ }
44
+ op = body.call(thisArg, _);
45
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
46
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
47
+ }
48
+ };
11
49
  Object.defineProperty(exports, "__esModule", { value: true });
12
50
  exports.uploadWithPartialFile = void 0;
13
- const uploadWithPartialFile = (url, file, headers, chunkSize = 26214400) => __awaiter(void 0, void 0, void 0, function* () {
14
- yield delay(50);
15
- const id = generateGuid();
16
- if (file.size <= chunkSize) {
17
- // small file
18
- let res = yield upload(url, file, 1, file.size, file.name, id, 0, headers);
19
- if (!res)
20
- return { success: false, id, message: "file could not be loaded" };
21
- }
22
- else {
23
- // big file
24
- const chunks = splitFileIntoChunks(file, chunkSize);
25
- for (let i = 0; i < chunks.length; i++) {
26
- let chunk = chunks[i];
27
- if (!chunk)
28
- return { success: false, id, message: "chunk is undefined" };
29
- let res = yield upload(url, chunk, chunks.length, file.size, file.name, id, i, headers);
30
- if (!res)
31
- return { success: false, id, message: "file could not be loaded" };
32
- yield delay(550);
33
- }
34
- ;
35
- }
36
- ;
37
- return { success: true, id, message: "file uploaded successfully" };
38
- });
39
- exports.uploadWithPartialFile = uploadWithPartialFile;
40
- const upload = (url, chunk, chunksLength, fileSize, filename, fileGuid, index, headers) => __awaiter(void 0, void 0, void 0, function* () {
41
- let formData = new FormData();
42
- formData.append('file', chunk, `${filename}_chunk_${index}`);
43
- formData.append('fileGuid', fileGuid);
44
- let isDone = chunksLength === index + 1;
45
- formData.append('isDone', isDone.toString());
46
- formData.append('totalSize', fileSize.toString());
47
- formData.append('totalChunks', chunksLength.toString());
48
- formData.append('filename', filename);
49
- formData.append('index', index.toString());
50
- return getRes(url, formData, headers);
51
- });
52
- const getRes = (url, formData, headers) => __awaiter(void 0, void 0, void 0, function* () {
53
- try {
54
- let res = yield uploadSubscribe(url, formData, headers);
55
- if (res.status === 406 || res.status === 401)
56
- return false;
57
- }
58
- catch (e) {
59
- yield delay(500);
60
- let res = yield uploadSubscribe(url, formData, headers);
61
- if (!res.ok)
62
- return false;
63
- }
64
- ;
65
- return true;
66
- });
67
- const delay = (ms) => __awaiter(void 0, void 0, void 0, function* () {
68
- yield new Promise(f => setTimeout(f, ms));
69
- });
70
- const uploadSubscribe = (url, formData, headers) => __awaiter(void 0, void 0, void 0, function* () { return (yield fetch(url, { method: 'POST', body: formData, headers: headers ? headers : {} })); });
71
- const splitFileIntoChunks = (file, chunkSize) => {
72
- const chunks = [];
73
- let start = 0;
74
- while (start < file.size) {
75
- const end = Math.min(start + chunkSize, file.size);
76
- const chunk = file.slice(start, end);
77
- chunks.push(chunk);
78
- start = end;
79
- }
80
- ;
81
- return chunks;
51
+ var uploadWithPartialFile = function (url, file, headers, chunkSize, delay_number, concurrency, onUnauthorized) {
52
+ if (headers === void 0) { headers = {}; }
53
+ if (chunkSize === void 0) { chunkSize = 26214400; }
54
+ if (delay_number === void 0) { delay_number = 50; }
55
+ if (concurrency === void 0) { concurrency = 1; }
56
+ return __awaiter(void 0, void 0, void 0, function () {
57
+ var sharedContext, id, fileName, totalSize, totalChunks, uploadChunk, queue_1, workers, e_1;
58
+ return __generator(this, function (_a) {
59
+ switch (_a.label) {
60
+ case 0:
61
+ sharedContext = {
62
+ url: url,
63
+ headers: __assign({}, headers)
64
+ };
65
+ return [4 /*yield*/, delay(delay_number)];
66
+ case 1:
67
+ _a.sent();
68
+ id = generateGuid();
69
+ fileName = file.name || 'file';
70
+ totalSize = file.size;
71
+ totalChunks = Math.ceil(totalSize / chunkSize) || 1;
72
+ uploadChunk = function (index) { return __awaiter(void 0, void 0, void 0, function () {
73
+ var start, end, chunk, retry, formData, res, refreshData, e_2;
74
+ return __generator(this, function (_a) {
75
+ switch (_a.label) {
76
+ case 0:
77
+ start = index * chunkSize;
78
+ end = Math.min(start + chunkSize, totalSize);
79
+ chunk = file.slice(start, end);
80
+ retry = 0;
81
+ _a.label = 1;
82
+ case 1:
83
+ if (!(retry < 3)) return [3 /*break*/, 9];
84
+ _a.label = 2;
85
+ case 2:
86
+ _a.trys.push([2, 6, , 8]);
87
+ formData = new FormData();
88
+ formData.append('file', chunk, "".concat(fileName, "_chunk_").concat(index));
89
+ formData.append('fileGuid', id);
90
+ formData.append('isDone', (index === totalChunks - 1).toString());
91
+ formData.append('totalSize', totalSize.toString());
92
+ formData.append('totalChunks', totalChunks.toString());
93
+ formData.append('filename', fileName);
94
+ formData.append('index', index.toString());
95
+ return [4 /*yield*/, fetch(sharedContext.url, {
96
+ method: 'POST',
97
+ body: formData,
98
+ headers: sharedContext.headers
99
+ })];
100
+ case 3:
101
+ res = _a.sent();
102
+ if (!(res.status === 401 && onUnauthorized)) return [3 /*break*/, 5];
103
+ return [4 /*yield*/, onUnauthorized(res)];
104
+ case 4:
105
+ refreshData = _a.sent();
106
+ if (refreshData) {
107
+ if (refreshData.headers) {
108
+ sharedContext.headers = __assign(__assign({}, sharedContext.headers), refreshData.headers);
109
+ }
110
+ if (refreshData.url) {
111
+ sharedContext.url = refreshData.url;
112
+ }
113
+ // Token yenilendi, bu retry hakkından düşmeden aynı parçayı tekrar dene
114
+ retry--;
115
+ return [3 /*break*/, 8];
116
+ }
117
+ _a.label = 5;
118
+ case 5:
119
+ if (res.ok)
120
+ return [2 /*return*/, true];
121
+ // Diğer hatalarda (500 vb.) retry devam etsin
122
+ if (retry === 2)
123
+ throw new Error("Server error: ".concat(res.status));
124
+ return [3 /*break*/, 8];
125
+ case 6:
126
+ e_2 = _a.sent();
127
+ if (retry === 2)
128
+ throw e_2;
129
+ // Ağ hatalarında bekleme süresini artır
130
+ return [4 /*yield*/, delay(delay_number + (retry + 1) * 1000)];
131
+ case 7:
132
+ // Ağ hatalarında bekleme süresini artır
133
+ _a.sent();
134
+ return [3 /*break*/, 8];
135
+ case 8:
136
+ retry++;
137
+ return [3 /*break*/, 1];
138
+ case 9: return [2 /*return*/, false];
139
+ }
140
+ });
141
+ }); };
142
+ _a.label = 2;
143
+ case 2:
144
+ _a.trys.push([2, 4, , 5]);
145
+ queue_1 = Array.from({ length: totalChunks }, function (_, i) { return i; });
146
+ workers = Array(Math.min(concurrency, totalChunks)).fill(null).map(function () { return __awaiter(void 0, void 0, void 0, function () {
147
+ var index, success;
148
+ return __generator(this, function (_a) {
149
+ switch (_a.label) {
150
+ case 0:
151
+ if (!(queue_1.length > 0)) return [3 /*break*/, 4];
152
+ index = queue_1.shift();
153
+ return [4 /*yield*/, uploadChunk(index)];
154
+ case 1:
155
+ success = _a.sent();
156
+ if (!success)
157
+ throw new Error("file could not be loaded");
158
+ if (!(delay_number > 0)) return [3 /*break*/, 3];
159
+ return [4 /*yield*/, delay(delay_number)];
160
+ case 2:
161
+ _a.sent();
162
+ _a.label = 3;
163
+ case 3: return [3 /*break*/, 0];
164
+ case 4: return [2 /*return*/];
165
+ }
166
+ });
167
+ }); });
168
+ return [4 /*yield*/, Promise.all(workers)];
169
+ case 3:
170
+ _a.sent();
171
+ return [2 /*return*/, { success: true, id: id, message: "file uploaded successfully" }];
172
+ case 4:
173
+ e_1 = _a.sent();
174
+ return [2 /*return*/, { success: false, id: id, message: e_1.message || "file could not be loaded" }];
175
+ case 5: return [2 /*return*/];
176
+ }
177
+ });
178
+ });
82
179
  };
83
- const generateGuid = () => {
180
+ exports.uploadWithPartialFile = uploadWithPartialFile;
181
+ var delay = function (ms) { return new Promise(function (f) { return setTimeout(f, ms); }); };
182
+ var generateGuid = function () {
84
183
  var chars = '0123456789abcdef';
85
184
  var guid = '';
86
185
  for (var i = 0; i < 40; i++) {
87
186
  var randomIndex = Math.floor(Math.random() * chars.length);
88
187
  guid += chars.charAt(randomIndex);
89
188
  }
90
- ;
91
189
  return guid;
92
190
  };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ var vitest_1 = require("vitest");
40
+ var index_1 = require("./index");
41
+ // @vitest-environment happy-dom
42
+ (0, vitest_1.describe)('uploadWithPartialFile', function () {
43
+ var mockUrl = 'https://api.example.com/upload';
44
+ var mockFile = new File(['hello world'], 'test.txt', { type: 'text/plain' });
45
+ (0, vitest_1.beforeEach)(function () {
46
+ vitest_1.vi.stubGlobal('fetch', vitest_1.vi.fn());
47
+ vitest_1.vi.useFakeTimers();
48
+ });
49
+ (0, vitest_1.it)('should upload a small file in a single chunk successfully', function () { return __awaiter(void 0, void 0, void 0, function () {
50
+ var resultPromise, result, formData;
51
+ var _a;
52
+ return __generator(this, function (_b) {
53
+ switch (_b.label) {
54
+ case 0:
55
+ fetch.mockResolvedValue({
56
+ ok: true,
57
+ status: 200
58
+ });
59
+ resultPromise = (0, index_1.uploadWithPartialFile)(mockUrl, mockFile, {}, 1024, 0);
60
+ // Fast-forward any delays
61
+ return [4 /*yield*/, vitest_1.vi.runAllTimersAsync()];
62
+ case 1:
63
+ // Fast-forward any delays
64
+ _b.sent();
65
+ return [4 /*yield*/, resultPromise];
66
+ case 2:
67
+ result = _b.sent();
68
+ (0, vitest_1.expect)(result.success).toBe(true);
69
+ (0, vitest_1.expect)(fetch).toHaveBeenCalledTimes(1);
70
+ formData = (_a = vitest_1.vi.mocked(fetch).mock.calls[0][1]) === null || _a === void 0 ? void 0 : _a.body;
71
+ (0, vitest_1.expect)(formData.get('isDone')).toBe('true');
72
+ (0, vitest_1.expect)(formData.get('totalChunks')).toBe('1');
73
+ return [2 /*return*/];
74
+ }
75
+ });
76
+ }); });
77
+ (0, vitest_1.it)('should upload a large file in multiple chunks', function () { return __awaiter(void 0, void 0, void 0, function () {
78
+ var largeFile, resultPromise, result;
79
+ return __generator(this, function (_a) {
80
+ switch (_a.label) {
81
+ case 0:
82
+ fetch.mockResolvedValue({
83
+ ok: true,
84
+ status: 200
85
+ });
86
+ largeFile = new File(['0123456789a'], 'large.txt');
87
+ resultPromise = (0, index_1.uploadWithPartialFile)(mockUrl, largeFile, {}, 5, 0, 1);
88
+ return [4 /*yield*/, vitest_1.vi.runAllTimersAsync()];
89
+ case 1:
90
+ _a.sent();
91
+ return [4 /*yield*/, resultPromise];
92
+ case 2:
93
+ result = _a.sent();
94
+ (0, vitest_1.expect)(result.success).toBe(true);
95
+ (0, vitest_1.expect)(fetch).toHaveBeenCalledTimes(3);
96
+ return [2 /*return*/];
97
+ }
98
+ });
99
+ }); });
100
+ (0, vitest_1.it)('should retry on network error and succeed', function () { return __awaiter(void 0, void 0, void 0, function () {
101
+ var resultPromise, result;
102
+ return __generator(this, function (_a) {
103
+ switch (_a.label) {
104
+ case 0:
105
+ fetch
106
+ .mockRejectedValueOnce(new Error('Network error'))
107
+ .mockResolvedValueOnce({ ok: true, status: 200 });
108
+ resultPromise = (0, index_1.uploadWithPartialFile)(mockUrl, mockFile, {}, 1024, 10);
109
+ // First attempt fails, wait for delay
110
+ return [4 /*yield*/, vitest_1.vi.runAllTimersAsync()];
111
+ case 1:
112
+ // First attempt fails, wait for delay
113
+ _a.sent();
114
+ return [4 /*yield*/, resultPromise];
115
+ case 2:
116
+ result = _a.sent();
117
+ (0, vitest_1.expect)(result.success).toBe(true);
118
+ (0, vitest_1.expect)(fetch).toHaveBeenCalledTimes(2);
119
+ return [2 /*return*/];
120
+ }
121
+ });
122
+ }); });
123
+ (0, vitest_1.it)('should refresh token on 401 and retry successfully', function () { return __awaiter(void 0, void 0, void 0, function () {
124
+ var onUnauthorized, resultPromise, result, secondCallHeaders;
125
+ var _a;
126
+ return __generator(this, function (_b) {
127
+ switch (_b.label) {
128
+ case 0:
129
+ fetch
130
+ .mockResolvedValueOnce({ ok: false, status: 401 }) // First attempt: 401
131
+ .mockResolvedValueOnce({ ok: true, status: 200 }); // Second attempt: Success
132
+ onUnauthorized = vitest_1.vi.fn().mockResolvedValue({
133
+ headers: { 'Authorization': 'Bearer new-token' }
134
+ });
135
+ resultPromise = (0, index_1.uploadWithPartialFile)(mockUrl, mockFile, { 'Authorization': 'Bearer old' }, 1024, 0, 1, onUnauthorized);
136
+ return [4 /*yield*/, vitest_1.vi.runAllTimersAsync()];
137
+ case 1:
138
+ _b.sent();
139
+ return [4 /*yield*/, resultPromise];
140
+ case 2:
141
+ result = _b.sent();
142
+ (0, vitest_1.expect)(onUnauthorized).toHaveBeenCalled();
143
+ (0, vitest_1.expect)(result.success).toBe(true);
144
+ (0, vitest_1.expect)(fetch).toHaveBeenCalledTimes(2);
145
+ secondCallHeaders = (_a = vitest_1.vi.mocked(fetch).mock.calls[1][1]) === null || _a === void 0 ? void 0 : _a.headers;
146
+ (0, vitest_1.expect)(secondCallHeaders['Authorization']).toBe('Bearer new-token');
147
+ return [2 /*return*/];
148
+ }
149
+ });
150
+ }); });
151
+ (0, vitest_1.it)('should fail after maximum retries', function () { return __awaiter(void 0, void 0, void 0, function () {
152
+ var resultPromise, result;
153
+ return __generator(this, function (_a) {
154
+ switch (_a.label) {
155
+ case 0:
156
+ fetch.mockRejectedValue(new Error('Persistent error'));
157
+ resultPromise = (0, index_1.uploadWithPartialFile)(mockUrl, mockFile, {}, 1024, 0);
158
+ return [4 /*yield*/, vitest_1.vi.runAllTimersAsync()];
159
+ case 1:
160
+ _a.sent();
161
+ return [4 /*yield*/, resultPromise];
162
+ case 2:
163
+ result = _a.sent();
164
+ (0, vitest_1.expect)(result.success).toBe(false);
165
+ (0, vitest_1.expect)(fetch).toHaveBeenCalledTimes(3); // Retry limit is 3 in code
166
+ return [2 /*return*/];
167
+ }
168
+ });
169
+ }); });
170
+ (0, vitest_1.it)('should handle concurrency correctly', function () { return __awaiter(void 0, void 0, void 0, function () {
171
+ var largeFile, resultPromise, result;
172
+ return __generator(this, function (_a) {
173
+ switch (_a.label) {
174
+ case 0:
175
+ fetch.mockResolvedValue({ ok: true, status: 200 });
176
+ largeFile = new File(['a'.repeat(100)], 'concurrency.txt');
177
+ resultPromise = (0, index_1.uploadWithPartialFile)(mockUrl, largeFile, {}, 10, 0, 5);
178
+ return [4 /*yield*/, vitest_1.vi.runAllTimersAsync()];
179
+ case 1:
180
+ _a.sent();
181
+ return [4 /*yield*/, resultPromise];
182
+ case 2:
183
+ result = _a.sent();
184
+ (0, vitest_1.expect)(result.success).toBe(true);
185
+ (0, vitest_1.expect)(fetch).toHaveBeenCalledTimes(10);
186
+ return [2 /*return*/];
187
+ }
188
+ });
189
+ }); });
190
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partial-uploader",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,7 +8,8 @@
8
8
  "/dist"
9
9
  ],
10
10
  "scripts": {
11
- "build": "tsc src/index.ts"
11
+ "build": "tsc src/index.ts",
12
+ "test": "vitest run"
12
13
  },
13
14
  "author": "Yusuf Unal <theunal9@gmail.com> (https://github.com/theunal)",
14
15
  "license": "MIT",
@@ -23,6 +24,8 @@
23
24
  "upload"
24
25
  ],
25
26
  "devDependencies": {
26
- "typescript": "^5.1.3"
27
+ "happy-dom": "^20.3.4",
28
+ "typescript": "^5.1.3",
29
+ "vitest": "^4.0.17"
27
30
  }
28
31
  }