h3 1.10.0 → 1.10.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/README.md +1 -1
- package/dist/index.cjs +36 -15
- package/dist/index.d.cts +5 -3
- package/dist/index.d.mts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.mjs +36 -15
- package/package.json +13 -12
package/README.md
CHANGED
|
@@ -222,7 +222,7 @@ H3 has a concept of composable utilities that accept `event` (from `eventHandler
|
|
|
222
222
|
- `getMethod(event, default?)`
|
|
223
223
|
- `isMethod(event, expected, allowHead?)`
|
|
224
224
|
- `assertMethod(event, expected, allowHead?)`
|
|
225
|
-
- `getRequestHeaders(event
|
|
225
|
+
- `getRequestHeaders(event)` (alias: `getHeaders`)
|
|
226
226
|
- `getRequestHeader(event, name)` (alias: `getHeader`)
|
|
227
227
|
- `getRequestURL(event)`
|
|
228
228
|
- `getRequestHost(event)`
|
package/dist/index.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const ufo = require('ufo');
|
|
4
4
|
const cookieEs = require('cookie-es');
|
|
5
|
+
const ohash = require('ohash');
|
|
5
6
|
const radix3 = require('radix3');
|
|
6
7
|
const destr = require('destr');
|
|
7
8
|
const defu = require('defu');
|
|
@@ -363,7 +364,7 @@ function getRequestIP(event, opts = {}) {
|
|
|
363
364
|
return event.context.clientAddress;
|
|
364
365
|
}
|
|
365
366
|
if (opts.xForwardedFor) {
|
|
366
|
-
const xForwardedFor = getRequestHeader(event, "x-forwarded-for")?.split(",")?.
|
|
367
|
+
const xForwardedFor = getRequestHeader(event, "x-forwarded-for")?.split(",").shift()?.trim();
|
|
367
368
|
if (xForwardedFor) {
|
|
368
369
|
return xForwardedFor;
|
|
369
370
|
}
|
|
@@ -482,7 +483,23 @@ function getRequestWebStream(event) {
|
|
|
482
483
|
if (!PayloadMethods$1.includes(event.method)) {
|
|
483
484
|
return;
|
|
484
485
|
}
|
|
485
|
-
|
|
486
|
+
const bodyStream = event.web?.request?.body || event._requestBody;
|
|
487
|
+
if (bodyStream) {
|
|
488
|
+
return bodyStream;
|
|
489
|
+
}
|
|
490
|
+
const _hasRawBody = RawBodySymbol in event.node.req || "rawBody" in event.node.req || "body" in event.node.req || "__unenv__" in event.node.req;
|
|
491
|
+
if (_hasRawBody) {
|
|
492
|
+
return new ReadableStream({
|
|
493
|
+
async start(controller) {
|
|
494
|
+
const _rawBody = await readRawBody(event, false);
|
|
495
|
+
if (_rawBody) {
|
|
496
|
+
controller.enqueue(_rawBody);
|
|
497
|
+
}
|
|
498
|
+
controller.close();
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
return new ReadableStream({
|
|
486
503
|
start: (controller) => {
|
|
487
504
|
event.node.req.on("data", (chunk) => {
|
|
488
505
|
controller.enqueue(chunk);
|
|
@@ -587,16 +604,15 @@ function getCookie(event, name) {
|
|
|
587
604
|
return parseCookies(event)[name];
|
|
588
605
|
}
|
|
589
606
|
function setCookie(event, name, value, serializeOptions) {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
...serializeOptions
|
|
593
|
-
});
|
|
607
|
+
serializeOptions = { path: "/", ...serializeOptions };
|
|
608
|
+
const cookieStr = cookieEs.serialize(name, value, serializeOptions);
|
|
594
609
|
let setCookies = event.node.res.getHeader("set-cookie");
|
|
595
610
|
if (!Array.isArray(setCookies)) {
|
|
596
611
|
setCookies = [setCookies];
|
|
597
612
|
}
|
|
613
|
+
const _optionsHash = ohash.objectHash(serializeOptions);
|
|
598
614
|
setCookies = setCookies.filter((cookieValue) => {
|
|
599
|
-
return cookieValue &&
|
|
615
|
+
return cookieValue && _optionsHash !== ohash.objectHash(cookieEs.parse(cookieValue));
|
|
600
616
|
});
|
|
601
617
|
event.node.res.setHeader("set-cookie", [...setCookies, cookieStr]);
|
|
602
618
|
}
|
|
@@ -708,7 +724,7 @@ function getResponseStatusText(event) {
|
|
|
708
724
|
return event.node.res.statusMessage;
|
|
709
725
|
}
|
|
710
726
|
function defaultContentType(event, type) {
|
|
711
|
-
if (type && !event.node.res.getHeader("content-type")) {
|
|
727
|
+
if (type && event.node.res.statusCode !== 304 && !event.node.res.getHeader("content-type")) {
|
|
712
728
|
event.node.res.setHeader("content-type", type);
|
|
713
729
|
}
|
|
714
730
|
}
|
|
@@ -1215,6 +1231,7 @@ function mergeHeaders(defaults, ...inputs) {
|
|
|
1215
1231
|
return merged;
|
|
1216
1232
|
}
|
|
1217
1233
|
|
|
1234
|
+
const getSessionPromise = Symbol("getSession");
|
|
1218
1235
|
const DEFAULT_NAME = "h3";
|
|
1219
1236
|
const DEFAULT_COOKIE = {
|
|
1220
1237
|
path: "/",
|
|
@@ -1247,8 +1264,9 @@ async function getSession(event, config) {
|
|
|
1247
1264
|
if (!event.context.sessions) {
|
|
1248
1265
|
event.context.sessions = /* @__PURE__ */ Object.create(null);
|
|
1249
1266
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1267
|
+
const existingSession = event.context.sessions[sessionName];
|
|
1268
|
+
if (existingSession) {
|
|
1269
|
+
return existingSession[getSessionPromise] || existingSession;
|
|
1252
1270
|
}
|
|
1253
1271
|
const session = {
|
|
1254
1272
|
id: "",
|
|
@@ -1268,11 +1286,14 @@ async function getSession(event, config) {
|
|
|
1268
1286
|
sealedSession = getCookie(event, sessionName);
|
|
1269
1287
|
}
|
|
1270
1288
|
if (sealedSession) {
|
|
1271
|
-
const
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1289
|
+
const promise = unsealSession(event, config, sealedSession).catch(() => {
|
|
1290
|
+
}).then((unsealed) => {
|
|
1291
|
+
Object.assign(session, unsealed);
|
|
1292
|
+
delete event.context.sessions[sessionName][getSessionPromise];
|
|
1293
|
+
return session;
|
|
1294
|
+
});
|
|
1295
|
+
event.context.sessions[sessionName][getSessionPromise] = promise;
|
|
1296
|
+
await promise;
|
|
1276
1297
|
}
|
|
1277
1298
|
if (!session.id) {
|
|
1278
1299
|
session.id = config.generateId?.() ?? (config.crypto || crypto__default).randomUUID();
|
package/dist/index.d.cts
CHANGED
|
@@ -107,10 +107,12 @@ declare const H3Response: {
|
|
|
107
107
|
|
|
108
108
|
type SessionDataT = Record<string, any>;
|
|
109
109
|
type SessionData<T extends SessionDataT = SessionDataT> = T;
|
|
110
|
+
declare const getSessionPromise: unique symbol;
|
|
110
111
|
interface Session<T extends SessionDataT = SessionDataT> {
|
|
111
112
|
id: string;
|
|
112
113
|
createdAt: number;
|
|
113
114
|
data: SessionData<T>;
|
|
115
|
+
[getSessionPromise]?: Promise<Session<T>>;
|
|
114
116
|
}
|
|
115
117
|
interface SessionConfig {
|
|
116
118
|
/** Private key used to encrypt session tokens */
|
|
@@ -580,9 +582,9 @@ declare function getResponseStatus(event: H3Event): number;
|
|
|
580
582
|
declare function getResponseStatusText(event: H3Event): string;
|
|
581
583
|
declare function defaultContentType(event: H3Event, type?: string): void;
|
|
582
584
|
declare function sendRedirect(event: H3Event, location: string, code?: number): Promise<void>;
|
|
583
|
-
declare function getResponseHeaders(event: H3Event): ReturnType<H3Event["res"]["getHeaders"]>;
|
|
584
|
-
declare function getResponseHeader(event: H3Event, name: HTTPHeaderName): ReturnType<H3Event["res"]["getHeader"]>;
|
|
585
|
-
declare function setResponseHeaders(event: H3Event, headers: Record<HTTPHeaderName, Parameters<OutgoingMessage["setHeader"]>[1]
|
|
585
|
+
declare function getResponseHeaders(event: H3Event): ReturnType<H3Event["node"]["res"]["getHeaders"]>;
|
|
586
|
+
declare function getResponseHeader(event: H3Event, name: HTTPHeaderName): ReturnType<H3Event["node"]["res"]["getHeader"]>;
|
|
587
|
+
declare function setResponseHeaders(event: H3Event, headers: Partial<Record<HTTPHeaderName, Parameters<OutgoingMessage["setHeader"]>[1]>>): void;
|
|
586
588
|
declare const setHeaders: typeof setResponseHeaders;
|
|
587
589
|
declare function setResponseHeader(event: H3Event, name: HTTPHeaderName, value: Parameters<OutgoingMessage["setHeader"]>[1]): void;
|
|
588
590
|
declare const setHeader: typeof setResponseHeader;
|
package/dist/index.d.mts
CHANGED
|
@@ -107,10 +107,12 @@ declare const H3Response: {
|
|
|
107
107
|
|
|
108
108
|
type SessionDataT = Record<string, any>;
|
|
109
109
|
type SessionData<T extends SessionDataT = SessionDataT> = T;
|
|
110
|
+
declare const getSessionPromise: unique symbol;
|
|
110
111
|
interface Session<T extends SessionDataT = SessionDataT> {
|
|
111
112
|
id: string;
|
|
112
113
|
createdAt: number;
|
|
113
114
|
data: SessionData<T>;
|
|
115
|
+
[getSessionPromise]?: Promise<Session<T>>;
|
|
114
116
|
}
|
|
115
117
|
interface SessionConfig {
|
|
116
118
|
/** Private key used to encrypt session tokens */
|
|
@@ -580,9 +582,9 @@ declare function getResponseStatus(event: H3Event): number;
|
|
|
580
582
|
declare function getResponseStatusText(event: H3Event): string;
|
|
581
583
|
declare function defaultContentType(event: H3Event, type?: string): void;
|
|
582
584
|
declare function sendRedirect(event: H3Event, location: string, code?: number): Promise<void>;
|
|
583
|
-
declare function getResponseHeaders(event: H3Event): ReturnType<H3Event["res"]["getHeaders"]>;
|
|
584
|
-
declare function getResponseHeader(event: H3Event, name: HTTPHeaderName): ReturnType<H3Event["res"]["getHeader"]>;
|
|
585
|
-
declare function setResponseHeaders(event: H3Event, headers: Record<HTTPHeaderName, Parameters<OutgoingMessage["setHeader"]>[1]
|
|
585
|
+
declare function getResponseHeaders(event: H3Event): ReturnType<H3Event["node"]["res"]["getHeaders"]>;
|
|
586
|
+
declare function getResponseHeader(event: H3Event, name: HTTPHeaderName): ReturnType<H3Event["node"]["res"]["getHeader"]>;
|
|
587
|
+
declare function setResponseHeaders(event: H3Event, headers: Partial<Record<HTTPHeaderName, Parameters<OutgoingMessage["setHeader"]>[1]>>): void;
|
|
586
588
|
declare const setHeaders: typeof setResponseHeaders;
|
|
587
589
|
declare function setResponseHeader(event: H3Event, name: HTTPHeaderName, value: Parameters<OutgoingMessage["setHeader"]>[1]): void;
|
|
588
590
|
declare const setHeader: typeof setResponseHeader;
|
package/dist/index.d.ts
CHANGED
|
@@ -107,10 +107,12 @@ declare const H3Response: {
|
|
|
107
107
|
|
|
108
108
|
type SessionDataT = Record<string, any>;
|
|
109
109
|
type SessionData<T extends SessionDataT = SessionDataT> = T;
|
|
110
|
+
declare const getSessionPromise: unique symbol;
|
|
110
111
|
interface Session<T extends SessionDataT = SessionDataT> {
|
|
111
112
|
id: string;
|
|
112
113
|
createdAt: number;
|
|
113
114
|
data: SessionData<T>;
|
|
115
|
+
[getSessionPromise]?: Promise<Session<T>>;
|
|
114
116
|
}
|
|
115
117
|
interface SessionConfig {
|
|
116
118
|
/** Private key used to encrypt session tokens */
|
|
@@ -580,9 +582,9 @@ declare function getResponseStatus(event: H3Event): number;
|
|
|
580
582
|
declare function getResponseStatusText(event: H3Event): string;
|
|
581
583
|
declare function defaultContentType(event: H3Event, type?: string): void;
|
|
582
584
|
declare function sendRedirect(event: H3Event, location: string, code?: number): Promise<void>;
|
|
583
|
-
declare function getResponseHeaders(event: H3Event): ReturnType<H3Event["res"]["getHeaders"]>;
|
|
584
|
-
declare function getResponseHeader(event: H3Event, name: HTTPHeaderName): ReturnType<H3Event["res"]["getHeader"]>;
|
|
585
|
-
declare function setResponseHeaders(event: H3Event, headers: Record<HTTPHeaderName, Parameters<OutgoingMessage["setHeader"]>[1]
|
|
585
|
+
declare function getResponseHeaders(event: H3Event): ReturnType<H3Event["node"]["res"]["getHeaders"]>;
|
|
586
|
+
declare function getResponseHeader(event: H3Event, name: HTTPHeaderName): ReturnType<H3Event["node"]["res"]["getHeader"]>;
|
|
587
|
+
declare function setResponseHeaders(event: H3Event, headers: Partial<Record<HTTPHeaderName, Parameters<OutgoingMessage["setHeader"]>[1]>>): void;
|
|
586
588
|
declare const setHeaders: typeof setResponseHeaders;
|
|
587
589
|
declare function setResponseHeader(event: H3Event, name: HTTPHeaderName, value: Parameters<OutgoingMessage["setHeader"]>[1]): void;
|
|
588
590
|
declare const setHeader: typeof setResponseHeader;
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { withoutTrailingSlash, withoutBase, getQuery as getQuery$1, decode, decodePath, withLeadingSlash, parseURL } from 'ufo';
|
|
2
2
|
import { parse as parse$1, serialize } from 'cookie-es';
|
|
3
|
+
import { objectHash } from 'ohash';
|
|
3
4
|
import { createRouter as createRouter$1, toRouteMatcher } from 'radix3';
|
|
4
5
|
import destr from 'destr';
|
|
5
6
|
import { defu } from 'defu';
|
|
@@ -356,7 +357,7 @@ function getRequestIP(event, opts = {}) {
|
|
|
356
357
|
return event.context.clientAddress;
|
|
357
358
|
}
|
|
358
359
|
if (opts.xForwardedFor) {
|
|
359
|
-
const xForwardedFor = getRequestHeader(event, "x-forwarded-for")?.split(",")?.
|
|
360
|
+
const xForwardedFor = getRequestHeader(event, "x-forwarded-for")?.split(",").shift()?.trim();
|
|
360
361
|
if (xForwardedFor) {
|
|
361
362
|
return xForwardedFor;
|
|
362
363
|
}
|
|
@@ -475,7 +476,23 @@ function getRequestWebStream(event) {
|
|
|
475
476
|
if (!PayloadMethods$1.includes(event.method)) {
|
|
476
477
|
return;
|
|
477
478
|
}
|
|
478
|
-
|
|
479
|
+
const bodyStream = event.web?.request?.body || event._requestBody;
|
|
480
|
+
if (bodyStream) {
|
|
481
|
+
return bodyStream;
|
|
482
|
+
}
|
|
483
|
+
const _hasRawBody = RawBodySymbol in event.node.req || "rawBody" in event.node.req || "body" in event.node.req || "__unenv__" in event.node.req;
|
|
484
|
+
if (_hasRawBody) {
|
|
485
|
+
return new ReadableStream({
|
|
486
|
+
async start(controller) {
|
|
487
|
+
const _rawBody = await readRawBody(event, false);
|
|
488
|
+
if (_rawBody) {
|
|
489
|
+
controller.enqueue(_rawBody);
|
|
490
|
+
}
|
|
491
|
+
controller.close();
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
return new ReadableStream({
|
|
479
496
|
start: (controller) => {
|
|
480
497
|
event.node.req.on("data", (chunk) => {
|
|
481
498
|
controller.enqueue(chunk);
|
|
@@ -580,16 +597,15 @@ function getCookie(event, name) {
|
|
|
580
597
|
return parseCookies(event)[name];
|
|
581
598
|
}
|
|
582
599
|
function setCookie(event, name, value, serializeOptions) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
...serializeOptions
|
|
586
|
-
});
|
|
600
|
+
serializeOptions = { path: "/", ...serializeOptions };
|
|
601
|
+
const cookieStr = serialize(name, value, serializeOptions);
|
|
587
602
|
let setCookies = event.node.res.getHeader("set-cookie");
|
|
588
603
|
if (!Array.isArray(setCookies)) {
|
|
589
604
|
setCookies = [setCookies];
|
|
590
605
|
}
|
|
606
|
+
const _optionsHash = objectHash(serializeOptions);
|
|
591
607
|
setCookies = setCookies.filter((cookieValue) => {
|
|
592
|
-
return cookieValue &&
|
|
608
|
+
return cookieValue && _optionsHash !== objectHash(parse$1(cookieValue));
|
|
593
609
|
});
|
|
594
610
|
event.node.res.setHeader("set-cookie", [...setCookies, cookieStr]);
|
|
595
611
|
}
|
|
@@ -701,7 +717,7 @@ function getResponseStatusText(event) {
|
|
|
701
717
|
return event.node.res.statusMessage;
|
|
702
718
|
}
|
|
703
719
|
function defaultContentType(event, type) {
|
|
704
|
-
if (type && !event.node.res.getHeader("content-type")) {
|
|
720
|
+
if (type && event.node.res.statusCode !== 304 && !event.node.res.getHeader("content-type")) {
|
|
705
721
|
event.node.res.setHeader("content-type", type);
|
|
706
722
|
}
|
|
707
723
|
}
|
|
@@ -1208,6 +1224,7 @@ function mergeHeaders(defaults, ...inputs) {
|
|
|
1208
1224
|
return merged;
|
|
1209
1225
|
}
|
|
1210
1226
|
|
|
1227
|
+
const getSessionPromise = Symbol("getSession");
|
|
1211
1228
|
const DEFAULT_NAME = "h3";
|
|
1212
1229
|
const DEFAULT_COOKIE = {
|
|
1213
1230
|
path: "/",
|
|
@@ -1240,8 +1257,9 @@ async function getSession(event, config) {
|
|
|
1240
1257
|
if (!event.context.sessions) {
|
|
1241
1258
|
event.context.sessions = /* @__PURE__ */ Object.create(null);
|
|
1242
1259
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1260
|
+
const existingSession = event.context.sessions[sessionName];
|
|
1261
|
+
if (existingSession) {
|
|
1262
|
+
return existingSession[getSessionPromise] || existingSession;
|
|
1245
1263
|
}
|
|
1246
1264
|
const session = {
|
|
1247
1265
|
id: "",
|
|
@@ -1261,11 +1279,14 @@ async function getSession(event, config) {
|
|
|
1261
1279
|
sealedSession = getCookie(event, sessionName);
|
|
1262
1280
|
}
|
|
1263
1281
|
if (sealedSession) {
|
|
1264
|
-
const
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1282
|
+
const promise = unsealSession(event, config, sealedSession).catch(() => {
|
|
1283
|
+
}).then((unsealed) => {
|
|
1284
|
+
Object.assign(session, unsealed);
|
|
1285
|
+
delete event.context.sessions[sessionName][getSessionPromise];
|
|
1286
|
+
return session;
|
|
1287
|
+
});
|
|
1288
|
+
event.context.sessions[sessionName][getSessionPromise] = promise;
|
|
1289
|
+
await promise;
|
|
1269
1290
|
}
|
|
1270
1291
|
if (!session.id) {
|
|
1271
1292
|
session.id = config.generateId?.() ?? (config.crypto || crypto).randomUUID();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "h3",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.1",
|
|
4
4
|
"description": "Minimal H(TTP) framework built for high performance and portability.",
|
|
5
5
|
"repository": "unjs/h3",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,40 +21,41 @@
|
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"cookie-es": "^1.0.0",
|
|
24
|
-
"defu": "^6.1.
|
|
24
|
+
"defu": "^6.1.4",
|
|
25
25
|
"destr": "^2.0.2",
|
|
26
26
|
"iron-webcrypto": "^1.0.0",
|
|
27
|
+
"ohash": "^1.1.3",
|
|
27
28
|
"radix3": "^1.1.0",
|
|
28
29
|
"ufo": "^1.3.2",
|
|
29
30
|
"uncrypto": "^0.1.3",
|
|
30
|
-
"unenv": "^1.
|
|
31
|
+
"unenv": "^1.9.0"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"0x": "^5.7.0",
|
|
34
35
|
"@types/express": "^4.17.21",
|
|
35
|
-
"@types/node": "^20.
|
|
36
|
+
"@types/node": "^20.11.6",
|
|
36
37
|
"@types/supertest": "^6.0.2",
|
|
37
|
-
"@vitest/coverage-v8": "^1.
|
|
38
|
+
"@vitest/coverage-v8": "^1.2.1",
|
|
38
39
|
"autocannon": "^7.14.0",
|
|
39
40
|
"changelogen": "^0.5.5",
|
|
40
41
|
"connect": "^3.7.0",
|
|
41
|
-
"eslint": "^8.
|
|
42
|
+
"eslint": "^8.56.0",
|
|
42
43
|
"eslint-config-unjs": "^0.2.1",
|
|
43
44
|
"express": "^4.18.2",
|
|
44
45
|
"get-port": "^7.0.0",
|
|
45
46
|
"jiti": "^1.21.0",
|
|
46
|
-
"listhen": "^1.5.
|
|
47
|
-
"node-fetch-native": "^1.
|
|
48
|
-
"prettier": "^3.
|
|
47
|
+
"listhen": "^1.5.6",
|
|
48
|
+
"node-fetch-native": "^1.6.1",
|
|
49
|
+
"prettier": "^3.2.4",
|
|
49
50
|
"react": "^18.2.0",
|
|
50
51
|
"react-dom": "^18.2.0",
|
|
51
|
-
"supertest": "^6.3.
|
|
52
|
+
"supertest": "^6.3.4",
|
|
52
53
|
"typescript": "^5.3.3",
|
|
53
54
|
"unbuild": "^2.0.0",
|
|
54
|
-
"vitest": "^1.
|
|
55
|
+
"vitest": "^1.2.1",
|
|
55
56
|
"zod": "^3.22.4"
|
|
56
57
|
},
|
|
57
|
-
"packageManager": "pnpm@8.
|
|
58
|
+
"packageManager": "pnpm@8.14.3",
|
|
58
59
|
"scripts": {
|
|
59
60
|
"build": "unbuild",
|
|
60
61
|
"dev": "vitest",
|