@server/next 0.49.0 → 0.49.2
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/index.d.ts +17 -6
- package/index.js +244 -143
- package/package.json +7 -6
- package/readme.md +12 -10
- package/src/jsx/jsx-runtime.js +14 -12
package/index.d.ts
CHANGED
|
@@ -7,22 +7,28 @@ type FileInfo = {
|
|
|
7
7
|
type: string | null;
|
|
8
8
|
modified: Date;
|
|
9
9
|
};
|
|
10
|
+
type ReadOptions = {
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
};
|
|
10
13
|
type BucketFile = {
|
|
11
14
|
readonly path: string;
|
|
12
15
|
readonly name: string;
|
|
13
16
|
readonly type?: string;
|
|
14
|
-
exists(): Promise<boolean>;
|
|
15
|
-
info?(): Promise<FileInfo | null>;
|
|
17
|
+
exists(opts?: ReadOptions): Promise<boolean>;
|
|
18
|
+
info?(opts?: ReadOptions): Promise<FileInfo | null>;
|
|
16
19
|
write(content: string | Buffer | ReadableStream, options?: {
|
|
17
20
|
type?: string;
|
|
18
|
-
}): Promise<
|
|
19
|
-
stream(): ReadableStream;
|
|
21
|
+
} & ReadOptions): Promise<unknown>;
|
|
22
|
+
stream(opts?: ReadOptions): ReadableStream;
|
|
20
23
|
slice?(start: number, end?: number): BucketFile;
|
|
21
|
-
bytes(): Promise<Uint8Array>;
|
|
22
|
-
remove(): Promise<
|
|
24
|
+
bytes(opts?: ReadOptions): Promise<Uint8Array>;
|
|
25
|
+
remove(opts?: ReadOptions): Promise<unknown>;
|
|
23
26
|
};
|
|
24
27
|
type Bucket = {
|
|
25
28
|
file(name: string): BucketFile;
|
|
29
|
+
create(content: string | Buffer | ReadableStream, options?: {
|
|
30
|
+
type?: string;
|
|
31
|
+
} & ReadOptions): Promise<BucketFile>;
|
|
26
32
|
folder?(prefix: string): Bucket;
|
|
27
33
|
};
|
|
28
34
|
|
|
@@ -203,6 +209,7 @@ type AuthOption = string | AuthFunction | AuthConfig<any> | AuthVerify<any> | Au
|
|
|
203
209
|
type AuthContext = Pick<Context, "options" | "headers" | "cookies" | "platform" | "app">;
|
|
204
210
|
type AuthEntry = {
|
|
205
211
|
name: string;
|
|
212
|
+
providers?: string[];
|
|
206
213
|
user: (ctx: AuthContext) => Promise<any>;
|
|
207
214
|
routes?: () => Router;
|
|
208
215
|
};
|
|
@@ -577,6 +584,10 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
|
|
|
577
584
|
referrerPolicy?: ReferrerPolicy;
|
|
578
585
|
window?: null;
|
|
579
586
|
}) => Promise<Response>;
|
|
587
|
+
readonly cookies: {
|
|
588
|
+
[k: string]: string;
|
|
589
|
+
};
|
|
590
|
+
clear: () => void;
|
|
580
591
|
};
|
|
581
592
|
}
|
|
582
593
|
declare function server<U = AuthProfile>(options: Omit<Options, "auth"> & {
|
package/index.js
CHANGED
|
@@ -245,7 +245,8 @@ function createCookies(key, val) {
|
|
|
245
245
|
if (val.value === null) val.expires = EXPIRED;
|
|
246
246
|
const { value, path, expires, maxAge, httpOnly, secure, sameSite } = val;
|
|
247
247
|
let str = `${key}=${encodeURIComponent(value ?? "")};Path=${path || "/"}`;
|
|
248
|
-
if (typeof expires !== "undefined")
|
|
248
|
+
if (typeof expires !== "undefined")
|
|
249
|
+
str += `;Expires=${normalizeExpires(expires)}`;
|
|
249
250
|
if (typeof maxAge === "number") str += `;Max-Age=${maxAge}`;
|
|
250
251
|
if (httpOnly) str += ";HttpOnly";
|
|
251
252
|
if (secure) str += ";Secure";
|
|
@@ -513,7 +514,11 @@ function serialize(body, headers2) {
|
|
|
513
514
|
return body;
|
|
514
515
|
}
|
|
515
516
|
if (typeof body === "string") {
|
|
516
|
-
fill(
|
|
517
|
+
fill(
|
|
518
|
+
headers2,
|
|
519
|
+
isHtml(body) ? mimes_default.html : mimes_default.text,
|
|
520
|
+
Buffer.byteLength(body)
|
|
521
|
+
);
|
|
517
522
|
return body;
|
|
518
523
|
}
|
|
519
524
|
if (body instanceof Uint8Array) {
|
|
@@ -676,13 +681,22 @@ var redirect = (...args) => r().redirect(...args);
|
|
|
676
681
|
// src/body/sniff.ts
|
|
677
682
|
var ascii = (text) => [...text].map((char) => char.charCodeAt(0));
|
|
678
683
|
var SIGNATURES = [
|
|
679
|
-
{
|
|
684
|
+
{
|
|
685
|
+
type: "image/png",
|
|
686
|
+
magic: [137, 80, 78, 71, 13, 10, 26, 10]
|
|
687
|
+
},
|
|
680
688
|
{ type: "image/jpeg", magic: [255, 216, 255] },
|
|
681
689
|
{ type: "image/gif", magic: ascii("GIF87a") },
|
|
682
690
|
{ type: "image/gif", magic: ascii("GIF89a") },
|
|
683
691
|
// RIFF containers: the format is at byte 8, so the whole thing is one match
|
|
684
|
-
{
|
|
685
|
-
|
|
692
|
+
{
|
|
693
|
+
type: "image/webp",
|
|
694
|
+
magic: [...ascii("RIFF"), null, null, null, null, ...ascii("WEBP")]
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
type: "audio/wav",
|
|
698
|
+
magic: [...ascii("RIFF"), null, null, null, null, ...ascii("WAVE")]
|
|
699
|
+
},
|
|
686
700
|
{ type: "image/bmp", magic: ascii("BM") },
|
|
687
701
|
{ type: "image/tiff", magic: [73, 73, 42, 0] },
|
|
688
702
|
{ type: "image/tiff", magic: [77, 77, 0, 42] },
|
|
@@ -748,26 +762,25 @@ function resolveUploads(up) {
|
|
|
748
762
|
maxFiles: DEFAULT_FILES
|
|
749
763
|
};
|
|
750
764
|
}
|
|
751
|
-
function getExt(filename) {
|
|
752
|
-
const i = filename.lastIndexOf(".");
|
|
753
|
-
if (i <= 0) return ".bin";
|
|
754
|
-
return filename.slice(i).toLowerCase();
|
|
755
|
-
}
|
|
756
765
|
function validateFile(originalName, contentType, limits, sniffed) {
|
|
757
766
|
const { fileType: fileType2 } = limits;
|
|
758
|
-
if (!fileType2 || fileType2.length === 0) return;
|
|
759
767
|
if (sniffed === null && isSniffable(contentType)) {
|
|
760
768
|
throw errors_default.UPLOAD_TYPE_NOT_ALLOWED({
|
|
761
769
|
name: originalName,
|
|
762
770
|
type: contentType,
|
|
763
|
-
allowed: fileType2
|
|
771
|
+
allowed: fileType2 ?? [contentType]
|
|
764
772
|
});
|
|
765
773
|
}
|
|
766
|
-
|
|
767
|
-
const
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
774
|
+
if (!fileType2 || fileType2.length === 0) return;
|
|
775
|
+
const base = (value) => value.split(";")[0].trim().toLowerCase();
|
|
776
|
+
const type2 = base(contentType);
|
|
777
|
+
const allowed = fileType2.some((one) => {
|
|
778
|
+
const entry = one.trim().toLowerCase();
|
|
779
|
+
if (entry.endsWith("/*")) return type2.startsWith(entry.slice(0, -1));
|
|
780
|
+
if (entry.includes("/")) return base(entry) === type2;
|
|
781
|
+
const mapped = mimes_default[entry.replace(/^\./, "")];
|
|
782
|
+
return Boolean(mapped) && base(mapped) === type2;
|
|
783
|
+
});
|
|
771
784
|
if (!allowed) {
|
|
772
785
|
throw errors_default.UPLOAD_TYPE_NOT_ALLOWED({
|
|
773
786
|
name: originalName,
|
|
@@ -828,7 +841,11 @@ var Router = class _Router {
|
|
|
828
841
|
}
|
|
829
842
|
const base = method === "socket" ? [] : this.middleware;
|
|
830
843
|
const fns = [...base, ...rest].filter((fn) => fn != null);
|
|
831
|
-
this.handlers[method].push({
|
|
844
|
+
this.handlers[method].push({
|
|
845
|
+
path,
|
|
846
|
+
options,
|
|
847
|
+
fns
|
|
848
|
+
});
|
|
832
849
|
return this.self();
|
|
833
850
|
}
|
|
834
851
|
socket(pathOrMid, optionsOrMid, ...middleware) {
|
|
@@ -1018,7 +1035,9 @@ function validate(strategy, expires, config2) {
|
|
|
1018
1035
|
seconds(expires);
|
|
1019
1036
|
const { onLogin, getUser, toPublicUser } = config2;
|
|
1020
1037
|
if (onLogin && !getUser) {
|
|
1021
|
-
throw new Error(
|
|
1038
|
+
throw new Error(
|
|
1039
|
+
"`onLogin` needs a `getUser`: something has to resolve the id it returns."
|
|
1040
|
+
);
|
|
1022
1041
|
}
|
|
1023
1042
|
if (isSigned(strategy)) {
|
|
1024
1043
|
if (getUser && !toPublicUser) {
|
|
@@ -1054,7 +1073,9 @@ async function credentialPayload(config2, strategy, ctx, profile) {
|
|
|
1054
1073
|
if (!isSigned(strategy)) return { sub: String(id) };
|
|
1055
1074
|
const user = await getUser(String(id), ctx);
|
|
1056
1075
|
if (user === void 0 || user === null) {
|
|
1057
|
-
throw new Error(
|
|
1076
|
+
throw new Error(
|
|
1077
|
+
`getUser returned nothing for the id "${id}" that onLogin just returned`
|
|
1078
|
+
);
|
|
1058
1079
|
}
|
|
1059
1080
|
return { user: await toPublicUser(user) };
|
|
1060
1081
|
}
|
|
@@ -1450,7 +1471,12 @@ var callbackRoute = ({ name, options, provider }, redirects, finish) => async (c
|
|
|
1450
1471
|
const pending = await readState(ctx, query.state);
|
|
1451
1472
|
if (!query.code) throw errors_default.AUTH_NO_CODE();
|
|
1452
1473
|
try {
|
|
1453
|
-
const profile = await provider.exchange(
|
|
1474
|
+
const profile = await provider.exchange(
|
|
1475
|
+
ctx,
|
|
1476
|
+
options,
|
|
1477
|
+
query.code,
|
|
1478
|
+
pending
|
|
1479
|
+
);
|
|
1454
1480
|
return spendState(await finish(ctx, profile));
|
|
1455
1481
|
} catch (error) {
|
|
1456
1482
|
const message = failureMessage(error, name);
|
|
@@ -1477,6 +1503,7 @@ function flowEntry(config2) {
|
|
|
1477
1503
|
};
|
|
1478
1504
|
return {
|
|
1479
1505
|
name: "flow",
|
|
1506
|
+
providers: list.map((one) => one.name),
|
|
1480
1507
|
async user(ctx) {
|
|
1481
1508
|
const payload = await read(ctx, strategy);
|
|
1482
1509
|
if (!payload) return;
|
|
@@ -1489,11 +1516,16 @@ function flowEntry(config2) {
|
|
|
1489
1516
|
const r2 = router();
|
|
1490
1517
|
for (const one of list) {
|
|
1491
1518
|
r2.get(`/auth/login/${one.name}`, SPEC, loginRoute(one));
|
|
1492
|
-
r2.get(
|
|
1519
|
+
r2.get(
|
|
1520
|
+
callbackPath(one.name),
|
|
1521
|
+
SPEC,
|
|
1522
|
+
callbackRoute(one, redirects, finish)
|
|
1523
|
+
);
|
|
1493
1524
|
}
|
|
1494
1525
|
r2.post("/auth/logout", SPEC, async (ctx) => {
|
|
1495
1526
|
const payload = await read(ctx, strategy).catch(() => void 0);
|
|
1496
|
-
|
|
1527
|
+
const id = payload?.sub ?? payload?.user?.id;
|
|
1528
|
+
if (onLogout && id != null) await onLogout(String(id), ctx);
|
|
1497
1529
|
const to = await target(redirects.logout, "/", null, ctx);
|
|
1498
1530
|
if (!inCookie(strategy)) return status(204);
|
|
1499
1531
|
return cookies(NAME, { value: null }).redirect(to);
|
|
@@ -1548,7 +1580,9 @@ function keysOf(issuer, refresh = false) {
|
|
|
1548
1580
|
if (!algorithm) continue;
|
|
1549
1581
|
out.set(
|
|
1550
1582
|
jwk.kid,
|
|
1551
|
-
await crypto.subtle.importKey("jwk", jwk, algorithm, false, [
|
|
1583
|
+
await crypto.subtle.importKey("jwk", jwk, algorithm, false, [
|
|
1584
|
+
"verify"
|
|
1585
|
+
])
|
|
1552
1586
|
);
|
|
1553
1587
|
}
|
|
1554
1588
|
return out;
|
|
@@ -1717,7 +1751,7 @@ function toEntry(auth2) {
|
|
|
1717
1751
|
if ("handler" in auth2) return instanceEntry(auth2);
|
|
1718
1752
|
}
|
|
1719
1753
|
throw new Error(
|
|
1720
|
-
"Invalid `auth`: it takes a string, a function, `{ providers }`, `{ issuer, audience }`, a library instance
|
|
1754
|
+
"Invalid `auth`: it takes a string, a function, `{ providers }`, `{ issuer, audience }`, or a library instance."
|
|
1721
1755
|
);
|
|
1722
1756
|
}
|
|
1723
1757
|
|
|
@@ -2005,7 +2039,10 @@ function config(options = {}) {
|
|
|
2005
2039
|
settings.onError = options.onError || defaultOnError;
|
|
2006
2040
|
settings.onResponse = options.onResponse;
|
|
2007
2041
|
const loc = (v) => typeof v === "string" ? v : "enabled";
|
|
2008
|
-
if (settings.auth)
|
|
2042
|
+
if (settings.auth) {
|
|
2043
|
+
const { name, providers: providers2 } = settings.auth;
|
|
2044
|
+
log.message("auth", `${providers2?.join(",") ?? name} enabled`);
|
|
2045
|
+
}
|
|
2009
2046
|
if (settings.public) log.message("public", loc(options.public));
|
|
2010
2047
|
if (settings.uploads) log.message("uploads", loc(options.uploads));
|
|
2011
2048
|
if (settings.cors) {
|
|
@@ -2109,11 +2146,11 @@ async function assets(ctx) {
|
|
|
2109
2146
|
try {
|
|
2110
2147
|
const key = ctx.url.pathname.replace(/^\/+/, "");
|
|
2111
2148
|
const file2 = ctx.options.public.file(key);
|
|
2149
|
+
const read2 = { signal: ctx.signal };
|
|
2112
2150
|
const info = file2.info?.bind(file2);
|
|
2113
|
-
const meta2 = info ? await info() : null;
|
|
2114
|
-
if (info ? !meta2 : !await file2.exists()) return;
|
|
2115
|
-
const
|
|
2116
|
-
const ctype = mimeOf(ctx.url.pathname) || meta2?.type || ext;
|
|
2151
|
+
const meta2 = info ? await info(read2) : null;
|
|
2152
|
+
if (info ? !meta2 : !await file2.exists(read2)) return;
|
|
2153
|
+
const ctype = mimeOf(ctx.url.pathname) || meta2?.type || void 0;
|
|
2117
2154
|
const headers2 = {
|
|
2118
2155
|
"cache-control": resolveCache(ctx.options.cache) ?? DEFAULT_CACHE
|
|
2119
2156
|
};
|
|
@@ -2142,10 +2179,10 @@ async function assets(ctx) {
|
|
|
2142
2179
|
...headers2,
|
|
2143
2180
|
"content-range": `bytes ${start}-${end}/${meta2.size}`,
|
|
2144
2181
|
"content-length": String(end - start + 1)
|
|
2145
|
-
}).send(file2.slice(start, end + 1).stream());
|
|
2182
|
+
}).send(file2.slice(start, end + 1).stream(read2));
|
|
2146
2183
|
}
|
|
2147
2184
|
}
|
|
2148
|
-
return type(ctype).headers(headers2).send(file2.stream());
|
|
2185
|
+
return type(ctype).headers(headers2).send(file2.stream(read2));
|
|
2149
2186
|
} catch {
|
|
2150
2187
|
}
|
|
2151
2188
|
}
|
|
@@ -2213,7 +2250,10 @@ var generateOpenApiPaths = async (handlers, specPath) => {
|
|
|
2213
2250
|
const schema = await toJsonSchema(meta2.response);
|
|
2214
2251
|
if (schema) {
|
|
2215
2252
|
responses = {
|
|
2216
|
-
200: {
|
|
2253
|
+
200: {
|
|
2254
|
+
description: "OK",
|
|
2255
|
+
content: { "application/json": { schema } }
|
|
2256
|
+
}
|
|
2217
2257
|
};
|
|
2218
2258
|
}
|
|
2219
2259
|
}
|
|
@@ -2354,70 +2394,6 @@ async function socketUser(app, headers2, cookies2) {
|
|
|
2354
2394
|
return app.settings.auth.user(ctx);
|
|
2355
2395
|
}
|
|
2356
2396
|
|
|
2357
|
-
// src/http/cors.ts
|
|
2358
|
-
var localhost = /^https?:\/\/localhost(:\d+)?$/;
|
|
2359
|
-
function cors(config2, origin = "") {
|
|
2360
|
-
origin = origin?.toLowerCase();
|
|
2361
|
-
if (config2 === true) return origin || null;
|
|
2362
|
-
if (config2 === "*") return "*";
|
|
2363
|
-
if (!origin) return null;
|
|
2364
|
-
if (localhost.test(origin)) return origin;
|
|
2365
|
-
const arr = typeof config2 === "string" ? config2.split(/\s*,\s*/g) : [];
|
|
2366
|
-
if (arr.includes(origin)) return origin;
|
|
2367
|
-
console.warn(`CORS: Origin "${origin}" not allowed. Allowed "${config2}"`);
|
|
2368
|
-
return null;
|
|
2369
|
-
}
|
|
2370
|
-
function applyCors(res, ctx) {
|
|
2371
|
-
const settings = ctx.options.cors;
|
|
2372
|
-
if (!settings) return;
|
|
2373
|
-
const requestOrigin = ctx.headers.origin || "";
|
|
2374
|
-
let origin = cors(settings.origin, requestOrigin);
|
|
2375
|
-
if (!origin) return;
|
|
2376
|
-
if (settings.credentials && origin === "*") {
|
|
2377
|
-
if (!requestOrigin) return;
|
|
2378
|
-
origin = requestOrigin.toLowerCase();
|
|
2379
|
-
}
|
|
2380
|
-
res.headers.set("Access-Control-Allow-Origin", origin);
|
|
2381
|
-
res.headers.set("Access-Control-Allow-Methods", settings.methods);
|
|
2382
|
-
res.headers.set("Access-Control-Allow-Headers", settings.headers);
|
|
2383
|
-
if (settings.credentials) {
|
|
2384
|
-
res.headers.set("Access-Control-Allow-Credentials", "true");
|
|
2385
|
-
}
|
|
2386
|
-
if (origin !== "*") res.headers.append("Vary", "Origin");
|
|
2387
|
-
if (ctx.method === "options") {
|
|
2388
|
-
res.headers.set("Access-Control-Max-Age", "86400");
|
|
2389
|
-
}
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
|
-
// src/pipeline/parseResponse.ts
|
|
2393
|
-
async function parseResponse(out, ctx) {
|
|
2394
|
-
if (!out && typeof out !== "string") return null;
|
|
2395
|
-
if (typeof out === "function") {
|
|
2396
|
-
out = await out(ctx);
|
|
2397
|
-
if (!out && typeof out !== "string") return null;
|
|
2398
|
-
}
|
|
2399
|
-
if (typeof out === "number") {
|
|
2400
|
-
return new Response(null, { status: out });
|
|
2401
|
-
}
|
|
2402
|
-
if (!(out instanceof Response) || out.url) {
|
|
2403
|
-
out = await send(out);
|
|
2404
|
-
}
|
|
2405
|
-
return out;
|
|
2406
|
-
}
|
|
2407
|
-
async function finalize(out, ctx) {
|
|
2408
|
-
applyCors(out, ctx);
|
|
2409
|
-
applySecurity(out, ctx);
|
|
2410
|
-
out = await applyCache(out, ctx);
|
|
2411
|
-
const stale = toClear(ctx);
|
|
2412
|
-
if (stale) {
|
|
2413
|
-
out.headers.append("set-cookie", clearCookie(stale));
|
|
2414
|
-
}
|
|
2415
|
-
if (ctx.time?.times?.length > 1) {
|
|
2416
|
-
out.headers.set("Server-Timing", ctx.time.headers());
|
|
2417
|
-
}
|
|
2418
|
-
return out;
|
|
2419
|
-
}
|
|
2420
|
-
|
|
2421
2397
|
// src/body/bodyParts.ts
|
|
2422
2398
|
var asIterable = (s) => s;
|
|
2423
2399
|
function getMatching(string, regex) {
|
|
@@ -2432,8 +2408,6 @@ function isProbablyText(buffer) {
|
|
|
2432
2408
|
}
|
|
2433
2409
|
return true;
|
|
2434
2410
|
}
|
|
2435
|
-
var extByMime = {};
|
|
2436
|
-
for (const ext in mimes_default) extByMime[mimes_default[ext]] = ext;
|
|
2437
2411
|
function addField(body, name, value) {
|
|
2438
2412
|
if (body[name] === void 0) {
|
|
2439
2413
|
body[name] = value;
|
|
@@ -2442,7 +2416,7 @@ function addField(body, name, value) {
|
|
|
2442
2416
|
if (!Array.isArray(body[name])) body[name] = [body[name]];
|
|
2443
2417
|
body[name].push(value);
|
|
2444
2418
|
}
|
|
2445
|
-
function makeFilePart(name, filename, declared, bucket2, limits, budget) {
|
|
2419
|
+
function makeFilePart(name, filename, declared, bucket2, limits, budget, signal) {
|
|
2446
2420
|
return {
|
|
2447
2421
|
kind: "file",
|
|
2448
2422
|
name,
|
|
@@ -2451,13 +2425,14 @@ function makeFilePart(name, filename, declared, bucket2, limits, budget) {
|
|
|
2451
2425
|
bucket: bucket2,
|
|
2452
2426
|
limits,
|
|
2453
2427
|
budget,
|
|
2428
|
+
signal,
|
|
2454
2429
|
head: [],
|
|
2455
2430
|
headSize: 0,
|
|
2456
2431
|
opened: null,
|
|
2457
2432
|
size: 0
|
|
2458
2433
|
};
|
|
2459
2434
|
}
|
|
2460
|
-
function startPart(headerStr, bucket2, limits, budget) {
|
|
2435
|
+
function startPart(headerStr, bucket2, limits, budget, signal) {
|
|
2461
2436
|
const name = getMatching(headerStr, /name="(.+?)"/).trim().replace(/\[\]$/, "");
|
|
2462
2437
|
if (!name) return { kind: "skip" };
|
|
2463
2438
|
const filename = getMatching(headerStr, /filename="(.+?)"/).trim();
|
|
@@ -2470,7 +2445,7 @@ function startPart(headerStr, bucket2, limits, budget) {
|
|
|
2470
2445
|
if (maxFiles != null && budget.files > maxFiles) {
|
|
2471
2446
|
throw errors_default.UPLOAD_TOO_MANY_FILES({ limit: String(maxFiles) });
|
|
2472
2447
|
}
|
|
2473
|
-
return makeFilePart(name, filename, type2, bucket2, limits, budget);
|
|
2448
|
+
return makeFilePart(name, filename, type2, bucket2, limits, budget, signal);
|
|
2474
2449
|
}
|
|
2475
2450
|
async function abortFile(part, error) {
|
|
2476
2451
|
if (part.opened) {
|
|
@@ -2480,8 +2455,6 @@ async function abortFile(part, error) {
|
|
|
2480
2455
|
});
|
|
2481
2456
|
} catch {
|
|
2482
2457
|
}
|
|
2483
|
-
await part.opened.file.remove().catch(() => {
|
|
2484
|
-
});
|
|
2485
2458
|
}
|
|
2486
2459
|
throw error;
|
|
2487
2460
|
}
|
|
@@ -2514,16 +2487,17 @@ function openFile(part) {
|
|
|
2514
2487
|
const sniffed = sniff(head);
|
|
2515
2488
|
const type2 = resolveType(sniffed, part.declared);
|
|
2516
2489
|
validateFile(part.filename, type2, part.limits, sniffed);
|
|
2517
|
-
const ext = sniffed ? extByMime[type2] : void 0;
|
|
2518
|
-
const id = `${createId()}${ext ? `.${ext}` : ""}`;
|
|
2519
2490
|
let controller;
|
|
2520
2491
|
const readable = new ReadableStream({
|
|
2521
2492
|
start(c) {
|
|
2522
2493
|
controller = c;
|
|
2523
2494
|
}
|
|
2524
2495
|
});
|
|
2525
|
-
|
|
2526
|
-
|
|
2496
|
+
part.opened = {
|
|
2497
|
+
type: type2,
|
|
2498
|
+
controller,
|
|
2499
|
+
write: part.bucket.create(readable, { type: type2, signal: part.signal })
|
|
2500
|
+
};
|
|
2527
2501
|
}
|
|
2528
2502
|
async function feedPart(part, data) {
|
|
2529
2503
|
if (data.length === 0) return;
|
|
@@ -2566,10 +2540,10 @@ async function endPart(part, body) {
|
|
|
2566
2540
|
}
|
|
2567
2541
|
const opened = part.opened;
|
|
2568
2542
|
opened.controller.close();
|
|
2569
|
-
await opened.write;
|
|
2543
|
+
const file2 = await opened.write;
|
|
2570
2544
|
const { minSize } = part.limits;
|
|
2571
2545
|
if (minSize != null && part.size < parseBytes(minSize)) {
|
|
2572
|
-
await
|
|
2546
|
+
await file2.remove().catch(() => {
|
|
2573
2547
|
});
|
|
2574
2548
|
throw errors_default.UPLOAD_TOO_SMALL({
|
|
2575
2549
|
name: part.filename,
|
|
@@ -2579,7 +2553,7 @@ async function endPart(part, body) {
|
|
|
2579
2553
|
}
|
|
2580
2554
|
addField(body, part.name, {
|
|
2581
2555
|
name: part.filename,
|
|
2582
|
-
path:
|
|
2556
|
+
path: file2.path,
|
|
2583
2557
|
type: opened.type,
|
|
2584
2558
|
size: part.size
|
|
2585
2559
|
});
|
|
@@ -2599,7 +2573,7 @@ function getBoundary(header) {
|
|
|
2599
2573
|
return null;
|
|
2600
2574
|
}
|
|
2601
2575
|
var BREAK = Buffer.from("\r\n\r\n");
|
|
2602
|
-
async function parseMultipart(stream, boundary, bucket2, limits, max = INF) {
|
|
2576
|
+
async function parseMultipart(stream, boundary, bucket2, limits, max = INF, signal) {
|
|
2603
2577
|
const budget = { used: 0, max: INF, files: 0 };
|
|
2604
2578
|
const delim = Buffer.from(`\r
|
|
2605
2579
|
--${boundary}`);
|
|
@@ -2637,7 +2611,13 @@ async function parseMultipart(stream, boundary, bucket2, limits, max = INF) {
|
|
|
2637
2611
|
} else if (state === "headers") {
|
|
2638
2612
|
const i = buf.indexOf(BREAK);
|
|
2639
2613
|
if (i === -1) break;
|
|
2640
|
-
part = startPart(
|
|
2614
|
+
part = startPart(
|
|
2615
|
+
buf.subarray(0, i).toString("utf-8"),
|
|
2616
|
+
bucket2,
|
|
2617
|
+
limits,
|
|
2618
|
+
budget,
|
|
2619
|
+
signal
|
|
2620
|
+
);
|
|
2641
2621
|
buf = buf.subarray(i + BREAK.length);
|
|
2642
2622
|
state = "body";
|
|
2643
2623
|
advanced = true;
|
|
@@ -2700,12 +2680,16 @@ function parseUrlEncoded(text) {
|
|
|
2700
2680
|
}
|
|
2701
2681
|
return out;
|
|
2702
2682
|
}
|
|
2703
|
-
async function streamRawToBucket(stream, type2, bucket2, limits) {
|
|
2704
|
-
const part = makeFilePart(
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2683
|
+
async function streamRawToBucket(stream, type2, bucket2, limits, signal) {
|
|
2684
|
+
const part = makeFilePart(
|
|
2685
|
+
"body",
|
|
2686
|
+
"upload",
|
|
2687
|
+
type2,
|
|
2688
|
+
bucket2,
|
|
2689
|
+
limits,
|
|
2690
|
+
{ used: 0, max: INF, files: 0 },
|
|
2691
|
+
signal
|
|
2692
|
+
);
|
|
2709
2693
|
for await (const chunk of asIterable(stream)) {
|
|
2710
2694
|
await feedPart(part, Buffer.from(chunk));
|
|
2711
2695
|
}
|
|
@@ -2713,7 +2697,7 @@ async function streamRawToBucket(stream, type2, bucket2, limits) {
|
|
|
2713
2697
|
await endPart(part, body);
|
|
2714
2698
|
return part.size ? body.body : void 0;
|
|
2715
2699
|
}
|
|
2716
|
-
async function parseBody(input, contentType, dest, max = INF, length) {
|
|
2700
|
+
async function parseBody(input, contentType, dest, max = INF, length, signal) {
|
|
2717
2701
|
const type2 = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
2718
2702
|
let bucket2;
|
|
2719
2703
|
let limits = {};
|
|
@@ -2727,7 +2711,14 @@ async function parseBody(input, contentType, dest, max = INF, length) {
|
|
|
2727
2711
|
if (type2 && /multipart\/form-data/i.test(type2)) {
|
|
2728
2712
|
const boundary = getBoundary(type2);
|
|
2729
2713
|
if (!boundary) throw errors_default.BODY_INVALID_MULTIPART();
|
|
2730
|
-
return parseMultipart(
|
|
2714
|
+
return parseMultipart(
|
|
2715
|
+
toStream(input),
|
|
2716
|
+
boundary,
|
|
2717
|
+
bucket2,
|
|
2718
|
+
limits,
|
|
2719
|
+
max,
|
|
2720
|
+
signal
|
|
2721
|
+
);
|
|
2731
2722
|
}
|
|
2732
2723
|
if (!type2 || /^text\//i.test(type2)) {
|
|
2733
2724
|
const buf = await toBuffer(input, max);
|
|
@@ -2745,7 +2736,8 @@ async function parseBody(input, contentType, dest, max = INF, length) {
|
|
|
2745
2736
|
const buf = await toBuffer(input, max);
|
|
2746
2737
|
return buf.length ? buf : void 0;
|
|
2747
2738
|
}
|
|
2748
|
-
if (!bucket2)
|
|
2739
|
+
if (!bucket2)
|
|
2740
|
+
throw errors_default.UPLOAD_NOT_CONFIGURED({ name: "the request body" });
|
|
2749
2741
|
const { maxFileSize } = limits;
|
|
2750
2742
|
if (length != null && maxFileSize != null && length > parseBytes(maxFileSize)) {
|
|
2751
2743
|
throw errors_default.UPLOAD_TOO_LARGE({
|
|
@@ -2754,7 +2746,7 @@ async function parseBody(input, contentType, dest, max = INF, length) {
|
|
|
2754
2746
|
limit: String(maxFileSize)
|
|
2755
2747
|
});
|
|
2756
2748
|
}
|
|
2757
|
-
return streamRawToBucket(toStream(input), type2, bucket2, limits);
|
|
2749
|
+
return streamRawToBucket(toStream(input), type2, bucket2, limits, signal);
|
|
2758
2750
|
}
|
|
2759
2751
|
|
|
2760
2752
|
// src/body/body.ts
|
|
@@ -2796,7 +2788,8 @@ async function resolveBody(ctx, mode = "parse", max = resolveMax(void 0)) {
|
|
|
2796
2788
|
ctx.headers["content-type"],
|
|
2797
2789
|
ctx.options.uploads,
|
|
2798
2790
|
max,
|
|
2799
|
-
Number.isFinite(declared) ? declared : void 0
|
|
2791
|
+
Number.isFinite(declared) ? declared : void 0,
|
|
2792
|
+
ctx.signal
|
|
2800
2793
|
);
|
|
2801
2794
|
if (size && !ctx.headers["content-length"]) {
|
|
2802
2795
|
ctx.headers["content-length"] = String(size);
|
|
@@ -2804,6 +2797,21 @@ async function resolveBody(ctx, mode = "parse", max = resolveMax(void 0)) {
|
|
|
2804
2797
|
return parsed;
|
|
2805
2798
|
}
|
|
2806
2799
|
|
|
2800
|
+
// src/context/isValidMethod.ts
|
|
2801
|
+
var methods = [
|
|
2802
|
+
"get",
|
|
2803
|
+
"post",
|
|
2804
|
+
"put",
|
|
2805
|
+
"patch",
|
|
2806
|
+
"delete",
|
|
2807
|
+
"head",
|
|
2808
|
+
"options",
|
|
2809
|
+
"socket"
|
|
2810
|
+
];
|
|
2811
|
+
function isValidMethod(method) {
|
|
2812
|
+
return methods.includes(method);
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2807
2815
|
// src/util/define.ts
|
|
2808
2816
|
function define(obj, key, cb) {
|
|
2809
2817
|
Object.defineProperty(obj, key, {
|
|
@@ -2820,6 +2828,70 @@ function define(obj, key, cb) {
|
|
|
2820
2828
|
});
|
|
2821
2829
|
}
|
|
2822
2830
|
|
|
2831
|
+
// src/http/cors.ts
|
|
2832
|
+
var localhost = /^https?:\/\/localhost(:\d+)?$/;
|
|
2833
|
+
function cors(config2, origin = "") {
|
|
2834
|
+
origin = origin?.toLowerCase();
|
|
2835
|
+
if (config2 === true) return origin || null;
|
|
2836
|
+
if (config2 === "*") return "*";
|
|
2837
|
+
if (!origin) return null;
|
|
2838
|
+
if (localhost.test(origin)) return origin;
|
|
2839
|
+
const arr = typeof config2 === "string" ? config2.split(/\s*,\s*/g) : [];
|
|
2840
|
+
if (arr.includes(origin)) return origin;
|
|
2841
|
+
console.warn(`CORS: Origin "${origin}" not allowed. Allowed "${config2}"`);
|
|
2842
|
+
return null;
|
|
2843
|
+
}
|
|
2844
|
+
function applyCors(res, ctx) {
|
|
2845
|
+
const settings = ctx.options.cors;
|
|
2846
|
+
if (!settings) return;
|
|
2847
|
+
const requestOrigin = ctx.headers.origin || "";
|
|
2848
|
+
let origin = cors(settings.origin, requestOrigin);
|
|
2849
|
+
if (!origin) return;
|
|
2850
|
+
if (settings.credentials && origin === "*") {
|
|
2851
|
+
if (!requestOrigin) return;
|
|
2852
|
+
origin = requestOrigin.toLowerCase();
|
|
2853
|
+
}
|
|
2854
|
+
res.headers.set("Access-Control-Allow-Origin", origin);
|
|
2855
|
+
res.headers.set("Access-Control-Allow-Methods", settings.methods);
|
|
2856
|
+
res.headers.set("Access-Control-Allow-Headers", settings.headers);
|
|
2857
|
+
if (settings.credentials) {
|
|
2858
|
+
res.headers.set("Access-Control-Allow-Credentials", "true");
|
|
2859
|
+
}
|
|
2860
|
+
if (origin !== "*") res.headers.append("Vary", "Origin");
|
|
2861
|
+
if (ctx.method === "options") {
|
|
2862
|
+
res.headers.set("Access-Control-Max-Age", "86400");
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
// src/pipeline/parseResponse.ts
|
|
2867
|
+
async function parseResponse(out, ctx) {
|
|
2868
|
+
if (!out && typeof out !== "string") return null;
|
|
2869
|
+
if (typeof out === "function") {
|
|
2870
|
+
out = await out(ctx);
|
|
2871
|
+
if (!out && typeof out !== "string") return null;
|
|
2872
|
+
}
|
|
2873
|
+
if (typeof out === "number") {
|
|
2874
|
+
return new Response(null, { status: out });
|
|
2875
|
+
}
|
|
2876
|
+
if (!(out instanceof Response) || out.url) {
|
|
2877
|
+
out = await send(out);
|
|
2878
|
+
}
|
|
2879
|
+
return out;
|
|
2880
|
+
}
|
|
2881
|
+
async function finalize(out, ctx) {
|
|
2882
|
+
applyCors(out, ctx);
|
|
2883
|
+
applySecurity(out, ctx);
|
|
2884
|
+
out = await applyCache(out, ctx);
|
|
2885
|
+
const stale = toClear(ctx);
|
|
2886
|
+
if (stale) {
|
|
2887
|
+
out.headers.append("set-cookie", clearCookie(stale));
|
|
2888
|
+
}
|
|
2889
|
+
if (ctx.time?.times?.length > 1) {
|
|
2890
|
+
out.headers.set("Server-Timing", ctx.time.headers());
|
|
2891
|
+
}
|
|
2892
|
+
return out;
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2823
2895
|
// src/errors/ValidationError.ts
|
|
2824
2896
|
var ValidationError = class extends errors_default {
|
|
2825
2897
|
source;
|
|
@@ -2862,21 +2934,6 @@ function replace2(target2, values) {
|
|
|
2862
2934
|
Object.assign(target2, values);
|
|
2863
2935
|
}
|
|
2864
2936
|
|
|
2865
|
-
// src/context/isValidMethod.ts
|
|
2866
|
-
var methods = [
|
|
2867
|
-
"get",
|
|
2868
|
-
"post",
|
|
2869
|
-
"put",
|
|
2870
|
-
"patch",
|
|
2871
|
-
"delete",
|
|
2872
|
-
"head",
|
|
2873
|
-
"options",
|
|
2874
|
-
"socket"
|
|
2875
|
-
];
|
|
2876
|
-
function isValidMethod(method) {
|
|
2877
|
-
return methods.includes(method);
|
|
2878
|
-
}
|
|
2879
|
-
|
|
2880
2937
|
// src/pipeline/handleRequest.ts
|
|
2881
2938
|
async function handleRequest(app, ctx) {
|
|
2882
2939
|
let res = await getResponse(app, ctx);
|
|
@@ -2943,6 +3000,7 @@ async function getResponse(app, ctx) {
|
|
|
2943
3000
|
if (ctx.platform.provider === "netlify") return;
|
|
2944
3001
|
throw errors_default.NOT_FOUND();
|
|
2945
3002
|
} catch (error) {
|
|
3003
|
+
if (ctx.signal.aborted) return;
|
|
2946
3004
|
return ctx.options.onError(error, ctx);
|
|
2947
3005
|
}
|
|
2948
3006
|
}
|
|
@@ -2984,6 +3042,11 @@ var parseHeaders_default = (raw) => {
|
|
|
2984
3042
|
|
|
2985
3043
|
// src/context/writeResponse.ts
|
|
2986
3044
|
async function writeResponse(out, response) {
|
|
3045
|
+
if (response.destroyed || response.writableEnded) {
|
|
3046
|
+
out.body?.cancel().catch(() => {
|
|
3047
|
+
});
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
2987
3050
|
response.writeHead(out.status || 200, parseHeaders_default(out.headers));
|
|
2988
3051
|
try {
|
|
2989
3052
|
if (out.body instanceof ReadableStream) {
|
|
@@ -3240,7 +3303,14 @@ function forwarded(url, headers2, trustProxy) {
|
|
|
3240
3303
|
}
|
|
3241
3304
|
|
|
3242
3305
|
// src/context/create.ts
|
|
3243
|
-
function createContext(app, {
|
|
3306
|
+
function createContext(app, {
|
|
3307
|
+
method: rawMethod,
|
|
3308
|
+
headers: rawHeaders,
|
|
3309
|
+
url: rawUrl,
|
|
3310
|
+
signal,
|
|
3311
|
+
remoteAddress,
|
|
3312
|
+
source
|
|
3313
|
+
}) {
|
|
3244
3314
|
const init = performance.now();
|
|
3245
3315
|
const method = rawMethod?.toLowerCase() || "get";
|
|
3246
3316
|
const headers2 = parseHeaders_default(rawHeaders);
|
|
@@ -3354,7 +3424,8 @@ var Node = async (app) => {
|
|
|
3354
3424
|
response.end("Server Error");
|
|
3355
3425
|
return;
|
|
3356
3426
|
}
|
|
3357
|
-
await writeResponse(out, response);
|
|
3427
|
+
if (out) await writeResponse(out, response);
|
|
3428
|
+
else if (!response.destroyed) response.destroy();
|
|
3358
3429
|
}
|
|
3359
3430
|
);
|
|
3360
3431
|
await attachWebsocket(server2, app);
|
|
@@ -3384,8 +3455,27 @@ function isSerializable(body) {
|
|
|
3384
3455
|
if (body instanceof URLSearchParams) return false;
|
|
3385
3456
|
return true;
|
|
3386
3457
|
}
|
|
3458
|
+
var deletes = (attrs) => attrs.some((attr) => {
|
|
3459
|
+
const [rawKey, value = ""] = attr.split("=");
|
|
3460
|
+
const key = rawKey.trim().toLowerCase();
|
|
3461
|
+
if (key === "max-age") return Number(value) <= 0;
|
|
3462
|
+
if (key === "expires")
|
|
3463
|
+
return new Date(value.trim()).getTime() <= Date.now();
|
|
3464
|
+
return false;
|
|
3465
|
+
});
|
|
3387
3466
|
function ServerTest(app) {
|
|
3388
3467
|
const port = app.settings.port;
|
|
3468
|
+
const jar = /* @__PURE__ */ new Map();
|
|
3469
|
+
const keep = (res) => {
|
|
3470
|
+
for (const line of res.headers.getSetCookie?.() ?? []) {
|
|
3471
|
+
const [pair, ...attrs] = line.split(";");
|
|
3472
|
+
const eq = pair.indexOf("=");
|
|
3473
|
+
if (eq === -1) continue;
|
|
3474
|
+
const name = pair.slice(0, eq).trim();
|
|
3475
|
+
if (deletes(attrs)) jar.delete(name);
|
|
3476
|
+
else jar.set(name, pair.slice(eq + 1).trim());
|
|
3477
|
+
}
|
|
3478
|
+
};
|
|
3389
3479
|
const fetch2 = async (method, path, options = {}) => {
|
|
3390
3480
|
const headers2 = new Headers(options.headers);
|
|
3391
3481
|
let body = options.body;
|
|
@@ -3393,13 +3483,17 @@ function ServerTest(app) {
|
|
|
3393
3483
|
headers2.set("content-type", "application/json");
|
|
3394
3484
|
body = JSON.stringify(body);
|
|
3395
3485
|
}
|
|
3486
|
+
if (jar.size && !headers2.has("cookie")) {
|
|
3487
|
+
const sent = [...jar].map(([name, value]) => `${name}=${value}`);
|
|
3488
|
+
headers2.set("cookie", sent.join("; "));
|
|
3489
|
+
}
|
|
3396
3490
|
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(path) && !/^https?:\/\//i.test(path)) {
|
|
3397
3491
|
throw new Error(
|
|
3398
3492
|
`Only http(s) URLs can be tested, received "${path}". Pass a path, or the full URL of the host the request should hit.`
|
|
3399
3493
|
);
|
|
3400
3494
|
}
|
|
3401
3495
|
const url = /^https?:\/\//i.test(path) ? path : `http://localhost:${port}${path}`;
|
|
3402
|
-
|
|
3496
|
+
const res = await app.fetch(
|
|
3403
3497
|
new Request(url, {
|
|
3404
3498
|
...options,
|
|
3405
3499
|
method,
|
|
@@ -3407,6 +3501,8 @@ function ServerTest(app) {
|
|
|
3407
3501
|
body
|
|
3408
3502
|
})
|
|
3409
3503
|
);
|
|
3504
|
+
if (res) keep(res);
|
|
3505
|
+
return res;
|
|
3410
3506
|
};
|
|
3411
3507
|
return {
|
|
3412
3508
|
get: (path, options) => fetch2("get", path, options),
|
|
@@ -3415,7 +3511,12 @@ function ServerTest(app) {
|
|
|
3415
3511
|
put: (path, body, options) => fetch2("put", path, { body, ...options }),
|
|
3416
3512
|
patch: (path, body, options) => fetch2("patch", path, { body, ...options }),
|
|
3417
3513
|
delete: (path, options) => fetch2("delete", path, options),
|
|
3418
|
-
options: (path, options) => fetch2("options", path, options)
|
|
3514
|
+
options: (path, options) => fetch2("options", path, options),
|
|
3515
|
+
// The cookies the app has set so far, and a fresh session on demand
|
|
3516
|
+
get cookies() {
|
|
3517
|
+
return Object.fromEntries(jar);
|
|
3518
|
+
},
|
|
3519
|
+
clear: () => jar.clear()
|
|
3419
3520
|
};
|
|
3420
3521
|
}
|
|
3421
3522
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.49.
|
|
3
|
+
"version": "0.49.2",
|
|
4
4
|
"description": "A fully-fledged web server with routing, file uploads, sessions, static files, schema validation, websockets, testing, etc.",
|
|
5
5
|
"homepage": "https://server-js.com/",
|
|
6
6
|
"repository": "github:franciscop/server-next",
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "bunx tsup src/index.ts --format esm --dts --out-dir . --target node24 --external zod --external @valibot/to-json-schema",
|
|
17
|
-
"
|
|
17
|
+
"postinstall": "mkdir -p node_modules/@server && ln -sfn ../.. node_modules/@server/next",
|
|
18
18
|
"start": "bun test --watch",
|
|
19
|
-
"lint": "npx tsc --noEmit && npx @biomejs/biome lint ./src --error-on-warnings",
|
|
20
|
-
"
|
|
21
|
-
"test
|
|
19
|
+
"lint": "npm run prettier && npx tsc --noEmit && npx @biomejs/biome lint ./src --error-on-warnings",
|
|
20
|
+
"prettier": "prettier --write ./src",
|
|
21
|
+
"test": "bun test src demo"
|
|
22
22
|
},
|
|
23
23
|
"main": "index.js",
|
|
24
24
|
"type": "module",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"antarctic": "^0.6.0",
|
|
69
|
-
"bucket": "^0.
|
|
69
|
+
"bucket": "^0.10.1"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@types/bun": "^1.3.0",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"arktype": "^2.2.3",
|
|
76
76
|
"bun": "^1.3.13",
|
|
77
77
|
"check-dts": "^0.8.2",
|
|
78
|
+
"prettier": "^3.9.6",
|
|
78
79
|
"tsup": "^8.5.1",
|
|
79
80
|
"typescript": "^6.0.2",
|
|
80
81
|
"valibot": "^1.4.2",
|
package/readme.md
CHANGED
|
@@ -7,7 +7,7 @@ npm install @server/next
|
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
```js
|
|
10
|
-
import server from
|
|
10
|
+
import server from "@server/next";
|
|
11
11
|
|
|
12
12
|
export default server({ uploads: './uploads' })
|
|
13
13
|
.get('/', () => 'Hello world')
|
|
@@ -15,24 +15,26 @@ export default server({ uploads: './uploads' })
|
|
|
15
15
|
.post('/avatar', (ctx) => ctx.body.avatar.path);
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
File storage comes included, so `uploads` takes a folder path or any bucket. Auth stores nothing of its own: two callbacks put the user wherever you already keep data.
|
|
19
19
|
|
|
20
20
|
```js
|
|
21
|
-
import server, { bucket
|
|
22
|
-
import { createClient } from
|
|
21
|
+
import server, { bucket } from "@server/next";
|
|
22
|
+
import { createClient } from "redis";
|
|
23
23
|
|
|
24
|
-
const redis =
|
|
25
|
-
const uploads = bucket.S3('my-bucket', { id,
|
|
24
|
+
const redis = await createClient({ url }).connect();
|
|
25
|
+
const uploads = bucket.S3('my-bucket', { id, secret });
|
|
26
26
|
|
|
27
27
|
export default server({
|
|
28
28
|
uploads,
|
|
29
29
|
auth: {
|
|
30
|
-
strategy: 'cookie',
|
|
31
30
|
providers: ['github'],
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
onLogin: async (profile) => {
|
|
32
|
+
await redis.set(`user:${profile.id}`, JSON.stringify(profile));
|
|
33
|
+
return profile.id;
|
|
34
|
+
},
|
|
35
|
+
getUser: async (id) => JSON.parse(await redis.get(`user:${id}`)),
|
|
34
36
|
},
|
|
35
37
|
});
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
See the [full documentation](https://
|
|
40
|
+
See the [full documentation](https://server-js.com/documentation).
|
package/src/jsx/jsx-runtime.js
CHANGED
|
@@ -47,18 +47,20 @@ const minifyCss = (str) => {
|
|
|
47
47
|
// squeezed, then put back. Comments go first and unconditionally, so one
|
|
48
48
|
// written inside a string is stripped along with the rest.
|
|
49
49
|
const quoted = [];
|
|
50
|
-
return
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
return (
|
|
51
|
+
str
|
|
52
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
53
|
+
.replace(
|
|
54
|
+
/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g,
|
|
55
|
+
(match) => `\0${quoted.push(match) - 1}\0`,
|
|
56
|
+
)
|
|
57
|
+
.replace(/\s+/g, " ")
|
|
58
|
+
// `+` is left out on purpose for `calc(100% + 10px)`
|
|
59
|
+
.replace(/\s*([{}:;,>~])\s*/g, "$1")
|
|
60
|
+
.replace(/;}/g, "}")
|
|
61
|
+
.replace(/\0(\d+)\0/g, (_, i) => quoted[i])
|
|
62
|
+
.trim()
|
|
63
|
+
);
|
|
62
64
|
};
|
|
63
65
|
|
|
64
66
|
// React element detection (safe for custom objects too)
|