evikit 2.0.0 → 2.1.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 +1 -1
- package/package.json +1 -1
- package/server.d.ts +3 -1
- package/server.js +66 -9
- package/use-enhance.js +20 -8
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Vite is a good foundation for building a server-rendered monolith SPA with an JS
|
|
|
13
13
|
- Show the language chosen by the user in browser settings
|
|
14
14
|
- Have SEO meta descriptions
|
|
15
15
|
- Generate API documentation with a Swagger overview
|
|
16
|
-
-
|
|
16
|
+
- Send multipart not urlencoded when there's a file
|
|
17
17
|
|
|
18
18
|
_TODO_ Add more simple language docs near code, don't overwhelm readers.
|
|
19
19
|
|
package/package.json
CHANGED
package/server.d.ts
CHANGED
|
@@ -91,7 +91,9 @@ export function router(
|
|
|
91
91
|
| {
|
|
92
92
|
content: {
|
|
93
93
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
94
|
-
"application/x-www-form-urlencoded"
|
|
94
|
+
"application/x-www-form-urlencoded"?: { schema: any };
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
96
|
+
"multipart/form-data"?: { schema: any };
|
|
95
97
|
};
|
|
96
98
|
};
|
|
97
99
|
responses: Record<
|
package/server.js
CHANGED
|
@@ -20,6 +20,7 @@ import { Po } from "./i18n.js";
|
|
|
20
20
|
|
|
21
21
|
/** @type {import("./server-generic.js").ServerEndpoint} */
|
|
22
22
|
export function endpoint({ input, outputs }, handler) {
|
|
23
|
+
const arrayOf = ArrayOf(input);
|
|
23
24
|
const schema = union(
|
|
24
25
|
Object.entries(outputs).map(([status, schema]) =>
|
|
25
26
|
object({
|
|
@@ -34,7 +35,7 @@ export function endpoint({ input, outputs }, handler) {
|
|
|
34
35
|
return {
|
|
35
36
|
handler: async (req, res) => {
|
|
36
37
|
try {
|
|
37
|
-
/** @type {Record<string, string | string[]>} */
|
|
38
|
+
/** @type {Record<string, string | File | (string | File)[]>} */
|
|
38
39
|
const body = {};
|
|
39
40
|
if (req.headers["content-type"]) {
|
|
40
41
|
const busboy = new Busboy({
|
|
@@ -44,13 +45,39 @@ export function endpoint({ input, outputs }, handler) {
|
|
|
44
45
|
body[name] =
|
|
45
46
|
typeof body[name] == "undefined"
|
|
46
47
|
? value
|
|
47
|
-
: [
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
: body[name]),
|
|
51
|
-
value,
|
|
52
|
-
];
|
|
48
|
+
: Array.isArray(body[name])
|
|
49
|
+
? [...body[name], value]
|
|
50
|
+
: [body[name], value];
|
|
53
51
|
});
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
Object.values(input.entries).some(
|
|
55
|
+
(entry) => entry.type == "file" || arrayOf(entry, "file"),
|
|
56
|
+
)
|
|
57
|
+
) {
|
|
58
|
+
busboy.on("file", (name, stream, filename) => {
|
|
59
|
+
const entry = input.entries[name];
|
|
60
|
+
if (entry && (entry.type == "file" || arrayOf(entry, "file"))) {
|
|
61
|
+
/** @type {ArrayBuffer[]} */
|
|
62
|
+
const buffers = [];
|
|
63
|
+
stream.on("data", (data) => {
|
|
64
|
+
buffers.push(data);
|
|
65
|
+
});
|
|
66
|
+
stream.on("end", () => {
|
|
67
|
+
const value = new File(buffers, filename);
|
|
68
|
+
body[name] =
|
|
69
|
+
typeof body[name] == "undefined"
|
|
70
|
+
? value
|
|
71
|
+
: Array.isArray(body[name])
|
|
72
|
+
? [...body[name], value]
|
|
73
|
+
: [body[name], value];
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
stream.resume();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
54
81
|
await new Promise((resolve, reject) => {
|
|
55
82
|
busboy.on("error", reject).on("finish", resolve);
|
|
56
83
|
req.pipe(busboy);
|
|
@@ -269,6 +296,7 @@ export function router({ pkg }, ...routes) {
|
|
|
269
296
|
path.replace(/:(\w+)/g, "{$1}"),
|
|
270
297
|
Object.fromEntries(
|
|
271
298
|
Object.entries(controller).map(([method, { input, outputs }]) => {
|
|
299
|
+
const arrayOf = ArrayOf(input);
|
|
272
300
|
const parameters = path
|
|
273
301
|
.split("/")
|
|
274
302
|
.filter((component) => /^:/.test(component))
|
|
@@ -286,7 +314,12 @@ export function router({ pkg }, ...routes) {
|
|
|
286
314
|
? {
|
|
287
315
|
requestBody: {
|
|
288
316
|
content: {
|
|
289
|
-
|
|
317
|
+
[Object.values(input.entries).some(
|
|
318
|
+
(entry) =>
|
|
319
|
+
entry.type == "file" || arrayOf(entry, "file"),
|
|
320
|
+
)
|
|
321
|
+
? "multipart/form-data"
|
|
322
|
+
: "application/x-www-form-urlencoded"]: {
|
|
290
323
|
schema: toJsonSchema(
|
|
291
324
|
object(
|
|
292
325
|
Object.fromEntries(
|
|
@@ -295,7 +328,13 @@ export function router({ pkg }, ...routes) {
|
|
|
295
328
|
),
|
|
296
329
|
),
|
|
297
330
|
),
|
|
298
|
-
{
|
|
331
|
+
{
|
|
332
|
+
ignoreActions: ["trim"],
|
|
333
|
+
overrideSchema: (context) =>
|
|
334
|
+
context.valibotSchema.type == "file"
|
|
335
|
+
? { format: "binary", type: "string" }
|
|
336
|
+
: context.jsonSchema,
|
|
337
|
+
},
|
|
299
338
|
),
|
|
300
339
|
},
|
|
301
340
|
},
|
|
@@ -426,3 +465,21 @@ export function text(output, status) {
|
|
|
426
465
|
status,
|
|
427
466
|
};
|
|
428
467
|
}
|
|
468
|
+
|
|
469
|
+
/** @param {import("valibot").ObjectSchema<any, any>} input */
|
|
470
|
+
function ArrayOf(input) {
|
|
471
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
472
|
+
const entries = Object.values(input);
|
|
473
|
+
/**
|
|
474
|
+
* @param {typeof entries[0]} entry
|
|
475
|
+
* @param {string} type
|
|
476
|
+
*/
|
|
477
|
+
return function arrayOf(entry, type) {
|
|
478
|
+
if (entry.type != "array") {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
/** @type {import("valibot").ArraySchema<any, any>} */
|
|
482
|
+
const array = /** @type {any} */ (entry);
|
|
483
|
+
return array.item.type == type;
|
|
484
|
+
};
|
|
485
|
+
}
|
package/use-enhance.js
CHANGED
|
@@ -21,14 +21,23 @@ export function useEnhance(schema, onSuccess) {
|
|
|
21
21
|
setBusy(true);
|
|
22
22
|
const form = /** @type {HTMLFormElement} */ (event.target);
|
|
23
23
|
let output;
|
|
24
|
+
|
|
24
25
|
try {
|
|
25
|
-
const body =
|
|
26
|
+
const body = form.querySelector('[type="file"]')
|
|
27
|
+
? new FormData()
|
|
28
|
+
: new URLSearchParams();
|
|
26
29
|
for (const input of [...form.querySelectorAll("input")]) {
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
if (!input.disabled) {
|
|
31
|
+
if (input.type == "file" && body instanceof FormData) {
|
|
32
|
+
for (const file of input.files || []) {
|
|
33
|
+
body.append(input.name, file, file.name);
|
|
34
|
+
}
|
|
35
|
+
} else if (
|
|
36
|
+
(input.type != "checkbox" && input.type != "radio") ||
|
|
37
|
+
input.checked
|
|
38
|
+
) {
|
|
39
|
+
body.append(input.name, input.value);
|
|
40
|
+
}
|
|
32
41
|
}
|
|
33
42
|
}
|
|
34
43
|
for (const select of [...form.querySelectorAll("select")]) {
|
|
@@ -41,11 +50,14 @@ export function useEnhance(schema, onSuccess) {
|
|
|
41
50
|
body.append(textarea.name, textarea.value);
|
|
42
51
|
}
|
|
43
52
|
}
|
|
53
|
+
|
|
44
54
|
const response = await fetch(form.action, {
|
|
45
|
-
body: body.toString(),
|
|
55
|
+
body: body instanceof FormData ? body : body.toString(),
|
|
46
56
|
headers: {
|
|
47
57
|
Accept: "application/json",
|
|
48
|
-
|
|
58
|
+
...(body instanceof FormData
|
|
59
|
+
? {}
|
|
60
|
+
: { "Content-Type": "application/x-www-form-urlencoded" }),
|
|
49
61
|
Cookie: jsdomCookie.setCookie.join("; "),
|
|
50
62
|
},
|
|
51
63
|
method: form.method,
|