@rexeus/typeweaver-hono 0.7.0 โ 0.9.0
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 +38 -23
- package/dist/index.cjs +39 -73
- package/dist/index.mjs +34 -62
- package/dist/index.mjs.map +1 -1
- package/dist/lib/TypeweaverHono.ts +167 -58
- package/dist/templates/HonoRouter.ejs +2 -0
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ npm install @rexeus/typeweaver-core
|
|
|
29
29
|
## ๐ก How to use
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
npx typeweaver generate --input ./api/
|
|
32
|
+
npx typeweaver generate --input ./api/spec/index.ts --output ./api/generated --plugins hono
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
More on the CLI in
|
|
@@ -51,28 +51,28 @@ Implement your handlers and mount the generated router in a Hono app.
|
|
|
51
51
|
// api/user-handlers.ts
|
|
52
52
|
import type { Context } from "hono";
|
|
53
53
|
import { HttpStatusCode } from "@rexeus/typeweaver-core";
|
|
54
|
-
import type { IGetUserRequest, GetUserResponse
|
|
55
|
-
import {
|
|
54
|
+
import type { HonoUserApiHandler, IGetUserRequest, GetUserResponse } from "./generated";
|
|
55
|
+
import { createUserNotFoundErrorResponse, createGetUserSuccessResponse } from "./generated";
|
|
56
56
|
|
|
57
57
|
export class UserHandlers implements HonoUserApiHandler {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// Will be properly handled by the generated router and returned as a 404 response
|
|
63
|
-
return new UserNotFoundErrorResponse({
|
|
64
|
-
statusCode: HttpStatusCode.NotFound,
|
|
65
|
-
header: { "Content-Type": "application/json" },
|
|
66
|
-
body: { message: "User not found" },
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return new GetUserSuccessResponse({
|
|
71
|
-
statusCode: HttpStatusCode.OK,
|
|
58
|
+
async handleGetUserRequest(request: IGetUserRequest, context: Context): Promise<GetUserResponse> {
|
|
59
|
+
const user = await db.findUser(request.param.userId);
|
|
60
|
+
if (!user) {
|
|
61
|
+
return createUserNotFoundErrorResponse({
|
|
72
62
|
header: { "Content-Type": "application/json" },
|
|
73
|
-
body: {
|
|
63
|
+
body: { message: "User not found" },
|
|
74
64
|
});
|
|
75
|
-
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return createGetUserSuccessResponse({
|
|
68
|
+
header: { "Content-Type": "application/json" },
|
|
69
|
+
body: {
|
|
70
|
+
id: request.param.userId,
|
|
71
|
+
name: "Jane",
|
|
72
|
+
email: "jane@example.com",
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
76
|
// Implement other operation handlers: handleCreateUserRequest, ...
|
|
77
77
|
}
|
|
78
78
|
```
|
|
@@ -91,8 +91,10 @@ const userHandlers = new UserHandlers();
|
|
|
91
91
|
const userRouter = new UserHono({
|
|
92
92
|
requestHandlers: userHandlers,
|
|
93
93
|
validateRequests: true, // default, validates requests
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
validateResponses: true, // default, validates responses and strips extra fields
|
|
95
|
+
handleRequestValidationErrors: true, // default: returns 400 with issues
|
|
96
|
+
handleResponseValidationErrors: true, // default: returns 500
|
|
97
|
+
handleHttpResponseErrors: true, // default: returns thrown typed HTTP responses as-is
|
|
96
98
|
handleUnknownErrors: true, // default: returns 500
|
|
97
99
|
});
|
|
98
100
|
|
|
@@ -110,14 +112,27 @@ serve({ fetch: app.fetch, port: 3000 }, () => {
|
|
|
110
112
|
|
|
111
113
|
- `requestHandlers`: object implementing the generated `Hono<ResourceName>ApiHandler` type
|
|
112
114
|
- `validateRequests` (default: `true`): enable/disable request validation
|
|
113
|
-
- `
|
|
115
|
+
- `validateResponses` (default: `true`): enable/disable response validation. When enabled, responses
|
|
116
|
+
are validated against the operation's schema and extra body fields are stripped before sending.
|
|
117
|
+
- `handleRequestValidationErrors`: `true` | `false` |
|
|
118
|
+
`(err, c) => IHttpResponse | Promise<IHttpResponse>`
|
|
114
119
|
- If `true` (default), returns `400 Bad Request` with validation issues in the body
|
|
115
120
|
- If `false`, disables this handler (errors fall through to the unknown error handler)
|
|
116
121
|
- If function, calls the function with the error and context, expects an `IHttpResponse` to
|
|
117
122
|
return, so you can customize the response in the way you want
|
|
123
|
+
- `handleResponseValidationErrors`: `true` | `false` |
|
|
124
|
+
`(err, response, c) => IHttpResponse | Promise<IHttpResponse>`
|
|
125
|
+
- If `true` (default), returns `500 Internal Server Error`
|
|
126
|
+
- If `false`, disables response validation error handling โ the invalid response is returned
|
|
127
|
+
as-is. Validation still runs (and strips extra fields on valid responses), but invalid responses
|
|
128
|
+
pass through unchanged. Useful when you want field stripping without blocking invalid responses.
|
|
129
|
+
- If function, calls the function with the `ResponseValidationError`, the original (invalid)
|
|
130
|
+
response, and the Hono context. The function should return an `IHttpResponse`. If the custom
|
|
131
|
+
handler throws, the original response is returned as a fallback.
|
|
118
132
|
- `handleHttpResponseErrors`: `true` | `false` |
|
|
119
133
|
`(err, c) => IHttpResponse | Promise<IHttpResponse>`
|
|
120
|
-
- If `true` (default), returns thrown `
|
|
134
|
+
- If `true` (default), returns thrown typed HTTP responses (`ITypedHttpResponse`) as-is, they will
|
|
135
|
+
be sent as the response
|
|
121
136
|
- If `false`, disables this handler (errors fall through to the unknown error handler)
|
|
122
137
|
- If function, calls the function with the error and context, expects an `IHttpResponse` to
|
|
123
138
|
return, so you can customize the response in the way you want
|
package/dist/index.cjs
CHANGED
|
@@ -6,16 +6,12 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
8
|
var __copyProps = (to, from, except, desc) => {
|
|
9
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
}
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
19
15
|
}
|
|
20
16
|
return to;
|
|
21
17
|
};
|
|
@@ -23,7 +19,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
19
|
value: mod,
|
|
24
20
|
enumerable: true
|
|
25
21
|
}) : target, mod));
|
|
26
|
-
|
|
27
22
|
//#endregion
|
|
28
23
|
let node_path = require("node:path");
|
|
29
24
|
node_path = __toESM(node_path);
|
|
@@ -32,65 +27,37 @@ let _rexeus_typeweaver_gen = require("@rexeus/typeweaver-gen");
|
|
|
32
27
|
let _rexeus_typeweaver_core = require("@rexeus/typeweaver-core");
|
|
33
28
|
let case$1 = require("case");
|
|
34
29
|
case$1 = __toESM(case$1);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
static compareRoutes(a, b) {
|
|
69
|
-
const aSegments = a.path.split("/").filter((s) => s);
|
|
70
|
-
const bSegments = b.path.split("/").filter((s) => s);
|
|
71
|
-
if (aSegments.length !== bSegments.length) return aSegments.length - bSegments.length;
|
|
72
|
-
for (let i = 0; i < aSegments.length; i++) {
|
|
73
|
-
const aSegment = aSegments[i];
|
|
74
|
-
const bSegment = bSegments[i];
|
|
75
|
-
const aIsParam = aSegment.startsWith(":");
|
|
76
|
-
if (aIsParam !== bSegment.startsWith(":")) return aIsParam ? 1 : -1;
|
|
77
|
-
if (aSegment !== bSegment) return aSegment.localeCompare(bSegment);
|
|
78
|
-
}
|
|
79
|
-
return this.getMethodPriority(a.method) - this.getMethodPriority(b.method);
|
|
80
|
-
}
|
|
81
|
-
static getMethodPriority(method) {
|
|
82
|
-
return {
|
|
83
|
-
GET: 1,
|
|
84
|
-
POST: 2,
|
|
85
|
-
PUT: 3,
|
|
86
|
-
PATCH: 4,
|
|
87
|
-
DELETE: 5,
|
|
88
|
-
OPTIONS: 6,
|
|
89
|
-
HEAD: 7
|
|
90
|
-
}[method] ?? 999;
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
|
|
30
|
+
//#region src/honoRouterGenerator.ts
|
|
31
|
+
function generate(context) {
|
|
32
|
+
const moduleDir = node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
33
|
+
const templateFile = node_path.default.join(moduleDir, "templates", "HonoRouter.ejs");
|
|
34
|
+
for (const resource of context.normalizedSpec.resources) writeHonoRouter(resource, templateFile, context);
|
|
35
|
+
}
|
|
36
|
+
function writeHonoRouter(resource, templateFile, context) {
|
|
37
|
+
const pascalCaseEntityName = case$1.default.pascal(resource.name);
|
|
38
|
+
const outputDir = context.getResourceOutputDir(resource.name);
|
|
39
|
+
const outputPath = node_path.default.join(outputDir, `${pascalCaseEntityName}Hono.ts`);
|
|
40
|
+
const operations = resource.operations.filter((operation) => operation.method !== _rexeus_typeweaver_core.HttpMethod.HEAD).map((operation) => createOperationData(operation)).sort((a, b) => (0, _rexeus_typeweaver_gen.compareRoutes)(a, b));
|
|
41
|
+
const content = context.renderTemplate(templateFile, {
|
|
42
|
+
coreDir: node_path.default.relative(outputDir, context.outputDir),
|
|
43
|
+
entityName: resource.name,
|
|
44
|
+
pascalCaseEntityName,
|
|
45
|
+
operations
|
|
46
|
+
});
|
|
47
|
+
const relativePath = node_path.default.relative(context.outputDir, outputPath);
|
|
48
|
+
context.writeFile(relativePath, content);
|
|
49
|
+
}
|
|
50
|
+
function createOperationData(operation) {
|
|
51
|
+
const operationId = operation.operationId;
|
|
52
|
+
const className = case$1.default.pascal(operationId);
|
|
53
|
+
return {
|
|
54
|
+
operationId,
|
|
55
|
+
className,
|
|
56
|
+
handlerName: `handle${className}Request`,
|
|
57
|
+
method: operation.method,
|
|
58
|
+
path: operation.path
|
|
59
|
+
};
|
|
60
|
+
}
|
|
94
61
|
//#endregion
|
|
95
62
|
//#region src/index.ts
|
|
96
63
|
const moduleDir = node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
@@ -99,9 +66,8 @@ var HonoPlugin = class extends _rexeus_typeweaver_gen.BasePlugin {
|
|
|
99
66
|
generate(context) {
|
|
100
67
|
const libSourceDir = node_path.default.join(moduleDir, "lib");
|
|
101
68
|
this.copyLibFiles(context, libSourceDir, this.name);
|
|
102
|
-
|
|
69
|
+
generate(context);
|
|
103
70
|
}
|
|
104
71
|
};
|
|
105
|
-
|
|
106
72
|
//#endregion
|
|
107
|
-
module.exports = HonoPlugin;
|
|
73
|
+
module.exports = HonoPlugin;
|
package/dist/index.mjs
CHANGED
|
@@ -1,67 +1,39 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { BasePlugin } from "@rexeus/typeweaver-gen";
|
|
3
|
+
import { BasePlugin, compareRoutes } from "@rexeus/typeweaver-gen";
|
|
4
4
|
import { HttpMethod } from "@rexeus/typeweaver-core";
|
|
5
5
|
import Case from "case";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
static compareRoutes(a, b) {
|
|
40
|
-
const aSegments = a.path.split("/").filter((s) => s);
|
|
41
|
-
const bSegments = b.path.split("/").filter((s) => s);
|
|
42
|
-
if (aSegments.length !== bSegments.length) return aSegments.length - bSegments.length;
|
|
43
|
-
for (let i = 0; i < aSegments.length; i++) {
|
|
44
|
-
const aSegment = aSegments[i];
|
|
45
|
-
const bSegment = bSegments[i];
|
|
46
|
-
const aIsParam = aSegment.startsWith(":");
|
|
47
|
-
if (aIsParam !== bSegment.startsWith(":")) return aIsParam ? 1 : -1;
|
|
48
|
-
if (aSegment !== bSegment) return aSegment.localeCompare(bSegment);
|
|
49
|
-
}
|
|
50
|
-
return this.getMethodPriority(a.method) - this.getMethodPriority(b.method);
|
|
51
|
-
}
|
|
52
|
-
static getMethodPriority(method) {
|
|
53
|
-
return {
|
|
54
|
-
GET: 1,
|
|
55
|
-
POST: 2,
|
|
56
|
-
PUT: 3,
|
|
57
|
-
PATCH: 4,
|
|
58
|
-
DELETE: 5,
|
|
59
|
-
OPTIONS: 6,
|
|
60
|
-
HEAD: 7
|
|
61
|
-
}[method] ?? 999;
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
|
|
6
|
+
//#region src/honoRouterGenerator.ts
|
|
7
|
+
function generate(context) {
|
|
8
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const templateFile = path.join(moduleDir, "templates", "HonoRouter.ejs");
|
|
10
|
+
for (const resource of context.normalizedSpec.resources) writeHonoRouter(resource, templateFile, context);
|
|
11
|
+
}
|
|
12
|
+
function writeHonoRouter(resource, templateFile, context) {
|
|
13
|
+
const pascalCaseEntityName = Case.pascal(resource.name);
|
|
14
|
+
const outputDir = context.getResourceOutputDir(resource.name);
|
|
15
|
+
const outputPath = path.join(outputDir, `${pascalCaseEntityName}Hono.ts`);
|
|
16
|
+
const operations = resource.operations.filter((operation) => operation.method !== HttpMethod.HEAD).map((operation) => createOperationData(operation)).sort((a, b) => compareRoutes(a, b));
|
|
17
|
+
const content = context.renderTemplate(templateFile, {
|
|
18
|
+
coreDir: path.relative(outputDir, context.outputDir),
|
|
19
|
+
entityName: resource.name,
|
|
20
|
+
pascalCaseEntityName,
|
|
21
|
+
operations
|
|
22
|
+
});
|
|
23
|
+
const relativePath = path.relative(context.outputDir, outputPath);
|
|
24
|
+
context.writeFile(relativePath, content);
|
|
25
|
+
}
|
|
26
|
+
function createOperationData(operation) {
|
|
27
|
+
const operationId = operation.operationId;
|
|
28
|
+
const className = Case.pascal(operationId);
|
|
29
|
+
return {
|
|
30
|
+
operationId,
|
|
31
|
+
className,
|
|
32
|
+
handlerName: `handle${className}Request`,
|
|
33
|
+
method: operation.method,
|
|
34
|
+
path: operation.path
|
|
35
|
+
};
|
|
36
|
+
}
|
|
65
37
|
//#endregion
|
|
66
38
|
//#region src/index.ts
|
|
67
39
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -70,10 +42,10 @@ var HonoPlugin = class extends BasePlugin {
|
|
|
70
42
|
generate(context) {
|
|
71
43
|
const libSourceDir = path.join(moduleDir, "lib");
|
|
72
44
|
this.copyLibFiles(context, libSourceDir, this.name);
|
|
73
|
-
|
|
45
|
+
generate(context);
|
|
74
46
|
}
|
|
75
47
|
};
|
|
76
|
-
|
|
77
48
|
//#endregion
|
|
78
49
|
export { HonoPlugin as default };
|
|
50
|
+
|
|
79
51
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/honoRouterGenerator.ts","../src/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { HttpMethod } from \"@rexeus/typeweaver-core\";\nimport { compareRoutes } from \"@rexeus/typeweaver-gen\";\nimport type {\n GeneratorContext,\n NormalizedOperation,\n NormalizedResource,\n} from \"@rexeus/typeweaver-gen\";\nimport Case from \"case\";\n\nexport function generate(context: GeneratorContext): void {\n const moduleDir = path.dirname(fileURLToPath(import.meta.url));\n const templateFile = path.join(moduleDir, \"templates\", \"HonoRouter.ejs\");\n\n for (const resource of context.normalizedSpec.resources) {\n writeHonoRouter(resource, templateFile, context);\n }\n}\n\nfunction writeHonoRouter(\n resource: NormalizedResource,\n templateFile: string,\n context: GeneratorContext\n): void {\n const pascalCaseEntityName = Case.pascal(resource.name);\n const outputDir = context.getResourceOutputDir(resource.name);\n const outputPath = path.join(outputDir, `${pascalCaseEntityName}Hono.ts`);\n\n const operations = resource.operations\n // Hono handles HEAD requests automatically, so we skip them\n .filter(operation => operation.method !== HttpMethod.HEAD)\n .map(operation => createOperationData(operation))\n .sort((a, b) => compareRoutes(a, b));\n\n const content = context.renderTemplate(templateFile, {\n coreDir: path.relative(outputDir, context.outputDir),\n entityName: resource.name,\n pascalCaseEntityName,\n operations,\n });\n\n const relativePath = path.relative(context.outputDir, outputPath);\n context.writeFile(relativePath, content);\n}\n\nfunction createOperationData(operation: NormalizedOperation) {\n const operationId = operation.operationId;\n const className = Case.pascal(operationId);\n const handlerName = `handle${className}Request`;\n\n return {\n operationId,\n className,\n handlerName,\n method: operation.method,\n path: operation.path,\n };\n}\n","import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { BasePlugin } from \"@rexeus/typeweaver-gen\";\nimport type { GeneratorContext } from \"@rexeus/typeweaver-gen\";\nimport { generate as generateHonoRouters } from \"./honoRouterGenerator\";\n\nconst moduleDir = path.dirname(fileURLToPath(import.meta.url));\n\nexport default class HonoPlugin extends BasePlugin {\n public name = \"hono\";\n\n public override generate(context: GeneratorContext): void {\n const libSourceDir = path.join(moduleDir, \"lib\");\n this.copyLibFiles(context, libSourceDir, this.name);\n\n generateHonoRouters(context);\n }\n}\n"],"mappings":";;;;;;AAWA,SAAgB,SAAS,SAAiC;CACxD,MAAM,YAAY,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;CAC9D,MAAM,eAAe,KAAK,KAAK,WAAW,aAAa,iBAAiB;AAExE,MAAK,MAAM,YAAY,QAAQ,eAAe,UAC5C,iBAAgB,UAAU,cAAc,QAAQ;;AAIpD,SAAS,gBACP,UACA,cACA,SACM;CACN,MAAM,uBAAuB,KAAK,OAAO,SAAS,KAAK;CACvD,MAAM,YAAY,QAAQ,qBAAqB,SAAS,KAAK;CAC7D,MAAM,aAAa,KAAK,KAAK,WAAW,GAAG,qBAAqB,SAAS;CAEzE,MAAM,aAAa,SAAS,WAEzB,QAAO,cAAa,UAAU,WAAW,WAAW,KAAK,CACzD,KAAI,cAAa,oBAAoB,UAAU,CAAC,CAChD,MAAM,GAAG,MAAM,cAAc,GAAG,EAAE,CAAC;CAEtC,MAAM,UAAU,QAAQ,eAAe,cAAc;EACnD,SAAS,KAAK,SAAS,WAAW,QAAQ,UAAU;EACpD,YAAY,SAAS;EACrB;EACA;EACD,CAAC;CAEF,MAAM,eAAe,KAAK,SAAS,QAAQ,WAAW,WAAW;AACjE,SAAQ,UAAU,cAAc,QAAQ;;AAG1C,SAAS,oBAAoB,WAAgC;CAC3D,MAAM,cAAc,UAAU;CAC9B,MAAM,YAAY,KAAK,OAAO,YAAY;AAG1C,QAAO;EACL;EACA;EACA,aALkB,SAAS,UAAU;EAMrC,QAAQ,UAAU;EAClB,MAAM,UAAU;EACjB;;;;ACnDH,MAAM,YAAY,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;AAE9D,IAAqB,aAArB,cAAwC,WAAW;CACjD,OAAc;CAEd,SAAyB,SAAiC;EACxD,MAAM,eAAe,KAAK,KAAK,WAAW,MAAM;AAChD,OAAK,aAAa,SAAS,cAAc,KAAK,KAAK;AAEnD,WAAoB,QAAQ"}
|
|
@@ -5,11 +5,21 @@
|
|
|
5
5
|
* @generated by @rexeus/typeweaver
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
createDefaultErrorBody,
|
|
10
|
+
createDefaultErrorResponse,
|
|
11
|
+
internalServerErrorDefaultError,
|
|
12
|
+
isTypedHttpResponse,
|
|
13
|
+
RequestValidationError,
|
|
14
|
+
validationDefaultError,
|
|
15
|
+
} from "@rexeus/typeweaver-core";
|
|
9
16
|
import type {
|
|
10
17
|
IHttpRequest,
|
|
11
18
|
IHttpResponse,
|
|
12
19
|
IRequestValidator,
|
|
20
|
+
IResponseValidator,
|
|
21
|
+
ITypedHttpResponse,
|
|
22
|
+
ResponseValidationError,
|
|
13
23
|
} from "@rexeus/typeweaver-core";
|
|
14
24
|
import { Hono } from "hono";
|
|
15
25
|
import { HonoAdapter } from "./HonoAdapter";
|
|
@@ -25,7 +35,7 @@ import type { BlankEnv, BlankSchema, Env, Schema } from "hono/types";
|
|
|
25
35
|
* @returns The HTTP response to send to the client
|
|
26
36
|
*/
|
|
27
37
|
export type HonoHttpResponseErrorHandler = (
|
|
28
|
-
error:
|
|
38
|
+
error: ITypedHttpResponse,
|
|
29
39
|
context: Context
|
|
30
40
|
) => Promise<IHttpResponse> | IHttpResponse;
|
|
31
41
|
|
|
@@ -35,7 +45,7 @@ export type HonoHttpResponseErrorHandler = (
|
|
|
35
45
|
* @param context - The Hono context for the current request
|
|
36
46
|
* @returns The HTTP response to send to the client
|
|
37
47
|
*/
|
|
38
|
-
export type
|
|
48
|
+
export type HonoRequestValidationErrorHandler = (
|
|
39
49
|
error: RequestValidationError,
|
|
40
50
|
context: Context
|
|
41
51
|
) => Promise<IHttpResponse> | IHttpResponse;
|
|
@@ -51,6 +61,20 @@ export type HonoUnknownErrorHandler = (
|
|
|
51
61
|
context: Context
|
|
52
62
|
) => Promise<IHttpResponse> | IHttpResponse;
|
|
53
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Handles response validation errors.
|
|
66
|
+
* Called when a handler returns a response that does not match the expected schema.
|
|
67
|
+
* @param error - The response validation error with schema mismatch details
|
|
68
|
+
* @param response - The original (invalid) response from the handler
|
|
69
|
+
* @param context - The Hono context for the current request
|
|
70
|
+
* @returns The HTTP response to send to the client (typically a 500)
|
|
71
|
+
*/
|
|
72
|
+
export type HonoResponseValidationErrorHandler = (
|
|
73
|
+
error: ResponseValidationError,
|
|
74
|
+
response: IHttpResponse,
|
|
75
|
+
context: Context
|
|
76
|
+
) => Promise<IHttpResponse> | IHttpResponse;
|
|
77
|
+
|
|
54
78
|
/**
|
|
55
79
|
* Configuration options for TypeweaverHono routers.
|
|
56
80
|
* @template RequestHandlers - Type containing all request handler methods
|
|
@@ -64,32 +88,52 @@ export type TypeweaverHonoOptions<
|
|
|
64
88
|
* Request handler methods for each operation.
|
|
65
89
|
* Each handler receives a request (validated if `validateRequests` is true) and Hono context.
|
|
66
90
|
*/
|
|
67
|
-
requestHandlers: RequestHandlers;
|
|
91
|
+
readonly requestHandlers: RequestHandlers;
|
|
68
92
|
|
|
69
93
|
/**
|
|
70
94
|
* Enable request validation using generated validators.
|
|
71
95
|
* When false, requests are passed through without validation.
|
|
72
96
|
* @default true
|
|
73
97
|
*/
|
|
74
|
-
validateRequests?: boolean;
|
|
98
|
+
readonly validateRequests?: boolean;
|
|
75
99
|
|
|
76
100
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* - `false`: Let errors bubble up to Hono
|
|
80
|
-
* - `function`: Use custom error handler
|
|
101
|
+
* Enable response validation using generated validators.
|
|
102
|
+
* When true, responses are validated and stripped of extra fields before sending.
|
|
81
103
|
* @default true
|
|
82
104
|
*/
|
|
83
|
-
|
|
105
|
+
readonly validateResponses?: boolean;
|
|
84
106
|
|
|
85
107
|
/**
|
|
86
108
|
* Configure handling of request validation errors.
|
|
87
109
|
* - `true`: Use default handler (400 with error details)
|
|
88
110
|
* - `false`: Let errors bubble up to Hono
|
|
111
|
+
* - `function`: Use custom request validation error handler
|
|
112
|
+
* @default true
|
|
113
|
+
*/
|
|
114
|
+
readonly handleRequestValidationErrors?:
|
|
115
|
+
| HonoRequestValidationErrorHandler
|
|
116
|
+
| boolean;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Configure handling of response validation errors.
|
|
120
|
+
* - `true`: Use default handler (500 Internal Server Error)
|
|
121
|
+
* - `false`: Disable response validation error handling (return response as-is)
|
|
122
|
+
* - `function`: Use custom response validation error handler
|
|
123
|
+
* @default true
|
|
124
|
+
*/
|
|
125
|
+
readonly handleResponseValidationErrors?:
|
|
126
|
+
| HonoResponseValidationErrorHandler
|
|
127
|
+
| boolean;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Configure handling of HttpResponse errors thrown by handlers.
|
|
131
|
+
* - `true`: Use default handler (returns the error as-is)
|
|
132
|
+
* - `false`: Let errors bubble up to Hono
|
|
89
133
|
* - `function`: Use custom error handler
|
|
90
134
|
* @default true
|
|
91
135
|
*/
|
|
92
|
-
|
|
136
|
+
readonly handleHttpResponseErrors?: HonoHttpResponseErrorHandler | boolean;
|
|
93
137
|
|
|
94
138
|
/**
|
|
95
139
|
* Configure handling of unknown errors.
|
|
@@ -98,7 +142,7 @@ export type TypeweaverHonoOptions<
|
|
|
98
142
|
* - `function`: Use custom error handler
|
|
99
143
|
* @default true
|
|
100
144
|
*/
|
|
101
|
-
handleUnknownErrors?: HonoUnknownErrorHandler | boolean;
|
|
145
|
+
readonly handleUnknownErrors?: HonoUnknownErrorHandler | boolean;
|
|
102
146
|
};
|
|
103
147
|
|
|
104
148
|
/**
|
|
@@ -134,11 +178,15 @@ export abstract class TypeweaverHono<
|
|
|
134
178
|
* Resolved configuration for validation and error handling.
|
|
135
179
|
*/
|
|
136
180
|
private readonly config: {
|
|
137
|
-
validateRequests: boolean;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
181
|
+
readonly validateRequests: boolean;
|
|
182
|
+
readonly validateResponses: boolean;
|
|
183
|
+
readonly errorHandlers: {
|
|
184
|
+
readonly requestValidation: HonoRequestValidationErrorHandler | undefined;
|
|
185
|
+
readonly responseValidation:
|
|
186
|
+
| HonoResponseValidationErrorHandler
|
|
187
|
+
| undefined;
|
|
188
|
+
readonly httpResponse: HonoHttpResponseErrorHandler | undefined;
|
|
189
|
+
readonly unknown: HonoUnknownErrorHandler | undefined;
|
|
142
190
|
};
|
|
143
191
|
};
|
|
144
192
|
|
|
@@ -146,11 +194,10 @@ export abstract class TypeweaverHono<
|
|
|
146
194
|
* Default error handlers used when custom handlers are not provided.
|
|
147
195
|
*/
|
|
148
196
|
private readonly defaultHandlers = {
|
|
149
|
-
|
|
150
|
-
statusCode:
|
|
197
|
+
requestValidation: (error: RequestValidationError): IHttpResponse => ({
|
|
198
|
+
statusCode: validationDefaultError.statusCode,
|
|
151
199
|
body: {
|
|
152
|
-
|
|
153
|
-
message: error.message,
|
|
200
|
+
...createDefaultErrorBody(validationDefaultError),
|
|
154
201
|
issues: {
|
|
155
202
|
header: error.headerIssues,
|
|
156
203
|
body: error.bodyIssues,
|
|
@@ -160,15 +207,13 @@ export abstract class TypeweaverHono<
|
|
|
160
207
|
},
|
|
161
208
|
}),
|
|
162
209
|
|
|
163
|
-
|
|
210
|
+
responseValidation: (): IHttpResponse =>
|
|
211
|
+
createDefaultErrorResponse(internalServerErrorDefaultError),
|
|
164
212
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
message: "An unexpected error occurred.",
|
|
170
|
-
},
|
|
171
|
-
}),
|
|
213
|
+
httpResponse: (error: ITypedHttpResponse): IHttpResponse => error,
|
|
214
|
+
|
|
215
|
+
unknown: (): IHttpResponse =>
|
|
216
|
+
createDefaultErrorResponse(internalServerErrorDefaultError),
|
|
172
217
|
};
|
|
173
218
|
|
|
174
219
|
/**
|
|
@@ -178,15 +223,17 @@ export abstract class TypeweaverHono<
|
|
|
178
223
|
* @param options.requestHandlers - Object containing all request handler methods
|
|
179
224
|
* @param options.validateRequests - Whether to validate requests (default: true)
|
|
180
225
|
* @param options.handleHttpResponseErrors - Handler or boolean for HTTP errors (default: true)
|
|
181
|
-
* @param options.
|
|
226
|
+
* @param options.handleRequestValidationErrors - Handler or boolean for request validation errors (default: true)
|
|
182
227
|
* @param options.handleUnknownErrors - Handler or boolean for unknown errors (default: true)
|
|
183
228
|
*/
|
|
184
229
|
public constructor(options: TypeweaverHonoOptions<RequestHandlers, HonoEnv>) {
|
|
185
230
|
const {
|
|
186
231
|
requestHandlers,
|
|
187
232
|
validateRequests = true,
|
|
233
|
+
validateResponses = true,
|
|
188
234
|
handleHttpResponseErrors,
|
|
189
|
-
|
|
235
|
+
handleRequestValidationErrors,
|
|
236
|
+
handleResponseValidationErrors,
|
|
190
237
|
handleUnknownErrors,
|
|
191
238
|
...honoOptions
|
|
192
239
|
} = options;
|
|
@@ -198,9 +245,15 @@ export abstract class TypeweaverHono<
|
|
|
198
245
|
// Resolve configuration
|
|
199
246
|
this.config = {
|
|
200
247
|
validateRequests,
|
|
248
|
+
validateResponses,
|
|
201
249
|
errorHandlers: {
|
|
202
|
-
|
|
203
|
-
|
|
250
|
+
requestValidation: this.resolveErrorHandler(
|
|
251
|
+
handleRequestValidationErrors,
|
|
252
|
+
error => this.defaultHandlers.requestValidation(error)
|
|
253
|
+
),
|
|
254
|
+
responseValidation: this.resolveErrorHandler(
|
|
255
|
+
handleResponseValidationErrors,
|
|
256
|
+
(_error, _response) => this.defaultHandlers.responseValidation()
|
|
204
257
|
),
|
|
205
258
|
httpResponse: this.resolveErrorHandler(
|
|
206
259
|
handleHttpResponseErrors,
|
|
@@ -239,24 +292,29 @@ export abstract class TypeweaverHono<
|
|
|
239
292
|
* Processes errors in order: validation, HTTP response, unknown.
|
|
240
293
|
*/
|
|
241
294
|
protected registerErrorHandler(): void {
|
|
242
|
-
this.onError(
|
|
295
|
+
this.onError(async (error, context) =>
|
|
296
|
+
this.adapter.toResponse(await this.handleError(error, context))
|
|
297
|
+
);
|
|
243
298
|
}
|
|
244
299
|
|
|
245
300
|
/**
|
|
246
301
|
* Safely executes an error handler and returns null if it fails.
|
|
247
|
-
* This allows for graceful fallback to the next handler in the chain
|
|
302
|
+
* This allows for graceful fallback to the next handler in the chain
|
|
303
|
+
* without crashing the request pipeline.
|
|
248
304
|
*
|
|
249
305
|
* @param handlerFn - Function that executes the error handler
|
|
250
|
-
* @returns
|
|
306
|
+
* @returns The handler's response if successful, null if the handler throws
|
|
251
307
|
*/
|
|
252
|
-
private async
|
|
308
|
+
private async safelyExecuteErrorHandler(
|
|
253
309
|
handlerFn: () => Promise<IHttpResponse> | IHttpResponse
|
|
254
|
-
): Promise<
|
|
310
|
+
): Promise<IHttpResponse | null> {
|
|
255
311
|
try {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
312
|
+
return await handlerFn();
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.error(
|
|
315
|
+
"TypeweaverHono: error handler threw while handling error",
|
|
316
|
+
error
|
|
317
|
+
);
|
|
260
318
|
return null;
|
|
261
319
|
}
|
|
262
320
|
}
|
|
@@ -264,24 +322,21 @@ export abstract class TypeweaverHono<
|
|
|
264
322
|
protected async handleError(
|
|
265
323
|
error: unknown,
|
|
266
324
|
context: Context
|
|
267
|
-
): Promise<
|
|
325
|
+
): Promise<IHttpResponse> {
|
|
268
326
|
// Handle validation errors
|
|
269
327
|
if (
|
|
270
328
|
error instanceof RequestValidationError &&
|
|
271
|
-
this.config.errorHandlers.
|
|
329
|
+
this.config.errorHandlers.requestValidation
|
|
272
330
|
) {
|
|
273
|
-
const response = await this.
|
|
274
|
-
this.config.errorHandlers.
|
|
331
|
+
const response = await this.safelyExecuteErrorHandler(() =>
|
|
332
|
+
this.config.errorHandlers.requestValidation!(error, context)
|
|
275
333
|
);
|
|
276
334
|
if (response) return response;
|
|
277
335
|
}
|
|
278
336
|
|
|
279
337
|
// Handle HTTP response errors
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
this.config.errorHandlers.httpResponse
|
|
283
|
-
) {
|
|
284
|
-
const response = await this.safelyExecuteHandler(() =>
|
|
338
|
+
if (isTypedHttpResponse(error) && this.config.errorHandlers.httpResponse) {
|
|
339
|
+
const response = await this.safelyExecuteErrorHandler(() =>
|
|
285
340
|
this.config.errorHandlers.httpResponse!(error, context)
|
|
286
341
|
);
|
|
287
342
|
if (response) return response;
|
|
@@ -289,7 +344,7 @@ export abstract class TypeweaverHono<
|
|
|
289
344
|
|
|
290
345
|
// Handle unknown errors
|
|
291
346
|
if (this.config.errorHandlers.unknown) {
|
|
292
|
-
const response = await this.
|
|
347
|
+
const response = await this.safelyExecuteErrorHandler(() =>
|
|
293
348
|
this.config.errorHandlers.unknown!(error, context)
|
|
294
349
|
);
|
|
295
350
|
if (response) return response;
|
|
@@ -304,7 +359,8 @@ export abstract class TypeweaverHono<
|
|
|
304
359
|
*
|
|
305
360
|
* @param context - Hono context for the current request
|
|
306
361
|
* @param operationId - Unique operation identifier from the API definition
|
|
307
|
-
* @param
|
|
362
|
+
* @param requestValidator - Request validator for the specific operation
|
|
363
|
+
* @param responseValidator - Response validator for the specific operation
|
|
308
364
|
* @param handler - Type-safe request handler function
|
|
309
365
|
* @returns Hono-compatible Response object
|
|
310
366
|
*/
|
|
@@ -314,7 +370,8 @@ export abstract class TypeweaverHono<
|
|
|
314
370
|
>(
|
|
315
371
|
context: Context,
|
|
316
372
|
operationId: string,
|
|
317
|
-
|
|
373
|
+
requestValidator: IRequestValidator,
|
|
374
|
+
responseValidator: IResponseValidator,
|
|
318
375
|
handler: HonoRequestHandler<TRequest, TResponse>
|
|
319
376
|
): Promise<Response> {
|
|
320
377
|
try {
|
|
@@ -322,15 +379,67 @@ export abstract class TypeweaverHono<
|
|
|
322
379
|
|
|
323
380
|
const httpRequest = await this.adapter.toRequest(context);
|
|
324
381
|
|
|
325
|
-
// Conditionally validate
|
|
326
382
|
const validatedRequest = this.config.validateRequests
|
|
327
|
-
? (
|
|
383
|
+
? (requestValidator.validate(httpRequest) as TRequest)
|
|
328
384
|
: (httpRequest as TRequest);
|
|
329
385
|
|
|
330
386
|
const httpResponse = await handler(validatedRequest, context);
|
|
331
|
-
return this.adapter.toResponse(
|
|
387
|
+
return this.adapter.toResponse(
|
|
388
|
+
await this.validateResponse(responseValidator, httpResponse, context)
|
|
389
|
+
);
|
|
332
390
|
} catch (error) {
|
|
333
|
-
|
|
391
|
+
if (isTypedHttpResponse(error) && this.config.validateResponses) {
|
|
392
|
+
const validated = await this.validateResponse(
|
|
393
|
+
responseValidator,
|
|
394
|
+
error,
|
|
395
|
+
context
|
|
396
|
+
);
|
|
397
|
+
return this.adapter.toResponse(validated);
|
|
398
|
+
}
|
|
399
|
+
return this.adapter.toResponse(await this.handleError(error, context));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Validates a response against the operation's response validator.
|
|
405
|
+
*
|
|
406
|
+
* Behavior depends on configuration:
|
|
407
|
+
* - `validateResponses: false` โ returns the original response unchanged.
|
|
408
|
+
* - `validateResponses: true` (default) โ runs validation:
|
|
409
|
+
* - Valid response โ returns the stripped response (extra fields removed).
|
|
410
|
+
* - Invalid response + handler configured โ calls the handler safely.
|
|
411
|
+
* If the handler throws, falls back to the original response.
|
|
412
|
+
* - Invalid response + `handleResponseValidationErrors: false` โ returns
|
|
413
|
+
* the original (invalid) response as-is.
|
|
414
|
+
*
|
|
415
|
+
* @param responseValidator - The response validator for the operation
|
|
416
|
+
* @param response - The response to validate
|
|
417
|
+
* @param context - The Hono context for the current request
|
|
418
|
+
* @returns The validated (and stripped) response, the handler's response, or the original
|
|
419
|
+
*/
|
|
420
|
+
private async validateResponse(
|
|
421
|
+
responseValidator: IResponseValidator,
|
|
422
|
+
response: IHttpResponse,
|
|
423
|
+
context: Context
|
|
424
|
+
): Promise<IHttpResponse> {
|
|
425
|
+
if (!this.config.validateResponses) return response;
|
|
426
|
+
|
|
427
|
+
const result = responseValidator.safeValidate(response);
|
|
428
|
+
|
|
429
|
+
if (result.isValid) return result.data;
|
|
430
|
+
|
|
431
|
+
if (this.config.errorHandlers.responseValidation) {
|
|
432
|
+
const handlerResponse = await this.safelyExecuteErrorHandler(() =>
|
|
433
|
+
this.config.errorHandlers.responseValidation!(
|
|
434
|
+
result.error,
|
|
435
|
+
response,
|
|
436
|
+
context
|
|
437
|
+
)
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
if (handlerResponse) return handlerResponse;
|
|
334
441
|
}
|
|
442
|
+
|
|
443
|
+
return response;
|
|
335
444
|
}
|
|
336
445
|
}
|
|
@@ -12,6 +12,7 @@ import { TypeweaverHono, type HonoRequestHandler, type TypeweaverHonoOptions } f
|
|
|
12
12
|
import type { I<%- operation.className %>Request } from "./<%- operation.className %>Request";
|
|
13
13
|
import { <%- operation.className %>RequestValidator } from "./<%- operation.className %>RequestValidator";
|
|
14
14
|
import type { <%- operation.className %>Response } from "./<%- operation.className %>Response";
|
|
15
|
+
import { <%- operation.className %>ResponseValidator } from "./<%- operation.className %>ResponseValidator";
|
|
15
16
|
<% } %>
|
|
16
17
|
|
|
17
18
|
export type Hono<%- pascalCaseEntityName %>ApiHandler = {
|
|
@@ -33,6 +34,7 @@ export class <%- pascalCaseEntityName %>Hono extends TypeweaverHono<Hono<%- pasc
|
|
|
33
34
|
context,
|
|
34
35
|
'<%- operation.operationId %>',
|
|
35
36
|
new <%- operation.className %>RequestValidator(),
|
|
37
|
+
new <%- operation.className %>ResponseValidator(),
|
|
36
38
|
this.requestHandlers.<%- operation.handlerName %>.bind(this.requestHandlers)
|
|
37
39
|
));
|
|
38
40
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rexeus/typeweaver-hono",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Generates Hono routers and handlers straight from your API definitions. Powered by Typeweaver ๐งตโจ",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -47,20 +47,20 @@
|
|
|
47
47
|
"homepage": "https://github.com/rexeus/typeweaver#readme",
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"hono": "^4.11.0",
|
|
50
|
-
"@rexeus/typeweaver-core": "^0.
|
|
51
|
-
"@rexeus/typeweaver-gen": "^0.
|
|
50
|
+
"@rexeus/typeweaver-core": "^0.9.0",
|
|
51
|
+
"@rexeus/typeweaver-gen": "^0.9.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"hono": "^4.
|
|
54
|
+
"hono": "^4.12.9",
|
|
55
55
|
"test-utils": "file:../test-utils",
|
|
56
|
-
"@rexeus/typeweaver-core": "^0.
|
|
57
|
-
"@rexeus/typeweaver-gen": "^0.
|
|
56
|
+
"@rexeus/typeweaver-core": "^0.9.0",
|
|
57
|
+
"@rexeus/typeweaver-gen": "^0.9.0"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"case": "^1.6.3"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
|
-
"typecheck": "tsc --noEmit",
|
|
63
|
+
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
|
|
64
64
|
"format": "oxfmt",
|
|
65
65
|
"build": "tsdown && mkdir -p ./dist/templates ./dist/lib && cp -r ./src/templates/* ./dist/templates/ && cp -r ./src/lib/* ./dist/lib/ && cp ../../LICENSE ../../NOTICE ./dist/",
|
|
66
66
|
"test": "vitest --run",
|