@server/next 0.28.6 → 0.28.8

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 CHANGED
@@ -1,3 +1,18 @@
1
+ type LimitOptions = {
2
+ maxSize?: number | string;
3
+ minSize?: number | string;
4
+ fileType?: string[];
5
+ };
6
+ declare class UploadPipeline {
7
+ private _bucket;
8
+ private _limits;
9
+ constructor(bucket?: Bucket | null);
10
+ limit(options: LimitOptions): this;
11
+ store(bucket: Bucket): this;
12
+ processFile(originalName: string, data: Buffer, contentType: string): Promise<UploadedFile>;
13
+ }
14
+ declare function upload(bucket?: Bucket | null): UploadPipeline;
15
+
1
16
  type Method = "get" | "post" | "put" | "patch" | "delete" | "head" | "options" | "socket";
2
17
  type ServerConfig<Session = {}, User = {}> = {
3
18
  Session?: Session;
@@ -28,6 +43,13 @@ type Bucket = {
28
43
  write: (path: string, data: string | Buffer) => Promise<void | string>;
29
44
  delete: (path: string) => Promise<boolean>;
30
45
  };
46
+ type UploadedFile = {
47
+ name: string;
48
+ id: string;
49
+ path: string;
50
+ type: string;
51
+ size: number;
52
+ };
31
53
  type CorsSettings = {
32
54
  origin: string | boolean;
33
55
  methods: string;
@@ -88,7 +110,7 @@ type Options = {
88
110
  secret?: string;
89
111
  views?: string | Bucket;
90
112
  public?: string | Bucket;
91
- uploads?: string | Bucket;
113
+ uploads?: string | Bucket | UploadPipeline;
92
114
  store?: KVStore;
93
115
  cookies?: KVStore;
94
116
  session?: KVStore | {
@@ -104,7 +126,7 @@ type Settings = {
104
126
  secret: string;
105
127
  views?: Bucket;
106
128
  public?: Bucket;
107
- uploads?: Bucket;
129
+ uploads?: Bucket | UploadPipeline;
108
130
  store?: KVStore;
109
131
  cookies?: KVStore;
110
132
  session?: {
@@ -402,4 +424,4 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
402
424
  }
403
425
  declare function server<Session extends Record<string, any> = {}, User extends Record<string, any> = {}>(options?: Options): Server<ServerConfig<Session, User>>;
404
426
 
405
- export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type Bucket, type BunEnv, type Context, type Cookie, type CorsSettings, type EventCallback, type ExtractPathParams, type InferParamType, type InlineReply, type KVStore, type Method, type Middleware, type Options, type ParamTypeMap, type ParamsToObject, type PathToParams, type Platform, type Provider, type RouteOptions, type RouterMethod, type SerializableValue, Server, type ServerConfig, TypedServerError as ServerError, type Settings, type Strategy, type Time, cookies, server as default, download, file, headers, json, redirect, router, send, status, type };
427
+ export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type Bucket, type BunEnv, type Context, type Cookie, type CorsSettings, type EventCallback, type ExtractPathParams, type InferParamType, type InlineReply, type KVStore, type LimitOptions, type Method, type Middleware, type Options, type ParamTypeMap, type ParamsToObject, type PathToParams, type Platform, type Provider, type RouteOptions, type RouterMethod, type SerializableValue, Server, type ServerConfig, TypedServerError as ServerError, type Settings, type Strategy, type Time, UploadPipeline, type UploadedFile, cookies, server as default, download, file, headers, json, redirect, router, send, status, type, upload };
package/index.js CHANGED
@@ -197,7 +197,7 @@ var Reply = class {
197
197
  }
198
198
  download(name) {
199
199
  const ext = name?.split(".").pop();
200
- if (type && ext && !this.res.headers.get("content-type")) this.type(ext);
200
+ if (ext && !this.res.headers.get("content-type")) this.type(ext);
201
201
  const filename = name ? `; filename="${encodeURIComponent(name)}"` : "";
202
202
  return this.headers("content-disposition", `attachment${filename}`);
203
203
  }
@@ -542,59 +542,83 @@ function createId(source, size = 16) {
542
542
  return randomId(size);
543
543
  }
544
544
 
545
- // src/helpers/color.ts
546
- var map = {
547
- reset: 0,
548
- bright: 1,
549
- dim: 2,
550
- under: 4,
551
- blink: 5,
552
- reverse: 7,
553
- black: 30,
554
- red: 31,
555
- green: 32,
556
- yellow: 33,
557
- blue: 34,
558
- magenta: 35,
559
- cyan: 36,
560
- white: 37,
561
- bgblack: 40,
562
- bgred: 41,
563
- bggreen: 42,
564
- bgyellow: 43,
565
- bgblue: 44,
566
- bgmagenta: 45,
567
- bgcyan: 46,
568
- bgwhite: 47
569
- };
570
- var replace = (k) => {
571
- if (process.env.NO_COLOR) return "";
572
- if (!(k in map)) throw new Error(`"{${k}}" is not a valid color`);
573
- return `\x1B[${map[k]}m`;
574
- };
575
- function color(str, ...vals) {
576
- if (typeof str === "string") {
577
- return str.replace(/\{(\w+)\}/g, (_m, k) => replace(k)).replace(/\{\/\w*\}/g, () => replace("reset"));
578
- }
579
- return color(str[0] + vals.map((v, i) => v + str[i + 1]).join(""));
545
+ // src/helpers/upload.ts
546
+ function parseBytes(value) {
547
+ if (typeof value === "number") return value;
548
+ const units = {
549
+ b: 1,
550
+ kb: 1024,
551
+ mb: 1024 ** 2,
552
+ gb: 1024 ** 3
553
+ };
554
+ const match = value.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/);
555
+ if (!match) throw new Error(`Invalid size: "${value}"`);
556
+ return parseFloat(match[1]) * (units[match[2]] ?? 1);
580
557
  }
581
-
582
- // src/helpers/debugInfo.ts
583
- var isDebug = process.argv.includes("--debug");
584
- function debugInfo(options, name, cb, icon = "") {
585
- if (!isDebug) return;
586
- if (!options[name]) {
587
- console.log(color`options:${String(name)}\t→ {dim}[not set]{/}`);
588
- return;
558
+ function getExt(filename) {
559
+ const i = filename.lastIndexOf(".");
560
+ if (i <= 0) return ".bin";
561
+ return filename.slice(i).toLowerCase();
562
+ }
563
+ async function saveFileToBucket(originalName, data, bucket, contentType) {
564
+ const ext = getExt(originalName);
565
+ const id = `${createId()}${ext}`;
566
+ const path2 = await bucket.write(id, data);
567
+ return { name: originalName, id, path: path2, type: contentType, size: data.length };
568
+ }
569
+ var UploadPipeline = class {
570
+ _bucket;
571
+ _limits = {};
572
+ constructor(bucket) {
573
+ this._bucket = bucket ?? null;
574
+ }
575
+ limit(options) {
576
+ this._limits = { ...this._limits, ...options };
577
+ return this;
589
578
  }
590
- console.log(
591
- color`options:${String(name)}\t→ ${icon ? `${icon} ` : ""}${cb(options[name])}`
592
- );
579
+ store(bucket) {
580
+ this._bucket = bucket;
581
+ return this;
582
+ }
583
+ async processFile(originalName, data, contentType) {
584
+ const { maxSize, minSize, fileType } = this._limits;
585
+ if (maxSize !== void 0 && data.length > parseBytes(maxSize)) {
586
+ throw new Error(
587
+ `File "${originalName}" is too large (${data.length} bytes, limit is ${maxSize})`
588
+ );
589
+ }
590
+ if (minSize !== void 0 && data.length < parseBytes(minSize)) {
591
+ throw new Error(
592
+ `File "${originalName}" is too small (${data.length} bytes, minimum is ${minSize})`
593
+ );
594
+ }
595
+ if (fileType && fileType.length > 0) {
596
+ const ext = getExt(originalName);
597
+ const mime = contentType.toLowerCase();
598
+ const allowed = fileType.some(
599
+ (t) => t.toLowerCase() === mime || t.toLowerCase() === ext
600
+ );
601
+ if (!allowed) {
602
+ throw new Error(
603
+ `File type not allowed for "${originalName}" (got "${contentType}", allowed: ${fileType.join(", ")})`
604
+ );
605
+ }
606
+ }
607
+ if (!this._bucket) {
608
+ throw new Error(
609
+ `No destination configured \u2014 pass a bucket to upload() or call .store()`
610
+ );
611
+ }
612
+ return saveFileToBucket(originalName, data, this._bucket, contentType);
613
+ }
614
+ };
615
+ function upload(bucket) {
616
+ return new UploadPipeline(bucket);
593
617
  }
594
618
 
595
619
  // src/helpers/config.ts
596
- var env2 = globalThis.env;
597
620
  function config(options = {}) {
621
+ const env2 = globalThis.env;
598
622
  const settings = {
599
623
  port: options.port || env2.PORT || 3e3,
600
624
  secret: options.secret || env2.SECRET || `unsafe-${createId()}`
@@ -633,27 +657,16 @@ function config(options = {}) {
633
657
  settings.cors = cors2;
634
658
  }
635
659
  settings.views = options.views ? bucket_default(options.views) : null;
636
- debugInfo(options, "views", (views) => views?.location || "true", "\u{1F4C2}");
637
660
  settings.public = options.public ? bucket_default(options.public) : null;
638
- debugInfo(options, "public", (pub) => pub?.location || "true", "\u{1F4C2}");
639
- settings.uploads = options.uploads ? bucket_default(options.uploads) : null;
640
- debugInfo(options, "uploads", (ups) => ups?.location || "true", "\u{1F4C2}");
661
+ settings.uploads = options.uploads instanceof UploadPipeline ? options.uploads : options.uploads ? bucket_default(options.uploads) : null;
641
662
  settings.store = options.store ?? null;
642
- debugInfo(options, "store", (store) => store?.name || "working", "\u{1F4E6}");
643
663
  settings.cookies = options.cookies ?? null;
644
- debugInfo(options, "cookies", (cookies2) => cookies2?.name || "working", "\u{1F36A}");
645
664
  if (options.session) {
646
665
  settings.session = "store" in options.session ? options.session : { store: options.session };
647
666
  }
648
667
  if (options.store && !options.session) {
649
668
  settings.session = { store: options.store.prefix("session:") };
650
669
  }
651
- debugInfo(
652
- options,
653
- "session",
654
- (session2) => session2?.store?.name || "working",
655
- "\u{1F510}"
656
- );
657
670
  if (options.auth || env2.AUTH) {
658
671
  settings.auth = parseAuthOptions(options.auth || env2.AUTH || null, options);
659
672
  }
@@ -795,7 +808,7 @@ function getMachine() {
795
808
 
796
809
  // src/parseResponse.ts
797
810
  async function parseResponse(out, ctx) {
798
- if (!out && typeof out !== "string") return;
811
+ if (!out && typeof out !== "string") return null;
799
812
  if (typeof out === "function") {
800
813
  out = await out(ctx);
801
814
  }
@@ -1071,12 +1084,6 @@ function getMatching(string, regex) {
1071
1084
  const matches = string.match(regex);
1072
1085
  return matches?.[1] ?? "";
1073
1086
  }
1074
- var saveFile = async (name, value, bucket) => {
1075
- const ext = name.split(".").pop();
1076
- const id = `${createId()}.${ext}`;
1077
- await bucket.write(id, value);
1078
- return id;
1079
- };
1080
1087
  function splitBuffer(buffer, delimiter) {
1081
1088
  const result = [];
1082
1089
  let start = 0;
@@ -1090,6 +1097,7 @@ function splitBuffer(buffer, delimiter) {
1090
1097
  return result;
1091
1098
  }
1092
1099
  var BREAK_BUFFER = Buffer.from("\r\n\r\n");
1100
+ var END_BUFFER = Buffer.from("--\r\n");
1093
1101
  function isProbablyText(buffer) {
1094
1102
  for (let i = 0; i < Math.min(buffer.length, 512); i++) {
1095
1103
  const byte = buffer[i];
@@ -1100,7 +1108,7 @@ function isProbablyText(buffer) {
1100
1108
  }
1101
1109
  async function parseBody(raw, contentType, bucket) {
1102
1110
  const contentTypeStr = Array.isArray(contentType) ? contentType[0] : contentType;
1103
- if (!raw) return {};
1111
+ if (!raw || raw.length === 0) return {};
1104
1112
  if (!contentTypeStr || /^text\//.test(contentTypeStr)) {
1105
1113
  return raw.toString("utf-8");
1106
1114
  }
@@ -1113,7 +1121,7 @@ async function parseBody(raw, contentType, bucket) {
1113
1121
  const boundaryBuffer = Buffer.from(`--${boundary}`);
1114
1122
  const parts = splitBuffer(raw, boundaryBuffer);
1115
1123
  for (const part of parts) {
1116
- if (part.length === 0 || part.equals(Buffer.from("--\r\n"))) continue;
1124
+ if (part.length === 0 || part.equals(END_BUFFER)) continue;
1117
1125
  const idx = part.indexOf(BREAK_BUFFER);
1118
1126
  if (idx === -1) continue;
1119
1127
  const headerStr = part.slice(0, idx).toString("utf-8");
@@ -1122,8 +1130,24 @@ async function parseBody(raw, contentType, bucket) {
1122
1130
  if (!name) continue;
1123
1131
  const filename = getMatching(headerStr, /filename="(.+?)"/).trim();
1124
1132
  if (filename) {
1125
- if (!bucket) throw new Error("Bucket is required to save files");
1126
- body[name] = await saveFile(filename, contentBuf, bucket);
1133
+ const partContentType = getMatching(headerStr, /Content-Type:\s*([^\r\n]+)/i).trim() || "application/octet-stream";
1134
+ if (!bucket) {
1135
+ continue;
1136
+ }
1137
+ if (bucket instanceof UploadPipeline) {
1138
+ body[name] = await bucket.processFile(
1139
+ filename,
1140
+ contentBuf,
1141
+ partContentType
1142
+ );
1143
+ } else {
1144
+ body[name] = await saveFileToBucket(
1145
+ filename,
1146
+ contentBuf,
1147
+ bucket,
1148
+ partContentType
1149
+ );
1150
+ }
1127
1151
  } else {
1128
1152
  const value = isProbablyText(contentBuf) ? contentBuf.toString("utf-8").trim() : contentBuf;
1129
1153
  if (body[name]) {
@@ -1749,9 +1773,9 @@ async function createWinter(req, app) {
1749
1773
  }
1750
1774
 
1751
1775
  // src/context/handlers.ts
1752
- var Winter = async (app, request, env3) => {
1753
- if (env3?.upgrade(request)) return;
1754
- Object.assign(globalThis.env, env3);
1776
+ var Winter = async (app, request, env2) => {
1777
+ if (env2?.upgrade(request)) return;
1778
+ Object.assign(globalThis.env, env2);
1755
1779
  const ctx = await createWinter(request, app);
1756
1780
  const res = await handleRequest(app.handlers, ctx);
1757
1781
  ctx.events.trigger("finish", { ...ctx, res, end: performance.now() });
@@ -1965,8 +1989,8 @@ var Server = class extends Router {
1965
1989
  node() {
1966
1990
  return Node(this);
1967
1991
  }
1968
- fetch(request, env3) {
1969
- return Winter(this, request, env3);
1992
+ fetch(request, env2) {
1993
+ return Winter(this, request, env2);
1970
1994
  }
1971
1995
  callback(request, context) {
1972
1996
  return Netlify(this, request, context);
@@ -1991,5 +2015,6 @@ export {
1991
2015
  router,
1992
2016
  send,
1993
2017
  status,
1994
- type
2018
+ type,
2019
+ upload
1995
2020
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.28.6",
3
+ "version": "0.28.8",
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": "https://github.com/franciscop/server-next.git",
@@ -27,13 +27,13 @@ const altAttrs = {
27
27
  const isValidChild = (child) =>
28
28
  child != null && child !== false && child !== true;
29
29
 
30
- // CSS helpers
31
- const escapeCSS = (value) => String(value).replace(/[<>&"'`]/g, "\\$&");
32
-
33
30
  const minifyCss = (str) =>
34
31
  str
35
- .replace(/\s+/g, " ")
36
32
  .replace(/\/\*[\s\S]*?\*\//g, "")
33
+ .replace(/\s+/g, " ")
34
+ .replace(/\s*([{}:;,>~+])\s*/g, "$1")
35
+ .replace(/;}/g, "}")
36
+ .replace(/<\/style>/gi, "<\\/style>")
37
37
  .trim();
38
38
 
39
39
  // React element detection (safe for custom objects too)
@@ -104,9 +104,9 @@ const jsx = (tag, { children, ...props } = {}) => {
104
104
  children = raw(content);
105
105
  }
106
106
 
107
- // style: minified + escaped raw content
107
+ // style: minified raw content
108
108
  if (tag === "style" && typeof children === "string") {
109
- children = raw(minifyCss(escapeCSS(children)));
109
+ children = raw(minifyCss(children));
110
110
  }
111
111
 
112
112
  // dangerouslySetInnerHTML
@@ -299,20 +299,50 @@ describe("script tag", () => {
299
299
  describe("style tag", () => {
300
300
  it("renders style content", () => {
301
301
  expect(<style>{"body { color: red; }"}</style>).toRender(
302
- "<style>body { color: red; }</style>",
302
+ "<style>body{color:red}</style>",
303
303
  );
304
304
  });
305
305
 
306
306
  it("minifies whitespace in style content", () => {
307
307
  expect(
308
308
  <style>{"body {\n color: red;\n margin: 0;\n}"}</style>,
309
- ).toRender("<style>body { color: red; margin: 0; }</style>");
309
+ ).toRender("<style>body{color:red;margin:0}</style>");
310
310
  });
311
311
 
312
312
  it("strips CSS comments", () => {
313
313
  expect(
314
314
  <style>{"/* reset */ body { margin: 0; } /* end */"}</style>,
315
- ).toRender("<style>body { margin: 0; }</style>");
315
+ ).toRender("<style>body{margin:0}</style>");
316
+ });
317
+
318
+ it("preserves child combinator in selectors", () => {
319
+ expect(
320
+ <style>{":not(pre) > code { background: none; }"}</style>,
321
+ ).toRender("<style>:not(pre)>code{background:none}</style>");
322
+ });
323
+
324
+ it("preserves sibling combinators in selectors", () => {
325
+ expect(
326
+ <style>{"h2 + p { margin: 0; } h2 ~ p { color: red; }"}</style>,
327
+ ).toRender("<style>h2+p{margin:0}h2~p{color:red}</style>");
328
+ });
329
+
330
+ it("removes spaces around braces, colons, and semicolons", () => {
331
+ expect(
332
+ <style>{"a { color : red ; font-size : 1em ; }"}</style>,
333
+ ).toRender("<style>a{color:red;font-size:1em}</style>");
334
+ });
335
+
336
+ it("removes trailing semicolon before closing brace", () => {
337
+ expect(
338
+ <style>{"p { margin: 0; padding: 0; }"}</style>,
339
+ ).toRender("<style>p{margin:0;padding:0}</style>");
340
+ });
341
+
342
+ it("does not break </style> injection", () => {
343
+ expect(
344
+ <style>{"a { content: '</style>'; }"}</style>,
345
+ ).toRender("<style>a{content:'<\\/style>'}</style>");
316
346
  });
317
347
  });
318
348