@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.
Files changed (3) hide show
  1. package/index.d.ts +26 -23
  2. package/index.js +99 -144
  3. 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?: any;
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?: any;
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
- } & ContextExtension;
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 ext2 = getExt(originalName);
190
- const id = `${createId()}${ext2}`;
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 ext2 = getExt(originalName);
213
+ const ext = getExt(originalName);
214
214
  const mime = contentType.toLowerCase();
215
215
  const allowed = fileType2.some(
216
- (t) => t.toLowerCase() === mime || t.toLowerCase() === ext2
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 ext2 in mimes_default) extByMime[mimes_default[ext2]] = ext2;
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 ext2 = extByMime[base];
360
- if (ext2) return `.${ext2}`;
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 clean = name.replace(/[\r\n]/g, "").split(/[\\/]/).pop() || "";
774
- if (!clean) return "attachment";
775
- const ascii = clean.replace(/[^\x20-\x7e]/g, "?");
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 (clean === ascii) return value;
778
- return `${value}; filename*=UTF-8''${encodeExt(clean)}`;
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 ext2 = name.split(".").pop()?.toLowerCase();
786
- return ext2 ? mimes_default[ext2] : void 0;
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 ext2 = name?.split(".").pop();
821
- if (ext2 && !this.res.headers.get("content-type")) this.type(ext2);
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 ext2 = path.split(".").pop();
879
+ const ext = path.split(".").pop();
880
880
  await fs.promises.access(path);
881
881
  const stream = fs.createReadStream(path);
882
- return this.type(ext2).send(stream);
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
- if (options.openapi === true) {
1917
- settings.openapi = {};
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 || "/docs");
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
- for (const route of app.handlers[ctx.method]) {
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
- app.post("/auth/logout", logout);
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 ext2 = ctx.url.pathname.split(".").pop()?.toLowerCase();
2599
- const ctype = ext2 && mimes_default[ext2] || meta?.type || ext2;
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
- "&": "&amp;",
2665
- "<": "&lt;",
2666
- ">": "&gt;",
2667
- '"': "&quot;"
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 getTag = (name, fn) => {
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 === "/docs" || !fn) {
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 = zodToSchema(meta.body);
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 = zodToSchema(meta.response);
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
- Object.entries(meta.query).map(([key, value]) => ({
2766
- name: key,
2767
- in: "query",
2768
- required: false,
2769
- schema: { type: typeof value },
2770
- example: value
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 || getTag("@title", fn) || `${method.toUpperCase()} ${normalizedPath}`,
2776
- description: config2.description || getTitle(fn) || getDescription(fn),
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
- const openApi = {
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(ctx.app.handlers)
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 ctx = await createNode(request, app);
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 || "/docs", openapi_default);
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.43.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
- "Platforms": "docs/7. Platforms.md",
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",