@server/next 0.43.0 → 0.44.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/index.d.ts +26 -23
- package/index.js +99 -144
- package/package.json +5 -3
package/index.d.ts
CHANGED
|
@@ -96,7 +96,7 @@ type RouteSchema = {
|
|
|
96
96
|
description?: string;
|
|
97
97
|
};
|
|
98
98
|
type RouteOptions = {
|
|
99
|
-
schema?: RouteSchema;
|
|
99
|
+
schema?: RouteSchema | false;
|
|
100
100
|
parser?: BodyMode;
|
|
101
101
|
body?: StandardSchemaV1<any, any>;
|
|
102
102
|
query?: StandardSchemaV1<any, any>;
|
|
@@ -259,11 +259,15 @@ type Options = {
|
|
|
259
259
|
sessions?: StoreSource;
|
|
260
260
|
cors?: CorsOptions;
|
|
261
261
|
auth?: AuthOption;
|
|
262
|
-
openapi?:
|
|
262
|
+
openapi?: boolean | string | {
|
|
263
|
+
path?: string;
|
|
264
|
+
title?: string;
|
|
265
|
+
description?: string;
|
|
266
|
+
version?: string;
|
|
267
|
+
};
|
|
263
268
|
onError?: OnError;
|
|
264
269
|
onResponse?: OnResponse;
|
|
265
270
|
log?: LogLevel | boolean;
|
|
266
|
-
favicon?: string | BucketFile;
|
|
267
271
|
security?: boolean | SecurityOptions;
|
|
268
272
|
parser?: BodyMode;
|
|
269
273
|
cache?: CacheOption;
|
|
@@ -279,11 +283,15 @@ type Settings = {
|
|
|
279
283
|
sessionsDefault?: boolean;
|
|
280
284
|
cors?: CorsSettings;
|
|
281
285
|
auth?: AuthSettings;
|
|
282
|
-
openapi?:
|
|
286
|
+
openapi?: {
|
|
287
|
+
path: string;
|
|
288
|
+
title?: string;
|
|
289
|
+
description?: string;
|
|
290
|
+
version?: string;
|
|
291
|
+
};
|
|
283
292
|
onError?: OnError;
|
|
284
293
|
onResponse?: OnResponse;
|
|
285
294
|
log: Logger;
|
|
286
|
-
favicon?: string | BucketFile;
|
|
287
295
|
security: SecuritySettings;
|
|
288
296
|
parser: BodyMode;
|
|
289
297
|
cache?: CacheOption;
|
|
@@ -319,9 +327,9 @@ interface ContextExtension {
|
|
|
319
327
|
type Context<C extends ContextTypes = {}> = {
|
|
320
328
|
method: Method;
|
|
321
329
|
ip: string;
|
|
330
|
+
signal: AbortSignal;
|
|
322
331
|
headers: Record<string, string | string[]>;
|
|
323
332
|
cookies: Record<string, string>;
|
|
324
|
-
body?: Field<C, "body", SerializableValue | Buffer | ReadableStream>;
|
|
325
333
|
url: URL & {
|
|
326
334
|
params: Field<C, "params", Record<string, any>>;
|
|
327
335
|
query: Field<C, "query", Record<string, any>>;
|
|
@@ -334,12 +342,12 @@ type Context<C extends ContextTypes = {}> = {
|
|
|
334
342
|
session: Field<C, "session", Record<string, any>>;
|
|
335
343
|
user?: Field<C, "user", Record<string, any>>;
|
|
336
344
|
init: number;
|
|
337
|
-
req?: Request;
|
|
338
|
-
res?: Response & {
|
|
339
|
-
cookies?: Record<string, string>;
|
|
340
|
-
};
|
|
341
345
|
app: Server;
|
|
342
|
-
} &
|
|
346
|
+
} & ("body" extends keyof C ? {
|
|
347
|
+
body: Field<C, "body", never>;
|
|
348
|
+
} : {
|
|
349
|
+
body?: SerializableValue | Buffer | ReadableStream;
|
|
350
|
+
}) & ContextExtension;
|
|
343
351
|
type InlineReply = Response | Reply | BucketFile | {
|
|
344
352
|
body: string;
|
|
345
353
|
headers?: Headers;
|
|
@@ -434,11 +442,6 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
|
|
|
434
442
|
platform: Platform;
|
|
435
443
|
sockets: any[];
|
|
436
444
|
websocket: any;
|
|
437
|
-
faviconCache?: {
|
|
438
|
-
bytes: Buffer;
|
|
439
|
-
type: string;
|
|
440
|
-
etag: string;
|
|
441
|
-
} | null;
|
|
442
445
|
port?: number;
|
|
443
446
|
constructor(options?: Options);
|
|
444
447
|
self(): this;
|
|
@@ -448,6 +451,7 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
|
|
|
448
451
|
test(): {
|
|
449
452
|
get: (path: string, options?: {
|
|
450
453
|
method?: string;
|
|
454
|
+
signal?: AbortSignal | null;
|
|
451
455
|
headers?: HeadersInit;
|
|
452
456
|
cache?: RequestCache;
|
|
453
457
|
redirect?: RequestRedirect;
|
|
@@ -458,11 +462,11 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
|
|
|
458
462
|
priority?: RequestPriority;
|
|
459
463
|
referrer?: string;
|
|
460
464
|
referrerPolicy?: ReferrerPolicy;
|
|
461
|
-
signal?: AbortSignal | null;
|
|
462
465
|
window?: null;
|
|
463
466
|
}) => Promise<Response>;
|
|
464
467
|
head: (path: string, options?: {
|
|
465
468
|
method?: string;
|
|
469
|
+
signal?: AbortSignal | null;
|
|
466
470
|
headers?: HeadersInit;
|
|
467
471
|
cache?: RequestCache;
|
|
468
472
|
redirect?: RequestRedirect;
|
|
@@ -473,13 +477,13 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
|
|
|
473
477
|
priority?: RequestPriority;
|
|
474
478
|
referrer?: string;
|
|
475
479
|
referrerPolicy?: ReferrerPolicy;
|
|
476
|
-
signal?: AbortSignal | null;
|
|
477
480
|
window?: null;
|
|
478
481
|
}) => Promise<Response>;
|
|
479
482
|
post: (path: string, body?: string | number | boolean | ArrayBuffer | {
|
|
480
483
|
[key: string]: SerializableValue;
|
|
481
484
|
} | SerializableValue[] | ReadableStream<any> | Blob | ArrayBufferView<ArrayBuffer> | FormData | URLSearchParams, options?: {
|
|
482
485
|
method?: string;
|
|
486
|
+
signal?: AbortSignal | null;
|
|
483
487
|
headers?: HeadersInit;
|
|
484
488
|
cache?: RequestCache;
|
|
485
489
|
redirect?: RequestRedirect;
|
|
@@ -490,13 +494,13 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
|
|
|
490
494
|
priority?: RequestPriority;
|
|
491
495
|
referrer?: string;
|
|
492
496
|
referrerPolicy?: ReferrerPolicy;
|
|
493
|
-
signal?: AbortSignal | null;
|
|
494
497
|
window?: null;
|
|
495
498
|
}) => Promise<Response>;
|
|
496
499
|
put: (path: string, body?: string | number | boolean | ArrayBuffer | {
|
|
497
500
|
[key: string]: SerializableValue;
|
|
498
501
|
} | SerializableValue[] | ReadableStream<any> | Blob | ArrayBufferView<ArrayBuffer> | FormData | URLSearchParams, options?: {
|
|
499
502
|
method?: string;
|
|
503
|
+
signal?: AbortSignal | null;
|
|
500
504
|
headers?: HeadersInit;
|
|
501
505
|
cache?: RequestCache;
|
|
502
506
|
redirect?: RequestRedirect;
|
|
@@ -507,13 +511,13 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
|
|
|
507
511
|
priority?: RequestPriority;
|
|
508
512
|
referrer?: string;
|
|
509
513
|
referrerPolicy?: ReferrerPolicy;
|
|
510
|
-
signal?: AbortSignal | null;
|
|
511
514
|
window?: null;
|
|
512
515
|
}) => Promise<Response>;
|
|
513
516
|
patch: (path: string, body?: string | number | boolean | ArrayBuffer | {
|
|
514
517
|
[key: string]: SerializableValue;
|
|
515
518
|
} | SerializableValue[] | ReadableStream<any> | Blob | ArrayBufferView<ArrayBuffer> | FormData | URLSearchParams, options?: {
|
|
516
519
|
method?: string;
|
|
520
|
+
signal?: AbortSignal | null;
|
|
517
521
|
headers?: HeadersInit;
|
|
518
522
|
cache?: RequestCache;
|
|
519
523
|
redirect?: RequestRedirect;
|
|
@@ -524,11 +528,11 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
|
|
|
524
528
|
priority?: RequestPriority;
|
|
525
529
|
referrer?: string;
|
|
526
530
|
referrerPolicy?: ReferrerPolicy;
|
|
527
|
-
signal?: AbortSignal | null;
|
|
528
531
|
window?: null;
|
|
529
532
|
}) => Promise<Response>;
|
|
530
533
|
delete: (path: string, options?: {
|
|
531
534
|
method?: string;
|
|
535
|
+
signal?: AbortSignal | null;
|
|
532
536
|
headers?: HeadersInit;
|
|
533
537
|
cache?: RequestCache;
|
|
534
538
|
redirect?: RequestRedirect;
|
|
@@ -539,11 +543,11 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
|
|
|
539
543
|
priority?: RequestPriority;
|
|
540
544
|
referrer?: string;
|
|
541
545
|
referrerPolicy?: ReferrerPolicy;
|
|
542
|
-
signal?: AbortSignal | null;
|
|
543
546
|
window?: null;
|
|
544
547
|
}) => Promise<Response>;
|
|
545
548
|
options: (path: string, options?: {
|
|
546
549
|
method?: string;
|
|
550
|
+
signal?: AbortSignal | null;
|
|
547
551
|
headers?: HeadersInit;
|
|
548
552
|
cache?: RequestCache;
|
|
549
553
|
redirect?: RequestRedirect;
|
|
@@ -554,7 +558,6 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
|
|
|
554
558
|
priority?: RequestPriority;
|
|
555
559
|
referrer?: string;
|
|
556
560
|
referrerPolicy?: ReferrerPolicy;
|
|
557
|
-
signal?: AbortSignal | null;
|
|
558
561
|
window?: null;
|
|
559
562
|
}) => Promise<Response>;
|
|
560
563
|
};
|
package/index.js
CHANGED
|
@@ -186,8 +186,8 @@ function getExt(filename) {
|
|
|
186
186
|
return filename.slice(i).toLowerCase();
|
|
187
187
|
}
|
|
188
188
|
async function saveFileToBucket(originalName, data, bucket2, contentType) {
|
|
189
|
-
const
|
|
190
|
-
const id = `${createId()}${
|
|
189
|
+
const ext = getExt(originalName);
|
|
190
|
+
const id = `${createId()}${ext}`;
|
|
191
191
|
const file2 = bucket2.file(id);
|
|
192
192
|
await file2.write(data, { type: contentType });
|
|
193
193
|
return {
|
|
@@ -210,10 +210,10 @@ function validateFile(originalName, data, contentType, limits) {
|
|
|
210
210
|
);
|
|
211
211
|
}
|
|
212
212
|
if (fileType2 && fileType2.length > 0) {
|
|
213
|
-
const
|
|
213
|
+
const ext = getExt(originalName);
|
|
214
214
|
const mime = contentType.toLowerCase();
|
|
215
215
|
const allowed = fileType2.some(
|
|
216
|
-
(t) => t.toLowerCase() === mime || t.toLowerCase() ===
|
|
216
|
+
(t) => t.toLowerCase() === mime || t.toLowerCase() === ext
|
|
217
217
|
);
|
|
218
218
|
if (!allowed) {
|
|
219
219
|
throw new Error(
|
|
@@ -353,11 +353,11 @@ function isProbablyText(buffer) {
|
|
|
353
353
|
return true;
|
|
354
354
|
}
|
|
355
355
|
var extByMime = {};
|
|
356
|
-
for (const
|
|
356
|
+
for (const ext in mimes_default) extByMime[mimes_default[ext]] = ext;
|
|
357
357
|
function extFromType(type2) {
|
|
358
358
|
const base = (type2 || "").split(";")[0].trim().toLowerCase();
|
|
359
|
-
const
|
|
360
|
-
if (
|
|
359
|
+
const ext = extByMime[base];
|
|
360
|
+
if (ext) return `.${ext}`;
|
|
361
361
|
const sub = base.split("/")[1];
|
|
362
362
|
return sub && /^[a-z0-9]+$/.test(sub) ? `.${sub}` : ".bin";
|
|
363
363
|
}
|
|
@@ -770,20 +770,20 @@ var encodeExt = (name) => encodeURIComponent(name).replace(
|
|
|
770
770
|
);
|
|
771
771
|
function disposition(name) {
|
|
772
772
|
if (!name) return "attachment";
|
|
773
|
-
const
|
|
774
|
-
if (!
|
|
775
|
-
const ascii =
|
|
773
|
+
const clean2 = name.replace(/[\r\n]/g, "").split(/[\\/]/).pop() || "";
|
|
774
|
+
if (!clean2) return "attachment";
|
|
775
|
+
const ascii = clean2.replace(/[^\x20-\x7e]/g, "?");
|
|
776
776
|
const value = `attachment; filename="${ascii.replace(/["\\]/g, "\\$&")}"`;
|
|
777
|
-
if (
|
|
778
|
-
return `${value}; filename*=UTF-8''${encodeExt(
|
|
777
|
+
if (clean2 === ascii) return value;
|
|
778
|
+
return `${value}; filename*=UTF-8''${encodeExt(clean2)}`;
|
|
779
779
|
}
|
|
780
780
|
|
|
781
781
|
// src/helpers/fileType.ts
|
|
782
782
|
function fileType(file2) {
|
|
783
783
|
if (file2.type) return file2.type;
|
|
784
784
|
const name = file2.path || file2.name || "";
|
|
785
|
-
const
|
|
786
|
-
return
|
|
785
|
+
const ext = name.split(".").pop()?.toLowerCase();
|
|
786
|
+
return ext ? mimes_default[ext] : void 0;
|
|
787
787
|
}
|
|
788
788
|
|
|
789
789
|
// src/helpers/isHtml.ts
|
|
@@ -817,8 +817,8 @@ var Reply = class {
|
|
|
817
817
|
return this;
|
|
818
818
|
}
|
|
819
819
|
download(name) {
|
|
820
|
-
const
|
|
821
|
-
if (
|
|
820
|
+
const ext = name?.split(".").pop();
|
|
821
|
+
if (ext && !this.res.headers.get("content-type")) this.type(ext);
|
|
822
822
|
return this.headers("content-disposition", disposition(name));
|
|
823
823
|
}
|
|
824
824
|
headers(key, value) {
|
|
@@ -876,10 +876,10 @@ var Reply = class {
|
|
|
876
876
|
if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(path)) return this.status(404).send();
|
|
877
877
|
try {
|
|
878
878
|
const fs = await import("fs");
|
|
879
|
-
const
|
|
879
|
+
const ext = path.split(".").pop();
|
|
880
880
|
await fs.promises.access(path);
|
|
881
881
|
const stream = fs.createReadStream(path);
|
|
882
|
-
return this.type(
|
|
882
|
+
return this.type(ext).send(stream);
|
|
883
883
|
} catch (error) {
|
|
884
884
|
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
|
885
885
|
return this.status(404).send();
|
|
@@ -1884,8 +1884,6 @@ function config(options = {}) {
|
|
|
1884
1884
|
const publicDir = options.public || env2.PUBLIC;
|
|
1885
1885
|
settings.public = publicDir ? bucket(publicDir) : null;
|
|
1886
1886
|
settings.uploads = resolveUploads(options.uploads);
|
|
1887
|
-
const favicon2 = options.favicon || env2.FAVICON;
|
|
1888
|
-
if (favicon2) settings.favicon = favicon2;
|
|
1889
1887
|
const production = env2.NODE_ENV === "production";
|
|
1890
1888
|
const defaulted = options.sessions == null;
|
|
1891
1889
|
settings.sessionsDefault = defaulted;
|
|
@@ -1913,9 +1911,10 @@ function config(options = {}) {
|
|
|
1913
1911
|
);
|
|
1914
1912
|
}
|
|
1915
1913
|
if (options.openapi) {
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
}
|
|
1914
|
+
const o = options.openapi;
|
|
1915
|
+
if (o === true) settings.openapi = { path: "/openapi.json" };
|
|
1916
|
+
else if (typeof o === "string") settings.openapi = { path: o };
|
|
1917
|
+
else settings.openapi = { path: "/openapi.json", ...o };
|
|
1919
1918
|
}
|
|
1920
1919
|
settings.onError = options.onError || ((error) => {
|
|
1921
1920
|
return new Response(error.message || "Server Error", {
|
|
@@ -1934,9 +1933,8 @@ function config(options = {}) {
|
|
|
1934
1933
|
const origin = settings.cors.origin === true ? "*" : String(settings.cors.origin);
|
|
1935
1934
|
log.message("cors", origin);
|
|
1936
1935
|
}
|
|
1937
|
-
if (settings.favicon) log.message("favicon", loc(settings.favicon));
|
|
1938
1936
|
if (settings.cache !== void 0) log.message("cache", loc(options.cache));
|
|
1939
|
-
if (settings.openapi) log.message("openapi", settings.openapi.path
|
|
1937
|
+
if (settings.openapi) log.message("openapi", settings.openapi.path);
|
|
1940
1938
|
return settings;
|
|
1941
1939
|
}
|
|
1942
1940
|
|
|
@@ -2136,11 +2134,6 @@ async function parseResponse(out, ctx) {
|
|
|
2136
2134
|
}
|
|
2137
2135
|
ctx.options.sessions.set(id, ctx.session);
|
|
2138
2136
|
}
|
|
2139
|
-
if (ctx?.res?.headers) {
|
|
2140
|
-
for (const key in ctx.res.headers) {
|
|
2141
|
-
out.headers[key] = ctx.res.headers[key];
|
|
2142
|
-
}
|
|
2143
|
-
}
|
|
2144
2137
|
return out;
|
|
2145
2138
|
}
|
|
2146
2139
|
|
|
@@ -2239,12 +2232,18 @@ async function handleRequest(app, ctx) {
|
|
|
2239
2232
|
if (replaced) res = replaced;
|
|
2240
2233
|
}
|
|
2241
2234
|
if (res) ctx.options.log.request(ctx, res);
|
|
2235
|
+
if (res?.body && ctx.method === "head") {
|
|
2236
|
+
res.body.cancel().catch(() => {
|
|
2237
|
+
});
|
|
2238
|
+
res = new Response(null, { status: res.status, headers: res.headers });
|
|
2239
|
+
}
|
|
2242
2240
|
return res;
|
|
2243
2241
|
}
|
|
2244
2242
|
async function getResponse(app, ctx) {
|
|
2245
2243
|
try {
|
|
2246
2244
|
let matched = false;
|
|
2247
|
-
|
|
2245
|
+
const routes = ctx.method === "head" ? [...app.handlers.head, ...app.handlers.get] : app.handlers[ctx.method];
|
|
2246
|
+
for (const route of routes) {
|
|
2248
2247
|
const params = pathPattern(route.path, ctx.url.pathname || "/");
|
|
2249
2248
|
if (!params) continue;
|
|
2250
2249
|
matched = true;
|
|
@@ -2532,31 +2531,32 @@ function auth(app) {
|
|
|
2532
2531
|
app.use(async function middle(ctx) {
|
|
2533
2532
|
ctx.user = await getUser(ctx);
|
|
2534
2533
|
});
|
|
2535
|
-
|
|
2534
|
+
const spec = { schema: { tags: "auth" } };
|
|
2535
|
+
app.post("/auth/logout", spec, logout);
|
|
2536
2536
|
const enabled = app.settings.auth.providers;
|
|
2537
2537
|
for (const name of oauth2) {
|
|
2538
2538
|
if (!enabled.includes(name)) continue;
|
|
2539
2539
|
const key = name.toUpperCase();
|
|
2540
2540
|
if (!env[`${key}_ID`]) throw new Error(`${key}_ID not defined`);
|
|
2541
2541
|
if (!env[`${key}_SECRET`]) throw new Error(`${key}_SECRET not defined`);
|
|
2542
|
-
app.get(`/auth/login/${name}`, providers_default[name].login);
|
|
2543
|
-
app.get(`/auth/callback/${name}`, providers_default[name].callback);
|
|
2544
|
-
app.post(`/auth/verify/${name}`, providers_default[name].verify);
|
|
2542
|
+
app.get(`/auth/login/${name}`, spec, providers_default[name].login);
|
|
2543
|
+
app.get(`/auth/callback/${name}`, spec, providers_default[name].callback);
|
|
2544
|
+
app.post(`/auth/verify/${name}`, spec, providers_default[name].verify);
|
|
2545
2545
|
}
|
|
2546
2546
|
if (enabled.includes("apple")) {
|
|
2547
2547
|
const keys = ["APPLE_ID", "APPLE_TEAM_ID", "APPLE_KEY_ID", "APPLE_PRIVATE_KEY"];
|
|
2548
2548
|
for (const key of keys) {
|
|
2549
2549
|
if (!env[key]) throw new Error(`${key} not defined`);
|
|
2550
2550
|
}
|
|
2551
|
-
app.get("/auth/login/apple", providers_default.apple.login);
|
|
2552
|
-
app.post("/auth/callback/apple", providers_default.apple.callback);
|
|
2553
|
-
app.post("/auth/verify/apple", providers_default.apple.verify);
|
|
2551
|
+
app.get("/auth/login/apple", spec, providers_default.apple.login);
|
|
2552
|
+
app.post("/auth/callback/apple", spec, providers_default.apple.callback);
|
|
2553
|
+
app.post("/auth/verify/apple", spec, providers_default.apple.verify);
|
|
2554
2554
|
}
|
|
2555
2555
|
if (enabled.includes("email")) {
|
|
2556
|
-
app.post("/auth/register/email", providers_default.email.register);
|
|
2557
|
-
app.post("/auth/login/email", providers_default.email.login);
|
|
2558
|
-
app.put("/auth/password/email", providers_default.email.password);
|
|
2559
|
-
app.put("/auth/reset/email", providers_default.email.reset);
|
|
2556
|
+
app.post("/auth/register/email", spec, providers_default.email.register);
|
|
2557
|
+
app.post("/auth/login/email", spec, providers_default.email.login);
|
|
2558
|
+
app.put("/auth/password/email", spec, providers_default.email.password);
|
|
2559
|
+
app.put("/auth/reset/email", spec, providers_default.email.reset);
|
|
2560
2560
|
}
|
|
2561
2561
|
}
|
|
2562
2562
|
|
|
@@ -2587,7 +2587,7 @@ function parseRange(header, size) {
|
|
|
2587
2587
|
var CACHE_CONTROL = "public, max-age=3600";
|
|
2588
2588
|
async function assets(ctx) {
|
|
2589
2589
|
if (!ctx.options.public) return;
|
|
2590
|
-
if (ctx.method !== "get") return;
|
|
2590
|
+
if (ctx.method !== "get" && ctx.method !== "head") return;
|
|
2591
2591
|
if (ctx.url.pathname === "/") return;
|
|
2592
2592
|
try {
|
|
2593
2593
|
const key = ctx.url.pathname.replace(/^\/+/, "");
|
|
@@ -2595,8 +2595,8 @@ async function assets(ctx) {
|
|
|
2595
2595
|
const info = file2.info?.bind(file2);
|
|
2596
2596
|
const meta = info ? await info() : null;
|
|
2597
2597
|
if (info ? !meta : !await file2.exists()) return;
|
|
2598
|
-
const
|
|
2599
|
-
const ctype =
|
|
2598
|
+
const ext = ctx.url.pathname.split(".").pop()?.toLowerCase();
|
|
2599
|
+
const ctype = ext && mimes_default[ext] || meta?.type || ext;
|
|
2600
2600
|
const headers2 = { "cache-control": CACHE_CONTROL };
|
|
2601
2601
|
let tag;
|
|
2602
2602
|
if (meta) {
|
|
@@ -2631,46 +2631,8 @@ async function assets(ctx) {
|
|
|
2631
2631
|
}
|
|
2632
2632
|
}
|
|
2633
2633
|
|
|
2634
|
-
// src/middle/favicon.ts
|
|
2635
|
-
var CACHE_CONTROL2 = "public, max-age=86400";
|
|
2636
|
-
var ext = (name) => name.split(".").pop() || "ico";
|
|
2637
|
-
async function loadFavicon(fav) {
|
|
2638
|
-
try {
|
|
2639
|
-
const type2 = ext(typeof fav === "string" ? fav : fav?.name);
|
|
2640
|
-
const bytes = typeof fav === "string" ? await (await import("fs/promises")).readFile(fav) : Buffer.from(await fav.bytes());
|
|
2641
|
-
return { bytes, type: type2, etag: etag(bytes) };
|
|
2642
|
-
} catch {
|
|
2643
|
-
return null;
|
|
2644
|
-
}
|
|
2645
|
-
}
|
|
2646
|
-
async function favicon(ctx) {
|
|
2647
|
-
const fav = ctx.options.favicon;
|
|
2648
|
-
if (!fav) return;
|
|
2649
|
-
if (ctx.app.faviconCache === void 0) {
|
|
2650
|
-
ctx.app.faviconCache = await loadFavicon(fav);
|
|
2651
|
-
}
|
|
2652
|
-
const entry = ctx.app.faviconCache;
|
|
2653
|
-
if (!entry) return 204;
|
|
2654
|
-
const headers2 = { "cache-control": CACHE_CONTROL2, etag: entry.etag };
|
|
2655
|
-
if (ctx.headers["if-none-match"] === entry.etag) {
|
|
2656
|
-
return status(304).headers(headers2).send();
|
|
2657
|
-
}
|
|
2658
|
-
return type(entry.type).headers(headers2).send(entry.bytes);
|
|
2659
|
-
}
|
|
2660
|
-
|
|
2661
2634
|
// src/middle/openapi.ts
|
|
2662
2635
|
import * as fsp from "fs/promises";
|
|
2663
|
-
var entities = {
|
|
2664
|
-
"&": "&",
|
|
2665
|
-
"<": "<",
|
|
2666
|
-
">": ">",
|
|
2667
|
-
'"': """
|
|
2668
|
-
};
|
|
2669
|
-
var encode = (str = "") => {
|
|
2670
|
-
if (typeof str === "number") str = String(str);
|
|
2671
|
-
if (typeof str !== "string") return "";
|
|
2672
|
-
return str.replace(/[&<>"]/g, (tag) => entities[tag]);
|
|
2673
|
-
};
|
|
2674
2636
|
var getConfig = (options = {}) => {
|
|
2675
2637
|
const config2 = { ...options };
|
|
2676
2638
|
if (config2.tags) {
|
|
@@ -2684,6 +2646,25 @@ var getConfig = (options = {}) => {
|
|
|
2684
2646
|
}
|
|
2685
2647
|
return config2;
|
|
2686
2648
|
};
|
|
2649
|
+
var clean = ({ $schema, ...schema }) => schema;
|
|
2650
|
+
async function toJsonSchema(schema) {
|
|
2651
|
+
try {
|
|
2652
|
+
if (typeof schema?.toJsonSchema === "function") {
|
|
2653
|
+
return clean(schema.toJsonSchema());
|
|
2654
|
+
}
|
|
2655
|
+
const vendor = schema?.["~standard"]?.vendor;
|
|
2656
|
+
if (vendor === "zod") {
|
|
2657
|
+
const mod = await import("zod");
|
|
2658
|
+
return clean((mod.toJSONSchema ?? mod.z.toJSONSchema)(schema));
|
|
2659
|
+
}
|
|
2660
|
+
if (vendor === "valibot") {
|
|
2661
|
+
const mod = await import("@valibot/to-json-schema");
|
|
2662
|
+
return clean(mod.toJsonSchema(schema));
|
|
2663
|
+
}
|
|
2664
|
+
} catch {
|
|
2665
|
+
}
|
|
2666
|
+
return zodToSchema(schema);
|
|
2667
|
+
}
|
|
2687
2668
|
function zodToSchema(schema) {
|
|
2688
2669
|
const type2 = schema?.def?.type || "string";
|
|
2689
2670
|
if (type2 === "object") {
|
|
@@ -2706,48 +2687,31 @@ function zodToSchema(schema) {
|
|
|
2706
2687
|
return { type: type2 };
|
|
2707
2688
|
}
|
|
2708
2689
|
var pkgProm = fsp.readFile("package.json", "utf-8").then((data) => JSON.parse(data)).catch(() => ({}));
|
|
2709
|
-
var
|
|
2710
|
-
const found = fn.toString().split("\n").filter((l) => /\s+\/\/\s/.test(l)).map((l) => l.trim().replace("// ", "")).find((l) => l.startsWith(name));
|
|
2711
|
-
if (!found) return "";
|
|
2712
|
-
return encode(found.replace(name, "").trim());
|
|
2713
|
-
};
|
|
2714
|
-
var getDescription = (fn) => getTag("@description", fn) || "";
|
|
2715
|
-
var getReturn = (fn) => getTag("@returns", fn) || "OK";
|
|
2716
|
-
var generateOpenApiPaths = (handlers) => {
|
|
2690
|
+
var generateOpenApiPaths = async (handlers, specPath) => {
|
|
2717
2691
|
const paths = {};
|
|
2718
2692
|
for (const [method, routes] of Object.entries(handlers)) {
|
|
2719
2693
|
for (const route of routes) {
|
|
2720
2694
|
const path = route.path;
|
|
2721
|
-
const fn = route.fns.find((p) => typeof p === "function");
|
|
2722
2695
|
const meta = route.options ?? {};
|
|
2723
2696
|
const config2 = getConfig(route.options?.schema);
|
|
2724
|
-
if (typeof path !== "string" || path === "*" || path ===
|
|
2697
|
+
if (typeof path !== "string" || path === "*" || path === specPath) {
|
|
2725
2698
|
continue;
|
|
2726
2699
|
}
|
|
2700
|
+
if (route.options?.schema === false) continue;
|
|
2727
2701
|
const normalizedPath = path.replace(/\(\w+\)/gi, "").replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
|
|
2728
2702
|
if (!paths[normalizedPath]) {
|
|
2729
2703
|
paths[normalizedPath] = {};
|
|
2730
2704
|
}
|
|
2731
|
-
const getTitle = (fn2) => {
|
|
2732
|
-
if (!fn2.name) return null;
|
|
2733
|
-
const wrongNames = ["default"];
|
|
2734
|
-
if (wrongNames.includes(fn2.name)) return null;
|
|
2735
|
-
if (fn2.name.length <= 3) return null;
|
|
2736
|
-
if (fn2.name.includes("_")) return fn2.name.replace(/_/g, " ");
|
|
2737
|
-
const name = fn2.name.split(/(?=[A-Z])/).join(" ").toLowerCase();
|
|
2738
|
-
return name[0].toUpperCase() + name.slice(1);
|
|
2739
|
-
};
|
|
2740
2705
|
let requestBody;
|
|
2741
2706
|
if (meta?.body) {
|
|
2742
|
-
const schema =
|
|
2707
|
+
const schema = await toJsonSchema(meta.body);
|
|
2743
2708
|
requestBody = { content: { "application/json": { schema } } };
|
|
2744
2709
|
}
|
|
2745
2710
|
let responses;
|
|
2746
2711
|
if (meta?.response) {
|
|
2747
|
-
const schema =
|
|
2748
|
-
const description = getReturn(fn);
|
|
2712
|
+
const schema = await toJsonSchema(meta.response);
|
|
2749
2713
|
responses = {
|
|
2750
|
-
200: { description, content: { "application/json": { schema } } }
|
|
2714
|
+
200: { description: "OK", content: { "application/json": { schema } } }
|
|
2751
2715
|
};
|
|
2752
2716
|
}
|
|
2753
2717
|
const parameters = [];
|
|
@@ -2762,18 +2726,20 @@ var generateOpenApiPaths = (handlers) => {
|
|
|
2762
2726
|
});
|
|
2763
2727
|
});
|
|
2764
2728
|
if (meta?.query) {
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2729
|
+
const schema = await toJsonSchema(meta.query);
|
|
2730
|
+
for (const [name, prop] of Object.entries(schema.properties ?? {})) {
|
|
2731
|
+
parameters.push({
|
|
2732
|
+
name,
|
|
2733
|
+
in: "query",
|
|
2734
|
+
required: schema.required?.includes(name) ?? false,
|
|
2735
|
+
schema: prop
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2772
2738
|
}
|
|
2773
2739
|
paths[normalizedPath][method] = {
|
|
2774
2740
|
tags: config2.tags,
|
|
2775
|
-
summary: config2.title
|
|
2776
|
-
description: config2.description
|
|
2741
|
+
summary: config2.title,
|
|
2742
|
+
description: config2.description,
|
|
2777
2743
|
requestBody,
|
|
2778
2744
|
parameters,
|
|
2779
2745
|
responses
|
|
@@ -2784,34 +2750,21 @@ var generateOpenApiPaths = (handlers) => {
|
|
|
2784
2750
|
};
|
|
2785
2751
|
var openapi_default = async (ctx) => {
|
|
2786
2752
|
const pkg = await pkgProm;
|
|
2753
|
+
const { title, description, version } = ctx.options.openapi ?? {};
|
|
2787
2754
|
const domain = pkg.homepage || ctx.url.origin;
|
|
2788
|
-
|
|
2755
|
+
return {
|
|
2789
2756
|
openapi: "3.0.0",
|
|
2790
2757
|
info: {
|
|
2791
|
-
title: pkg.name || "API Documentation",
|
|
2792
|
-
version: pkg.version || "1.0.0",
|
|
2793
|
-
description: pkg.description || ""
|
|
2758
|
+
title: title || pkg.name || "API Documentation",
|
|
2759
|
+
version: version || pkg.version || "1.0.0",
|
|
2760
|
+
description: description ?? (pkg.description || "")
|
|
2794
2761
|
},
|
|
2795
2762
|
servers: domain ? [{ url: domain }] : [],
|
|
2796
|
-
paths: generateOpenApiPaths(
|
|
2763
|
+
paths: await generateOpenApiPaths(
|
|
2764
|
+
ctx.app.handlers,
|
|
2765
|
+
ctx.options.openapi?.path ?? ""
|
|
2766
|
+
)
|
|
2797
2767
|
};
|
|
2798
|
-
const configuration = ctx.options.openapi?.scalar || {};
|
|
2799
|
-
return `
|
|
2800
|
-
<!doctype html>
|
|
2801
|
-
<html>
|
|
2802
|
-
<head>
|
|
2803
|
-
<title>API Reference</title>
|
|
2804
|
-
<meta charset="utf-8" />
|
|
2805
|
-
<meta
|
|
2806
|
-
name="viewport"
|
|
2807
|
-
content="width=device-width, initial-scale=1" />
|
|
2808
|
-
<style>.open-api-client-button {display: none!important;}</style>
|
|
2809
|
-
</head>
|
|
2810
|
-
<body>
|
|
2811
|
-
<script id="api-reference" type="application/json" data-configuration="${encode(JSON.stringify(configuration))}">${JSON.stringify(openApi, null, 2)}</script>
|
|
2812
|
-
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
|
2813
|
-
</body>
|
|
2814
|
-
</html> `;
|
|
2815
2768
|
};
|
|
2816
2769
|
|
|
2817
2770
|
// src/middle/preflight.ts
|
|
@@ -3046,7 +2999,7 @@ function isValidMethod(method) {
|
|
|
3046
2999
|
|
|
3047
3000
|
// src/context/node.ts
|
|
3048
3001
|
var chunkArray = (arr) => arr.length > 2 ? [[arr[0], arr[1]], ...chunkArray(arr.slice(2))] : [arr];
|
|
3049
|
-
async function createNode(req, app) {
|
|
3002
|
+
async function createNode(req, app, signal = new AbortController().signal) {
|
|
3050
3003
|
const init = performance.now();
|
|
3051
3004
|
const method = req.method?.toLowerCase() || "get";
|
|
3052
3005
|
if (!isValidMethod(method)) {
|
|
@@ -3080,6 +3033,7 @@ async function createNode(req, app) {
|
|
|
3080
3033
|
body: void 0,
|
|
3081
3034
|
headers: headers2,
|
|
3082
3035
|
cookies: cookies2,
|
|
3036
|
+
signal,
|
|
3083
3037
|
session: {},
|
|
3084
3038
|
init,
|
|
3085
3039
|
app,
|
|
@@ -3120,6 +3074,7 @@ async function createWinter(req, app, server2) {
|
|
|
3120
3074
|
body: void 0,
|
|
3121
3075
|
headers: headers2,
|
|
3122
3076
|
cookies: cookies2,
|
|
3077
|
+
signal: req.signal,
|
|
3123
3078
|
session: {},
|
|
3124
3079
|
init,
|
|
3125
3080
|
app,
|
|
@@ -3157,7 +3112,11 @@ var Node = async (app) => {
|
|
|
3157
3112
|
const http = await import("http");
|
|
3158
3113
|
const server2 = http.createServer(
|
|
3159
3114
|
async (request, response) => {
|
|
3160
|
-
const
|
|
3115
|
+
const controller = new AbortController();
|
|
3116
|
+
response.on("close", () => {
|
|
3117
|
+
if (!response.writableFinished) controller.abort();
|
|
3118
|
+
});
|
|
3119
|
+
const ctx = await createNode(request, app, controller.signal);
|
|
3161
3120
|
if ("error" in ctx) throw ctx.error;
|
|
3162
3121
|
const out = await handleRequest(app, ctx);
|
|
3163
3122
|
response.writeHead(out.status || 200, parseHeaders_default(out.headers));
|
|
@@ -3348,9 +3307,6 @@ var Server = class extends Router {
|
|
|
3348
3307
|
platform;
|
|
3349
3308
|
sockets;
|
|
3350
3309
|
websocket;
|
|
3351
|
-
// Lazily-loaded favicon bytes, cached per server until restart (see favicon
|
|
3352
|
-
// middleware). `undefined` = not loaded yet; `null` = configured but missing.
|
|
3353
|
-
faviconCache;
|
|
3354
3310
|
port;
|
|
3355
3311
|
constructor(options = {}) {
|
|
3356
3312
|
super();
|
|
@@ -3370,13 +3326,12 @@ var Server = class extends Router {
|
|
|
3370
3326
|
app.use(timer);
|
|
3371
3327
|
if (this.settings.cors) app.use(preflight);
|
|
3372
3328
|
app.use(assets);
|
|
3373
|
-
if (this.settings.favicon) app.get("/favicon.ico", favicon);
|
|
3374
3329
|
app.use(session);
|
|
3375
3330
|
if (this.settings.auth) {
|
|
3376
3331
|
auth(app);
|
|
3377
3332
|
}
|
|
3378
3333
|
if (this.settings.openapi) {
|
|
3379
|
-
app.get(this.settings.openapi.path
|
|
3334
|
+
app.get(this.settings.openapi.path, openapi_default);
|
|
3380
3335
|
}
|
|
3381
3336
|
}
|
|
3382
3337
|
self() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.1",
|
|
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",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"server.js"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"build": "bunx tsup src/index.ts --format esm --dts --out-dir . --target node24",
|
|
16
|
+
"build": "bunx tsup src/index.ts --format esm --dts --out-dir . --target node24 --external zod --external @valibot/to-json-schema",
|
|
17
|
+
"prepare": "mkdir -p node_modules/@server && ln -sfn ../.. node_modules/@server/next",
|
|
17
18
|
"start": "bun test --watch",
|
|
18
19
|
"lint": "npx tsc --noEmit && npx @biomejs/biome lint ./src --skip=lint/suspicious/noExplicitAny --skip=lint/style/noParameterAssign --skip=lint/suspicious/noConfusingVoidType --skip=lint/complexity/noBannedTypes",
|
|
19
20
|
"test": "npm run test:bun && tsc --noEmit",
|
|
@@ -54,7 +55,7 @@
|
|
|
54
55
|
"Reply": "docs/4. Reply.md",
|
|
55
56
|
"Authentication": "docs/5. Authentication.md",
|
|
56
57
|
"Testing": "docs/6. Testing.md",
|
|
57
|
-
"
|
|
58
|
+
"Concepts": "docs/7. Concepts.md",
|
|
58
59
|
"FAQ": "docs/8. FAQ.md"
|
|
59
60
|
},
|
|
60
61
|
"tutorials": "docs/tutorials"
|
|
@@ -66,6 +67,7 @@
|
|
|
66
67
|
"devDependencies": {
|
|
67
68
|
"@types/bun": "^1.3.0",
|
|
68
69
|
"@types/node": "^24.10.0",
|
|
70
|
+
"@valibot/to-json-schema": "^1.7.1",
|
|
69
71
|
"arktype": "^2.2.3",
|
|
70
72
|
"bun": "^1.3.13",
|
|
71
73
|
"check-dts": "^0.8.2",
|