@server/next 0.40.0 → 0.40.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.40.0",
3
+ "version": "0.40.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",
@@ -65,7 +65,7 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "bucket": "^0.7.1",
68
- "polystore": "^0.23.3"
68
+ "polystore": "^0.24.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/bun": "^1.3.0",
@@ -38,14 +38,30 @@ const encode = (str = "") => {
38
38
  const isValidChild = (child) =>
39
39
  child != null && child !== false && child !== true;
40
40
 
41
- const minifyCss = (str) =>
42
- str
41
+ // A `</style>` inside the CSS would close the element early and let whatever
42
+ // follows run as HTML. The content is never encoded, so this runs on every
43
+ // <style>, minified or not.
44
+ const escapeCss = (str) => str.replace(/<\/style>/gi, "<\\/style>");
45
+
46
+ const minifyCss = (str) => {
47
+ // Quoted values are content, not formatting: the spaces and delimiters in
48
+ // `content: ", "` have to survive. They're set aside while the rest is
49
+ // squeezed, then put back. Comments go first and unconditionally, so one
50
+ // written inside a string is stripped along with the rest.
51
+ const quoted = [];
52
+ return str
43
53
  .replace(/\/\*[\s\S]*?\*\//g, "")
54
+ .replace(
55
+ /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g,
56
+ (match) => `\0${quoted.push(match) - 1}\0`,
57
+ )
44
58
  .replace(/\s+/g, " ")
45
- .replace(/\s*([{}:;,>~+])\s*/g, "$1")
59
+ // `+` is left out on purpose for `calc(100% + 10px)`
60
+ .replace(/\s*([{}:;,>~])\s*/g, "$1")
46
61
  .replace(/;}/g, "}")
47
- .replace(/<\/style>/gi, "<\\/style>")
62
+ .replace(/\0(\d+)\0/g, (_, i) => quoted[i])
48
63
  .trim();
64
+ };
49
65
 
50
66
  // React element detection (safe for custom objects too)
51
67
  const isReactElement = (val) =>
@@ -115,9 +131,9 @@ const jsx = (tag, { children, ...props } = {}) => {
115
131
  children = raw(content);
116
132
  }
117
133
 
118
- // style: minified raw content
134
+ // style: raw content, minified only when asked for with <style minify>
119
135
  if (tag === "style" && typeof children === "string") {
120
- children = raw(minifyCss(children));
136
+ children = raw(escapeCss(props?.minify ? minifyCss(children) : children));
121
137
  }
122
138
 
123
139
  // dangerouslySetInnerHTML
@@ -139,6 +155,8 @@ const jsx = (tag, { children, ...props } = {}) => {
139
155
  // attributes
140
156
  let attrStr = Object.entries(props || {})
141
157
  .filter(([k]) => k !== "dangerouslySetInnerHTML")
158
+ // `minify` drives <style>, it isn't an HTML attribute
159
+ .filter(([k]) => !(tag === "style" && k === "minify"))
142
160
  .filter(([k, v]) => !/on[A-Z]/.test(k) && typeof v !== "function")
143
161
  .filter(([, v]) => v !== false)
144
162
  .map(([k, v]) => {
@@ -299,52 +299,115 @@ describe("script tag", () => {
299
299
  });
300
300
 
301
301
  describe("style tag", () => {
302
- it("renders style content", () => {
302
+ it("renders the CSS as written", () => {
303
303
  expect(<style>{"body { color: red; }"}</style>).toRender(
304
- "<style>body{color:red}</style>",
304
+ "<style>body { color: red; }</style>",
305
305
  );
306
306
  });
307
307
 
308
- it("minifies whitespace in style content", () => {
309
- expect(
310
- <style>{"body {\n color: red;\n margin: 0;\n}"}</style>,
311
- ).toRender("<style>body{color:red;margin:0}</style>");
312
- });
313
-
314
- it("strips CSS comments", () => {
315
- expect(
316
- <style>{"/* reset */ body { margin: 0; } /* end */"}</style>,
317
- ).toRender("<style>body{margin:0}</style>");
318
- });
319
-
320
- it("preserves child combinator in selectors", () => {
321
- expect(<style>{":not(pre) > code { background: none; }"}</style>).toRender(
322
- "<style>:not(pre)>code{background:none}</style>",
308
+ it("keeps comments and whitespace without `minify`", () => {
309
+ expect(<style>{"/* reset */ body {\n margin: 0;\n}"}</style>).toRender(
310
+ "<style>/* reset */ body {\n margin: 0;\n}</style>",
323
311
  );
324
312
  });
325
313
 
326
- it("preserves sibling combinators in selectors", () => {
327
- expect(
328
- <style>{"h2 + p { margin: 0; } h2 ~ p { color: red; }"}</style>,
329
- ).toRender("<style>h2+p{margin:0}h2~p{color:red}</style>");
314
+ it("sends the content raw, like <script>", () => {
315
+ // Encoding it would break the CSS, so user input must never reach here
316
+ expect(<style>{"a::after { content: '>'; }"}</style>).toRender(
317
+ "<style>a::after { content: '>'; }</style>",
318
+ );
330
319
  });
331
320
 
332
- it("removes spaces around braces, colons, and semicolons", () => {
333
- expect(<style>{"a { color : red ; font-size : 1em ; }"}</style>).toRender(
334
- "<style>a{color:red;font-size:1em}</style>",
321
+ it("escapes </style> either way, so it can't close the tag early", () => {
322
+ expect(<style>{"a { content: '</style>'; }"}</style>).toRender(
323
+ "<style>a { content: '<\\/style>'; }</style>",
324
+ );
325
+ expect(<style minify>{"a { content: '</style>'; }"}</style>).toRender(
326
+ "<style>a{content:'<\\/style>'}</style>",
335
327
  );
336
328
  });
337
329
 
338
- it("removes trailing semicolon before closing brace", () => {
339
- expect(<style>{"p { margin: 0; padding: 0; }"}</style>).toRender(
340
- "<style>p{margin:0;padding:0}</style>",
330
+ it("never renders `minify` as an attribute", () => {
331
+ expect(<style minify>{"a { color: red; }"}</style>).toRender(
332
+ "<style>a{color:red}</style>",
341
333
  );
342
334
  });
343
335
 
344
- it("does not break </style> injection", () => {
345
- expect(<style>{"a { content: '</style>'; }"}</style>).toRender(
346
- "<style>a{content:'<\\/style>'}</style>",
347
- );
336
+ describe("with `minify`", () => {
337
+ it("minifies whitespace in style content", () => {
338
+ expect(
339
+ <style minify>{"body {\n color: red;\n margin: 0;\n}"}</style>,
340
+ ).toRender("<style>body{color:red;margin:0}</style>");
341
+ });
342
+
343
+ it("strips CSS comments", () => {
344
+ expect(
345
+ <style minify>{"/* reset */ body { margin: 0; } /* end */"}</style>,
346
+ ).toRender("<style>body{margin:0}</style>");
347
+ });
348
+
349
+ it("preserves child combinator in selectors", () => {
350
+ expect(
351
+ <style minify>{":not(pre) > code { background: none; }"}</style>,
352
+ ).toRender("<style>:not(pre)>code{background:none}</style>");
353
+ });
354
+
355
+ it("preserves sibling combinators in selectors", () => {
356
+ // `+` keeps a single space, since removing it breaks calc() below
357
+ expect(
358
+ <style minify>
359
+ {"h2 + p { margin: 0; } h2 ~ p { color: red; }"}
360
+ </style>,
361
+ ).toRender("<style>h2 + p{margin:0}h2~p{color:red}</style>");
362
+ });
363
+
364
+ it("keeps the spaces calc() needs around + and -", () => {
365
+ // `calc(100%+10px)` is invalid CSS: the operators need their whitespace
366
+ expect(<style minify>{"a { top: calc(100% + 10px); }"}</style>).toRender(
367
+ "<style>a{top:calc(100% + 10px)}</style>",
368
+ );
369
+ expect(<style minify>{"a { top: calc(100% - 10px); }"}</style>).toRender(
370
+ "<style>a{top:calc(100% - 10px)}</style>",
371
+ );
372
+ });
373
+
374
+ it("removes spaces around braces, colons, and semicolons", () => {
375
+ expect(
376
+ <style minify>{"a { color : red ; font-size : 1em ; }"}</style>,
377
+ ).toRender("<style>a{color:red;font-size:1em}</style>");
378
+ });
379
+
380
+ it("removes trailing semicolon before closing brace", () => {
381
+ expect(<style minify>{"p { margin: 0; padding: 0; }"}</style>).toRender(
382
+ "<style>p{margin:0;padding:0}</style>",
383
+ );
384
+ });
385
+
386
+ it("leaves quoted values alone", () => {
387
+ // The spaces and delimiters in here are content, not formatting
388
+ expect(<style minify>{'a::after { content: ", "; }'}</style>).toRender(
389
+ '<style>a::after{content:", "}</style>',
390
+ );
391
+ expect(
392
+ <style minify>{'a::after { content: "a; b: c"; }'}</style>,
393
+ ).toRender('<style>a::after{content:"a; b: c"}</style>');
394
+ expect(
395
+ <style minify>{"a::after { content: 'x y'; }"}</style>,
396
+ ).toRender("<style>a::after{content:'x y'}</style>");
397
+ });
398
+
399
+ it("handles an escaped quote inside a value", () => {
400
+ expect(
401
+ <style minify>{'a::after { content: "say \\" hi"; }'}</style>,
402
+ ).toRender('<style>a::after{content:"say \\" hi"}</style>');
403
+ });
404
+
405
+ it("still strips a comment written inside a value", () => {
406
+ // Comments are stripped first, unconditionally
407
+ expect(
408
+ <style minify>{'a::after { content: "/* hi */"; }'}</style>,
409
+ ).toRender('<style>a::after{content:""}</style>');
410
+ });
348
411
  });
349
412
  });
350
413